diff --git a/README.md b/README.md index a877c6fce..969240feb 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ yuzu emulator early access ============= -This is the source code for early-access 2819. +This is the source code for early-access 2820. ## Legal Notice diff --git a/externals/cubeb/CMakeLists.txt b/externals/cubeb/CMakeLists.txt index 6cd808e5a..9cfae641b 100755 --- a/externals/cubeb/CMakeLists.txt +++ b/externals/cubeb/CMakeLists.txt @@ -139,7 +139,7 @@ if(NOT BUNDLE_SPEEX) endif() if(NOT TARGET speex) - add_library(speex STATIC subprojects/speex/resample.c) + add_library(speex OBJECT subprojects/speex/resample.c) set_target_properties(speex PROPERTIES POSITION_INDEPENDENT_CODE TRUE) target_include_directories(speex INTERFACE subprojects) target_compile_definitions(speex PUBLIC @@ -259,7 +259,7 @@ if(USE_WASAPI) target_sources(cubeb PRIVATE src/cubeb_wasapi.cpp) target_compile_definitions(cubeb PRIVATE USE_WASAPI) - target_link_libraries(cubeb PRIVATE avrt ole32) + target_link_libraries(cubeb PRIVATE avrt ole32 ksuser) endif() check_include_files("windows.h;mmsystem.h" USE_WINMM) @@ -284,9 +284,22 @@ if(HAVE_SYS_SOUNDCARD_H) try_compile(USE_OSS "${PROJECT_BINARY_DIR}/compile_tests" ${PROJECT_SOURCE_DIR}/cmake/compile_tests/oss_is_v4.c) if(USE_OSS) - target_sources(cubeb PRIVATE - src/cubeb_oss.c) - target_compile_definitions(cubeb PRIVATE USE_OSS) + # strlcpy is not available on BSD systems that use glibc, + # like Debian kfreebsd, so try using libbsd if available + include(CheckSymbolExists) + check_symbol_exists(strlcpy string.h HAVE_STRLCPY) + if(NOT HAVE_STRLCPY) + pkg_check_modules(libbsd-overlay IMPORTED_TARGET libbsd-overlay) + if(libbsd-overlay_FOUND) + target_link_libraries(cubeb PRIVATE PkgConfig::libbsd-overlay) + set(HAVE_STRLCPY true) + endif() + endif() + if (HAVE_STRLCPY) + target_sources(cubeb PRIVATE + src/cubeb_oss.c) + target_compile_definitions(cubeb PRIVATE USE_OSS) + endif() endif() endif() @@ -367,7 +380,7 @@ if(BUILD_TESTS) add_executable(test_${NAME} test/test_${NAME}.cpp) target_include_directories(test_${NAME} PRIVATE ${gtest_SOURCE_DIR}/include src) target_link_libraries(test_${NAME} PRIVATE cubeb gtest_main) - add_test(${NAME} test_${NAME}) + add_test(${NAME} test_${NAME} --gtest_death_test_style=threadsafe) add_sanitizers(test_${NAME}) install(TARGETS test_${NAME}) endmacro(cubeb_add_test) diff --git a/externals/cubeb/INSTALL.md b/externals/cubeb/INSTALL.md index f8492243a..f4229c051 100755 --- a/externals/cubeb/INSTALL.md +++ b/externals/cubeb/INSTALL.md @@ -2,7 +2,7 @@ You must have CMake v3.1 or later installed. -1. `git clone --recursive https://github.com/kinetiknz/cubeb.git` +1. `git clone --recursive https://github.com/mozilla/cubeb.git` 2. `mkdir cubeb-build` 3. `cd cubeb-build` 3. `cmake ../cubeb` diff --git a/externals/cubeb/src/cubeb_aaudio.cpp b/externals/cubeb/src/cubeb_aaudio.cpp index b12c5966f..6076f1d21 100755 --- a/externals/cubeb/src/cubeb_aaudio.cpp +++ b/externals/cubeb/src/cubeb_aaudio.cpp @@ -935,7 +935,8 @@ aaudio_stream_init_impl(cubeb_stream * stm, cubeb_devid input_device, stm->resampler = cubeb_resampler_create( stm, input_stream_params ? &in_params : NULL, output_stream_params ? &out_params : NULL, target_sample_rate, - stm->data_callback, stm->user_ptr, CUBEB_RESAMPLER_QUALITY_DEFAULT); + stm->data_callback, stm->user_ptr, CUBEB_RESAMPLER_QUALITY_DEFAULT, + CUBEB_RESAMPLER_RECLOCK_NONE); if (!stm->resampler) { LOG("Failed to create resampler"); diff --git a/externals/cubeb/src/cubeb_audiounit.cpp b/externals/cubeb/src/cubeb_audiounit.cpp index 02cd13469..8f2741ace 100755 --- a/externals/cubeb/src/cubeb_audiounit.cpp +++ b/externals/cubeb/src/cubeb_audiounit.cpp @@ -1442,6 +1442,13 @@ audiounit_destroy(cubeb * ctx) audiounit_active_streams(ctx)); } + // Destroying a cubeb context with device collection callbacks registered + // is misuse of the API, assert then attempt to clean up. + assert(!ctx->input_collection_changed_callback && + !ctx->input_collection_changed_user_ptr && + !ctx->output_collection_changed_callback && + !ctx->output_collection_changed_user_ptr); + /* Unregister the callback if necessary. */ if (ctx->input_collection_changed_callback) { audiounit_remove_device_listener(ctx, CUBEB_DEVICE_TYPE_INPUT); @@ -2700,7 +2707,8 @@ audiounit_setup_stream(cubeb_stream * stm) stm->resampler.reset(cubeb_resampler_create( stm, has_input(stm) ? &input_unconverted_params : NULL, has_output(stm) ? &stm->output_stream_params : NULL, target_sample_rate, - stm->data_callback, stm->user_ptr, CUBEB_RESAMPLER_QUALITY_DESKTOP)); + stm->data_callback, stm->user_ptr, CUBEB_RESAMPLER_QUALITY_DESKTOP, + CUBEB_RESAMPLER_RECLOCK_NONE)); if (!stm->resampler) { LOG("(%p) Could not create resampler.", stm); return CUBEB_ERROR; diff --git a/externals/cubeb/src/cubeb_jack.cpp b/externals/cubeb/src/cubeb_jack.cpp index e6d948f1a..3b3056c64 100755 --- a/externals/cubeb/src/cubeb_jack.cpp +++ b/externals/cubeb/src/cubeb_jack.cpp @@ -925,15 +925,18 @@ cbjack_stream_init(cubeb * context, cubeb_stream ** stream, if (stm->devs == DUPLEX) { stm->resampler = cubeb_resampler_create( stm, &stm->in_params, &stm->out_params, stream_actual_rate, - stm->data_callback, stm->user_ptr, CUBEB_RESAMPLER_QUALITY_DESKTOP); + stm->data_callback, stm->user_ptr, CUBEB_RESAMPLER_QUALITY_DESKTOP, + CUBEB_RESAMPLER_RECLOCK_NONE); } else if (stm->devs == IN_ONLY) { stm->resampler = cubeb_resampler_create( stm, &stm->in_params, nullptr, stream_actual_rate, stm->data_callback, - stm->user_ptr, CUBEB_RESAMPLER_QUALITY_DESKTOP); + stm->user_ptr, CUBEB_RESAMPLER_QUALITY_DESKTOP, + CUBEB_RESAMPLER_RECLOCK_NONE); } else if (stm->devs == OUT_ONLY) { stm->resampler = cubeb_resampler_create( stm, nullptr, &stm->out_params, stream_actual_rate, stm->data_callback, - stm->user_ptr, CUBEB_RESAMPLER_QUALITY_DESKTOP); + stm->user_ptr, CUBEB_RESAMPLER_QUALITY_DESKTOP, + CUBEB_RESAMPLER_RECLOCK_NONE); } if (!stm->resampler) { diff --git a/externals/cubeb/src/cubeb_opensl.c b/externals/cubeb/src/cubeb_opensl.c index 78096d5dd..e5969984b 100755 --- a/externals/cubeb/src/cubeb_opensl.c +++ b/externals/cubeb/src/cubeb_opensl.c @@ -1479,7 +1479,8 @@ opensl_stream_init(cubeb * ctx, cubeb_stream ** stream, stm->resampler = cubeb_resampler_create( stm, input_stream_params ? &input_params : NULL, output_stream_params ? &output_params : NULL, target_sample_rate, - data_callback, user_ptr, CUBEB_RESAMPLER_QUALITY_DEFAULT); + data_callback, user_ptr, CUBEB_RESAMPLER_QUALITY_DEFAULT, + CUBEB_RESAMPLER_RECLOCK_NONE); if (!stm->resampler) { LOG("Failed to create resampler"); opensl_stream_destroy(stm); diff --git a/externals/cubeb/src/cubeb_oss.c b/externals/cubeb/src/cubeb_oss.c index c85aec554..88f8582a0 100755 --- a/externals/cubeb/src/cubeb_oss.c +++ b/externals/cubeb/src/cubeb_oss.c @@ -96,9 +96,8 @@ struct oss_stream { oss_devnode_t name; int fd; void * buf; - unsigned int nfr; /* Number of frames allocated */ - unsigned int nfrags; unsigned int bufframes; + unsigned int maxframes; struct stream_info { int channels; @@ -824,9 +823,9 @@ retry: pfds[0].fd = s->play.fd; pfds[1].fd = -1; goto retry; - } else if (tnfr > (long)s->play.bufframes) { + } else if (tnfr > (long)s->play.maxframes) { /* too many frames available - limit */ - tnfr = (long)s->play.bufframes; + tnfr = (long)s->play.maxframes; } if (nfr > tnfr) { nfr = tnfr; @@ -842,9 +841,9 @@ retry: pfds[0].fd = -1; pfds[1].fd = s->record.fd; goto retry; - } else if (tnfr > (long)s->record.bufframes) { + } else if (tnfr > (long)s->record.maxframes) { /* too many frames available - limit */ - tnfr = (long)s->record.bufframes; + tnfr = (long)s->record.maxframes; } if (nfr > tnfr) { nfr = tnfr; @@ -1061,6 +1060,8 @@ oss_stream_init(cubeb * context, cubeb_stream ** stream, } if (input_stream_params != NULL) { unsigned int nb_channels; + uint32_t minframes; + if (input_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { LOG("Loopback not supported"); ret = CUBEB_ERROR_NOT_SUPPORTED; @@ -1089,18 +1090,17 @@ oss_stream_init(cubeb * context, cubeb_stream ** stream, (input_stream_params->format == CUBEB_SAMPLE_FLOAT32NE); s->record.frame_size = s->record.info.channels * (s->record.info.precision / 8); - s->record.nfrags = OSS_NFRAGS; - s->record.nfr = latency_frames / OSS_NFRAGS; - s->record.bufframes = s->record.nfrags * s->record.nfr; - uint32_t minnfr; - oss_get_min_latency(context, *input_stream_params, &minnfr); - if (s->record.nfr < minnfr) { - s->record.nfr = minnfr; - s->record.nfrags = latency_frames / minnfr; + s->record.bufframes = latency_frames; + + oss_get_min_latency(context, *input_stream_params, &minframes); + if (s->record.bufframes < minframes) { + s->record.bufframes = minframes; } } if (output_stream_params != NULL) { unsigned int nb_channels; + uint32_t minframes; + if (output_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { LOG("Loopback not supported"); ret = CUBEB_ERROR_NOT_SUPPORTED; @@ -1128,19 +1128,16 @@ oss_stream_init(cubeb * context, cubeb_stream ** stream, } s->play.floating = (output_stream_params->format == CUBEB_SAMPLE_FLOAT32NE); s->play.frame_size = s->play.info.channels * (s->play.info.precision / 8); - s->play.nfrags = OSS_NFRAGS; - s->play.nfr = latency_frames / OSS_NFRAGS; - uint32_t minnfr; - oss_get_min_latency(context, *output_stream_params, &minnfr); - if (s->play.nfr < minnfr) { - s->play.nfr = minnfr; - s->play.nfrags = latency_frames / minnfr; + s->play.bufframes = latency_frames; + + oss_get_min_latency(context, *output_stream_params, &minframes); + if (s->play.bufframes < minframes) { + s->play.bufframes = minframes; } - s->play.bufframes = s->play.nfrags * s->play.nfr; } if (s->play.fd != -1) { int frag = oss_get_frag_params( - oss_calc_frag_shift(s->play.nfr, s->play.frame_size)); + oss_calc_frag_shift(s->play.bufframes, s->play.frame_size)); if (ioctl(s->play.fd, SNDCTL_DSP_SETFRAGMENT, &frag)) LOG("Failed to set play fd with SNDCTL_DSP_SETFRAGMENT. frag: 0x%x", frag); @@ -1148,19 +1145,28 @@ oss_stream_init(cubeb * context, cubeb_stream ** stream, if (ioctl(s->play.fd, SNDCTL_DSP_GETOSPACE, &bi)) LOG("Failed to get play fd's buffer info."); else { - s->play.nfr = bi.fragsize / s->play.frame_size; - s->play.nfrags = bi.fragments; - s->play.bufframes = s->play.nfr * s->play.nfrags; + s->play.bufframes = (bi.fragsize * bi.fragstotal) / s->play.frame_size; } + int lw; - int lw = s->play.frame_size; + /* + * Force 32 ms service intervals at most, or when recording is + * active, use the recording service intervals as a reference. + */ + s->play.maxframes = (32 * output_stream_params->rate) / 1000; + if (s->record.fd != -1 || s->play.maxframes >= s->play.bufframes) { + lw = s->play.frame_size; /* Feed data when possible. */ + s->play.maxframes = s->play.bufframes; + } else { + lw = (s->play.bufframes - s->play.maxframes) * s->play.frame_size; + } if (ioctl(s->play.fd, SNDCTL_DSP_LOW_WATER, &lw)) LOG("Audio device \"%s\" (play) could not set trigger threshold", s->play.name); } if (s->record.fd != -1) { int frag = oss_get_frag_params( - oss_calc_frag_shift(s->record.nfr, s->record.frame_size)); + oss_calc_frag_shift(s->record.bufframes, s->record.frame_size)); if (ioctl(s->record.fd, SNDCTL_DSP_SETFRAGMENT, &frag)) LOG("Failed to set record fd with SNDCTL_DSP_SETFRAGMENT. frag: 0x%x", frag); @@ -1168,11 +1174,11 @@ oss_stream_init(cubeb * context, cubeb_stream ** stream, if (ioctl(s->record.fd, SNDCTL_DSP_GETISPACE, &bi)) LOG("Failed to get record fd's buffer info."); else { - s->record.nfr = bi.fragsize / s->record.frame_size; - s->record.nfrags = bi.fragments; - s->record.bufframes = s->record.nfr * s->record.nfrags; + s->record.bufframes = + (bi.fragsize * bi.fragstotal) / s->record.frame_size; } + s->record.maxframes = s->record.bufframes; int lw = s->record.frame_size; if (ioctl(s->record.fd, SNDCTL_DSP_LOW_WATER, &lw)) LOG("Audio device \"%s\" (record) could not set trigger threshold", diff --git a/externals/cubeb/src/cubeb_pulse.c b/externals/cubeb/src/cubeb_pulse.c index 438a695e1..3ed6bdc52 100755 --- a/externals/cubeb/src/cubeb_pulse.c +++ b/externals/cubeb/src/cubeb_pulse.c @@ -783,6 +783,10 @@ pulse_context_destroy(cubeb * ctx) static void pulse_destroy(cubeb * ctx) { + assert(!ctx->input_collection_changed_callback && + !ctx->input_collection_changed_user_ptr && + !ctx->output_collection_changed_callback && + !ctx->output_collection_changed_user_ptr); free(ctx->context_name); if (ctx->context) { pulse_context_destroy(ctx); diff --git a/externals/cubeb/src/cubeb_resampler.cpp b/externals/cubeb/src/cubeb_resampler.cpp index d61b1daf2..c31944b82 100755 --- a/externals/cubeb/src/cubeb_resampler.cpp +++ b/externals/cubeb/src/cubeb_resampler.cpp @@ -323,7 +323,8 @@ cubeb_resampler_create(cubeb_stream * stream, cubeb_stream_params * input_params, cubeb_stream_params * output_params, unsigned int target_rate, cubeb_data_callback callback, - void * user_ptr, cubeb_resampler_quality quality) + void * user_ptr, cubeb_resampler_quality quality, + cubeb_resampler_reclock reclock) { cubeb_sample_format format; @@ -337,13 +338,13 @@ cubeb_resampler_create(cubeb_stream * stream, switch (format) { case CUBEB_SAMPLE_S16NE: - return cubeb_resampler_create_internal(stream, input_params, - output_params, target_rate, - callback, user_ptr, quality); + return cubeb_resampler_create_internal( + stream, input_params, output_params, target_rate, callback, user_ptr, + quality, reclock); case CUBEB_SAMPLE_FLOAT32NE: - return cubeb_resampler_create_internal(stream, input_params, - output_params, target_rate, - callback, user_ptr, quality); + return cubeb_resampler_create_internal( + stream, input_params, output_params, target_rate, callback, user_ptr, + quality, reclock); default: assert(false); return nullptr; diff --git a/externals/cubeb/src/cubeb_resampler.h b/externals/cubeb/src/cubeb_resampler.h index e9b95324e..711a3771d 100755 --- a/externals/cubeb/src/cubeb_resampler.h +++ b/externals/cubeb/src/cubeb_resampler.h @@ -21,6 +21,11 @@ typedef enum { CUBEB_RESAMPLER_QUALITY_DESKTOP } cubeb_resampler_quality; +typedef enum { + CUBEB_RESAMPLER_RECLOCK_NONE, + CUBEB_RESAMPLER_RECLOCK_INPUT +} cubeb_resampler_reclock; + /** * Create a resampler to adapt the requested sample rate into something that * is accepted by the audio backend. @@ -44,7 +49,8 @@ cubeb_resampler_create(cubeb_stream * stream, cubeb_stream_params * input_params, cubeb_stream_params * output_params, unsigned int target_rate, cubeb_data_callback callback, - void * user_ptr, cubeb_resampler_quality quality); + void * user_ptr, cubeb_resampler_quality quality, + cubeb_resampler_reclock reclock); /** * Fill the buffer with frames acquired using the data callback. Resampling will diff --git a/externals/cubeb/src/cubeb_resampler_internal.h b/externals/cubeb/src/cubeb_resampler_internal.h index 2eabb7c4c..b271afc15 100755 --- a/externals/cubeb/src/cubeb_resampler_internal.h +++ b/externals/cubeb/src/cubeb_resampler_internal.h @@ -496,7 +496,8 @@ cubeb_resampler_create_internal(cubeb_stream * stream, cubeb_stream_params * output_params, unsigned int target_rate, cubeb_data_callback callback, void * user_ptr, - cubeb_resampler_quality quality) + cubeb_resampler_quality quality, + cubeb_resampler_reclock reclock) { std::unique_ptr> input_resampler = nullptr; std::unique_ptr> output_resampler = nullptr; diff --git a/externals/cubeb/src/cubeb_utils_win.h b/externals/cubeb/src/cubeb_utils_win.h index 4c47f454c..48e7b1b6d 100755 --- a/externals/cubeb/src/cubeb_utils_win.h +++ b/externals/cubeb/src/cubeb_utils_win.h @@ -11,24 +11,23 @@ #include "cubeb-internal.h" #include -/* This wraps a critical section to track the owner in debug mode, adapted from +/* This wraps an SRWLock to track the owner in debug mode, adapted from NSPR and http://blogs.msdn.com/b/oldnewthing/archive/2013/07/12/10433554.aspx */ class owned_critical_section { public: owned_critical_section() + : srwlock(SRWLOCK_INIT) #ifndef NDEBUG - : owner(0) + , + owner(0) #endif { - InitializeCriticalSection(&critical_section); } - ~owned_critical_section() { DeleteCriticalSection(&critical_section); } - void lock() { - EnterCriticalSection(&critical_section); + AcquireSRWLockExclusive(&srwlock); #ifndef NDEBUG XASSERT(owner != GetCurrentThreadId() && "recursive locking"); owner = GetCurrentThreadId(); @@ -41,7 +40,7 @@ public: /* GetCurrentThreadId cannot return 0: it is not a the valid thread id */ owner = 0; #endif - LeaveCriticalSection(&critical_section); + ReleaseSRWLockExclusive(&srwlock); } /* This is guaranteed to have the good behaviour if it succeeds. The behaviour @@ -55,12 +54,12 @@ public: } private: - CRITICAL_SECTION critical_section; + SRWLOCK srwlock; #ifndef NDEBUG DWORD owner; #endif - // Disallow copy and assignment because CRICICAL_SECTION cannot be copied. + // Disallow copy and assignment because SRWLock cannot be copied. owned_critical_section(const owned_critical_section &); owned_critical_section & operator=(const owned_critical_section &); }; diff --git a/externals/cubeb/src/cubeb_wasapi.cpp b/externals/cubeb/src/cubeb_wasapi.cpp index 9cc8a262e..90e195066 100755 --- a/externals/cubeb/src/cubeb_wasapi.cpp +++ b/externals/cubeb/src/cubeb_wasapi.cpp @@ -183,6 +183,46 @@ private: extern cubeb_ops const wasapi_ops; +static com_heap_ptr +wasapi_get_default_device_id(EDataFlow flow, ERole role, + IMMDeviceEnumerator * enumerator); + +struct wasapi_default_devices { + wasapi_default_devices(IMMDeviceEnumerator * enumerator) + : render_console_id( + wasapi_get_default_device_id(eRender, eConsole, enumerator)), + render_comms_id( + wasapi_get_default_device_id(eRender, eCommunications, enumerator)), + capture_console_id( + wasapi_get_default_device_id(eCapture, eConsole, enumerator)), + capture_comms_id( + wasapi_get_default_device_id(eCapture, eCommunications, enumerator)) + { + } + + bool is_default(EDataFlow flow, ERole role, wchar_t const * id) + { + wchar_t const * default_id = nullptr; + if (flow == eRender && role == eConsole) { + default_id = this->render_console_id.get(); + } else if (flow == eRender && role == eCommunications) { + default_id = this->render_comms_id.get(); + } else if (flow == eCapture && role == eConsole) { + default_id = this->capture_console_id.get(); + } else if (flow == eCapture && role == eCommunications) { + default_id = this->capture_comms_id.get(); + } + + return default_id && wcscmp(id, default_id) == 0; + } + +private: + com_heap_ptr render_console_id; + com_heap_ptr render_comms_id; + com_heap_ptr capture_console_id; + com_heap_ptr capture_comms_id; +}; + int wasapi_stream_stop(cubeb_stream * stm); int @@ -195,7 +235,8 @@ ERole pref_to_role(cubeb_stream_prefs param); int wasapi_create_device(cubeb * ctx, cubeb_device_info & ret, - IMMDeviceEnumerator * enumerator, IMMDevice * dev); + IMMDeviceEnumerator * enumerator, IMMDevice * dev, + wasapi_default_devices * defaults); void wasapi_destroy_device(cubeb_device_info * device_info); static int @@ -216,6 +257,7 @@ class monitor_device_notifications; struct cubeb { cubeb_ops const * ops = &wasapi_ops; + owned_critical_section lock; cubeb_strings * device_ids; /* Device enumerator to get notifications when the device collection change. */ @@ -716,6 +758,8 @@ intern_device_id(cubeb * ctx, wchar_t const * id) { XASSERT(id); + auto_lock lock(ctx->lock); + char const * tmp = wstr_to_utf8(id); if (!tmp) { return nullptr; @@ -902,16 +946,16 @@ refill(cubeb_stream * stm, void * input_buffer, long input_frames_count, return out_frames; } -int +bool trigger_async_reconfigure(cubeb_stream * stm) { XASSERT(stm && stm->reconfigure_event); + LOG("Try reconfiguring the stream"); BOOL ok = SetEvent(stm->reconfigure_event); if (!ok) { LOG("SetEvent on reconfigure_event failed: %lx", GetLastError()); - return CUBEB_ERROR; } - return CUBEB_OK; + return static_cast(ok); } /* This helper grabs all the frames available from a capture client, put them in @@ -940,8 +984,16 @@ get_input_buffer(cubeb_stream * stm) if (hr == AUDCLNT_E_DEVICE_INVALIDATED) { // Application can recover from this error. More info // https://msdn.microsoft.com/en-us/library/windows/desktop/dd316605(v=vs.85).aspx - LOG("Device invalidated error, reset default device"); - trigger_async_reconfigure(stm); + LOG("Input device invalidated error"); + // No need to reset device if user asks to use particular device, or + // switching is disabled. + if (stm->input_device_id || + (stm->input_stream_params.prefs & + CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING) || + !trigger_async_reconfigure(stm)) { + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + return false; + } return true; } @@ -1046,8 +1098,16 @@ get_output_buffer(cubeb_stream * stm, void *& buffer, size_t & frame_count) if (hr == AUDCLNT_E_DEVICE_INVALIDATED) { // Application can recover from this error. More info // https://msdn.microsoft.com/en-us/library/windows/desktop/dd316605(v=vs.85).aspx - LOG("Device invalidated error, reset default device"); - trigger_async_reconfigure(stm); + LOG("Output device invalidated error"); + // No need to reset device if user asks to use particular device, or + // switching is disabled. + if (stm->output_device_id || + (stm->output_stream_params.prefs & + CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING) || + !trigger_async_reconfigure(stm)) { + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + return false; + } return true; } @@ -1397,7 +1457,7 @@ wasapi_destroy(cubeb * context); HRESULT register_notification_client(cubeb_stream * stm) { - XASSERT(stm->device_enumerator); + XASSERT(stm->device_enumerator && !stm->notification_client); stm->notification_client.reset(new wasapi_endpoint_notification_client( stm->reconfigure_event, stm->role)); @@ -1415,7 +1475,7 @@ register_notification_client(cubeb_stream * stm) HRESULT unregister_notification_client(cubeb_stream * stm) { - XASSERT(stm->device_enumerator); + XASSERT(stm->device_enumerator && stm->notification_client); HRESULT hr = stm->device_enumerator->UnregisterEndpointNotificationCallback( stm->notification_client.get()); @@ -1454,6 +1514,9 @@ get_endpoint(com_ptr & device, LPCWSTR devid) HRESULT register_collection_notification_client(cubeb * context) { + context->lock.assert_current_thread_owns(); + XASSERT(!context->device_collection_enumerator && + !context->collection_notification_client); HRESULT hr = CoCreateInstance( __uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(context->device_collection_enumerator.receive())); @@ -1480,6 +1543,9 @@ register_collection_notification_client(cubeb * context) HRESULT unregister_collection_notification_client(cubeb * context) { + context->lock.assert_current_thread_owns(); + XASSERT(context->device_collection_enumerator && + context->collection_notification_client); HRESULT hr = context->device_collection_enumerator ->UnregisterEndpointNotificationCallback( context->collection_notification_client.get()); @@ -1608,6 +1674,7 @@ wasapi_init(cubeb ** context, char const * context_name) cubeb * ctx = new cubeb(); ctx->ops = &wasapi_ops; + auto_lock lock(ctx->lock); if (cubeb_strings_init(&ctx->device_ids) != CUBEB_OK) { delete ctx; return CUBEB_ERROR; @@ -1682,6 +1749,10 @@ stop_and_join_render_thread(cubeb_stream * stm) void wasapi_destroy(cubeb * context) { + auto_lock lock(context->lock); + XASSERT(!context->device_collection_enumerator && + !context->collection_notification_client); + if (context->device_ids) { cubeb_strings_destroy(context->device_ids); } @@ -1887,7 +1958,7 @@ handle_channel_layout(cubeb_stream * stm, EDataFlow direction, } } -static bool +static int initialize_iaudioclient2(com_ptr & audio_client) { com_ptr audio_client2; @@ -1910,8 +1981,8 @@ initialize_iaudioclient2(com_ptr & audio_client) return CUBEB_OK; } -// Not static to suppress a warning. -/* static */ bool +#if 0 +bool initialize_iaudioclient3(com_ptr & audio_client, cubeb_stream * stm, const com_heap_ptr & mix_format, @@ -2021,6 +2092,7 @@ initialize_iaudioclient3(com_ptr & audio_client, LOG("Could not initialize shared stream with IAudioClient3: error: %lx", hr); return false; } +#endif #define DIRECTION_NAME (direction == eCapture ? "capture" : "render") @@ -2035,6 +2107,8 @@ setup_wasapi_stream_one_side(cubeb_stream * stm, cubeb_stream_params * mix_params, com_ptr & device) { + XASSERT(direction == eCapture || direction == eRender); + HRESULT hr; bool is_loopback = stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK; if (is_loopback && direction != eCapture) { @@ -2043,6 +2117,10 @@ setup_wasapi_stream_one_side(cubeb_stream * stm, } stm->stream_reset_lock.assert_current_thread_owns(); + // If user doesn't specify a particular device, we can choose another one when + // the given devid is unavailable. + bool allow_fallback = + direction == eCapture ? !stm->input_device_id : !stm->output_device_id; bool try_again = false; // This loops until we find a device that works, or we've exhausted all // possibilities. @@ -2092,7 +2170,7 @@ setup_wasapi_stream_one_side(cubeb_stream * stm, DIRECTION_NAME, hr); // A particular device can't be activated because it has been // unplugged, try fall back to the default audio device. - if (devid && hr == AUDCLNT_E_DEVICE_INVALIDATED) { + if (devid && hr == AUDCLNT_E_DEVICE_INVALIDATED && allow_fallback) { LOG("Trying again with the default %s audio device.", DIRECTION_NAME); devid = nullptr; device = nullptr; @@ -2169,40 +2247,37 @@ setup_wasapi_stream_one_side(cubeb_stream * stm, flags |= AUDCLNT_STREAMFLAGS_EVENTCALLBACK; } - // Sanity check the latency, it may be that the device doesn't support it. - REFERENCE_TIME minimum_period; - REFERENCE_TIME default_period; - hr = audio_client->GetDevicePeriod(&default_period, &minimum_period); - if (FAILED(hr)) { - LOG("Could not get device period: %lx", hr); - return CUBEB_ERROR; - } - REFERENCE_TIME latency_hns = frames_to_hns(stream_params->rate, stm->latency); - stm->input_bluetooth_handsfree = false; - cubeb_device_info device_info; - if (wasapi_create_device(stm->context, device_info, - stm->device_enumerator.get(), - device.get()) == CUBEB_OK) { - const char * HANDSFREE_TAG = "BTHHFENUM"; - size_t len = sizeof(HANDSFREE_TAG); - if (direction == eCapture) { - uint32_t default_period_frames = - hns_to_frames(device_info.default_rate, default_period); + // Adjust input latency and check if input is using bluetooth handsfree + // protocol. + if (direction == eCapture) { + stm->input_bluetooth_handsfree = false; + + wasapi_default_devices default_devices(stm->device_enumerator.get()); + cubeb_device_info device_info; + if (wasapi_create_device(stm->context, device_info, + stm->device_enumerator.get(), device.get(), + &default_devices) == CUBEB_OK) { + // This multiplicator has been found empirically. + XASSERT(device_info.latency_hi > 0); + uint32_t latency_frames = device_info.latency_hi * 8; + LOG("Input: latency increased to %u frames from a default of %u", + latency_frames, device_info.latency_hi); + latency_hns = frames_to_hns(device_info.default_rate, latency_frames); + + const char * HANDSFREE_TAG = "BTHHFENUM"; + size_t len = sizeof(HANDSFREE_TAG); if (strlen(device_info.group_id) >= len && strncmp(device_info.group_id, HANDSFREE_TAG, len) == 0) { + LOG("Input device is using bluetooth handsfree protocol"); stm->input_bluetooth_handsfree = true; } - // This multiplicator has been found empirically. - uint32_t latency_frames = default_period_frames * 8; - LOG("Input: latency increased to %u frames from a default of %u", - latency_frames, default_period_frames); - latency_hns = frames_to_hns(device_info.default_rate, latency_frames); + + wasapi_destroy_device(&device_info); + } else { + LOG("Could not get cubeb_device_info. Skip customizing input settings"); } - wasapi_destroy_device(&device_info); - } else { - LOG("Could not get cubeb_device_info."); } if (stream_params->prefs & CUBEB_STREAM_PREF_RAW) { @@ -2258,8 +2333,10 @@ setup_wasapi_stream_one_side(cubeb_stream * stm, #undef DIRECTION_NAME -void -wasapi_find_matching_output_device(cubeb_stream * stm) +// Returns a non-null cubeb_devid if we find a matched device, or nullptr +// otherwise. +cubeb_devid +wasapi_find_bt_handsfree_output_device(cubeb_stream * stm) { HRESULT hr; cubeb_device_info * input_device = nullptr; @@ -2268,19 +2345,21 @@ wasapi_find_matching_output_device(cubeb_stream * stm) // Only try to match to an output device if the input device is a bluetooth // device that is using the handsfree protocol if (!stm->input_bluetooth_handsfree) { - return; + return nullptr; } wchar_t * tmp = nullptr; hr = stm->input_device->GetId(&tmp); if (FAILED(hr)) { - LOG("Couldn't get input device id in wasapi_find_matching_output_device"); - return; + LOG("Couldn't get input device id in " + "wasapi_find_bt_handsfree_output_device"); + return nullptr; } com_heap_ptr device_id(tmp); - cubeb_devid input_device_id = intern_device_id(stm->context, device_id.get()); + cubeb_devid input_device_id = reinterpret_cast( + intern_device_id(stm->context, device_id.get())); if (!input_device_id) { - return; + return nullptr; } int rv = wasapi_enumerate_devices( @@ -2288,7 +2367,7 @@ wasapi_find_matching_output_device(cubeb_stream * stm) (cubeb_device_type)(CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT), &collection); if (rv != CUBEB_OK) { - return; + return nullptr; } // Find the input device, and then find the output device with the same group @@ -2300,19 +2379,36 @@ wasapi_find_matching_output_device(cubeb_stream * stm) } } - for (uint32_t i = 0; i < collection.count; i++) { - cubeb_device_info & dev = collection.device[i]; - if (dev.type == CUBEB_DEVICE_TYPE_OUTPUT && dev.group_id && input_device && - !strcmp(dev.group_id, input_device->group_id) && - dev.default_rate == input_device->default_rate) { - LOG("Found matching device for %s: %s", input_device->friendly_name, - dev.friendly_name); - stm->output_device_id = - utf8_to_wstr(reinterpret_cast(dev.devid)); + cubeb_devid matched_output = nullptr; + + if (input_device) { + for (uint32_t i = 0; i < collection.count; i++) { + cubeb_device_info & dev = collection.device[i]; + if (dev.type == CUBEB_DEVICE_TYPE_OUTPUT && dev.group_id && + !strcmp(dev.group_id, input_device->group_id) && + dev.default_rate == input_device->default_rate) { + LOG("Found matching device for %s: %s", input_device->friendly_name, + dev.friendly_name); + matched_output = dev.devid; + break; + } } } wasapi_device_collection_destroy(stm->context, &collection); + return matched_output; +} + +std::unique_ptr +copy_wide_string(const wchar_t * src) +{ + XASSERT(src); + size_t len = wcslen(src); + std::unique_ptr copy(new wchar_t[len + 1]); + if (wcsncpy_s(copy.get(), len + 1, src, len) != 0) { + return nullptr; + } + return copy; } int @@ -2325,6 +2421,17 @@ setup_wasapi_stream(cubeb_stream * stm) XASSERT((!stm->output_client || !stm->input_client) && "WASAPI stream already setup, close it first."); + std::unique_ptr selected_output_device_id; + if (stm->output_device_id) { + if (std::unique_ptr tmp = + move(copy_wide_string(stm->output_device_id.get()))) { + selected_output_device_id = move(tmp); + } else { + LOG("Failed to copy output device identifier."); + return CUBEB_ERROR; + } + } + if (has_input(stm)) { LOG("(%p) Setup capture: device=%p", stm, stm->input_device_id.get()); rv = setup_wasapi_stream_one_side( @@ -2356,8 +2463,12 @@ setup_wasapi_stream(cubeb_stream * stm) // device, and the default device is the same bluetooth device, pick the // right output device, running at the same rate and with the same protocol // as the input. - if (!stm->output_device_id) { - wasapi_find_matching_output_device(stm); + if (!selected_output_device_id) { + cubeb_devid matched = wasapi_find_bt_handsfree_output_device(stm); + if (matched) { + selected_output_device_id = + move(utf8_to_wstr(reinterpret_cast(matched))); + } } } @@ -2371,23 +2482,24 @@ setup_wasapi_stream(cubeb_stream * stm) stm->output_stream_params.channels = stm->input_stream_params.channels; stm->output_stream_params.layout = stm->input_stream_params.layout; if (stm->input_device_id) { - size_t len = wcslen(stm->input_device_id.get()); - std::unique_ptr tmp(new wchar_t[len + 1]); - if (wcsncpy_s(tmp.get(), len + 1, stm->input_device_id.get(), len) != 0) { - LOG("Failed to copy device identifier while copying input stream" - " configuration to output stream configuration to drive loopback."); + if (std::unique_ptr tmp = + move(copy_wide_string(stm->input_device_id.get()))) { + XASSERT(!selected_output_device_id); + selected_output_device_id = move(tmp); + } else { + LOG("Failed to copy device identifier while copying input stream " + "configuration to output stream configuration to drive loopback."); return CUBEB_ERROR; } - stm->output_device_id = move(tmp); } stm->has_dummy_output = true; } if (has_output(stm)) { - LOG("(%p) Setup render: device=%p", stm, stm->output_device_id.get()); + LOG("(%p) Setup render: device=%p", stm, selected_output_device_id.get()); rv = setup_wasapi_stream_one_side( - stm, &stm->output_stream_params, stm->output_device_id.get(), eRender, - __uuidof(IAudioRenderClient), stm->output_client, + stm, &stm->output_stream_params, selected_output_device_id.get(), + eRender, __uuidof(IAudioRenderClient), stm->output_client, &stm->output_buffer_frame_count, stm->refill_event, stm->render_client, &stm->output_mix_params, stm->output_device); if (rv != CUBEB_OK) { @@ -2448,10 +2560,11 @@ setup_wasapi_stream(cubeb_stream * stm) stm->resampler.reset(cubeb_resampler_create( stm, has_input(stm) ? &input_params : nullptr, - has_output(stm) ? &output_params : nullptr, target_sample_rate, - stm->data_callback, stm->user_ptr, + has_output(stm) && !stm->has_dummy_output ? &output_params : nullptr, + target_sample_rate, stm->data_callback, stm->user_ptr, stm->voice ? CUBEB_RESAMPLER_QUALITY_VOIP - : CUBEB_RESAMPLER_QUALITY_DESKTOP)); + : CUBEB_RESAMPLER_QUALITY_DESKTOP, + CUBEB_RESAMPLER_RECLOCK_NONE)); if (!stm->resampler) { LOG("Could not get a resampler"); return CUBEB_ERROR; @@ -2616,12 +2729,15 @@ wasapi_stream_init(cubeb * context, cubeb_stream ** stream, return rv; } - if (!((input_stream_params ? (input_stream_params->prefs & - CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING) - : 0) || - (output_stream_params ? (output_stream_params->prefs & - CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING) - : 0))) { + // Follow the system default devices when not specifying devices explicitly + // and CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING is not set. + if ((!input_device && input_stream_params && + !(input_stream_params->prefs & + CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING)) || + (!output_device && output_stream_params && + !(output_stream_params->prefs & + CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING))) { + LOG("Follow the system default input or/and output devices"); HRESULT hr = register_notification_client(stm.get()); if (FAILED(hr)) { /* this is not fatal, we can still play audio, but we won't be able @@ -3011,31 +3127,30 @@ static com_ptr wasapi_get_device_node( return ret; } -static BOOL -wasapi_is_default_device(EDataFlow flow, ERole role, LPCWSTR device_id, - IMMDeviceEnumerator * enumerator) +static com_heap_ptr +wasapi_get_default_device_id(EDataFlow flow, ERole role, + IMMDeviceEnumerator * enumerator) { - BOOL ret = FALSE; com_ptr dev; - HRESULT hr; - hr = enumerator->GetDefaultAudioEndpoint(flow, role, dev.receive()); + HRESULT hr = enumerator->GetDefaultAudioEndpoint(flow, role, dev.receive()); if (SUCCEEDED(hr)) { wchar_t * tmp = nullptr; if (SUCCEEDED(dev->GetId(&tmp))) { - com_heap_ptr defdevid(tmp); - ret = (wcscmp(defdevid.get(), device_id) == 0); + com_heap_ptr devid(tmp); + return devid; } } - return ret; + return nullptr; } /* `ret` must be deallocated with `wasapi_destroy_device`, iff the return value * of this function is `CUBEB_OK`. */ int wasapi_create_device(cubeb * ctx, cubeb_device_info & ret, - IMMDeviceEnumerator * enumerator, IMMDevice * dev) + IMMDeviceEnumerator * enumerator, IMMDevice * dev, + wasapi_default_devices * defaults) { com_ptr endpoint; com_ptr devnode; @@ -3046,6 +3161,8 @@ wasapi_create_device(cubeb * ctx, cubeb_device_info & ret, REFERENCE_TIME def_period, min_period; HRESULT hr; + XASSERT(enumerator && dev && defaults); + // zero-out to be able to safely delete the pointers to friendly_name and // group_id at all time in this function. PodZero(&ret, 1); @@ -3133,19 +3250,14 @@ wasapi_create_device(cubeb * ctx, cubeb_device_info & ret, } ret.preferred = CUBEB_DEVICE_PREF_NONE; - if (wasapi_is_default_device(flow, eConsole, device_id.get(), enumerator)) { + if (defaults->is_default(flow, eConsole, device_id.get())) { ret.preferred = - (cubeb_device_pref)(ret.preferred | CUBEB_DEVICE_PREF_MULTIMEDIA); - } - if (wasapi_is_default_device(flow, eCommunications, device_id.get(), - enumerator)) { + (cubeb_device_pref)(ret.preferred | CUBEB_DEVICE_PREF_MULTIMEDIA | + CUBEB_DEVICE_PREF_NOTIFICATION); + } else if (defaults->is_default(flow, eCommunications, device_id.get())) { ret.preferred = (cubeb_device_pref)(ret.preferred | CUBEB_DEVICE_PREF_VOICE); } - if (wasapi_is_default_device(flow, eConsole, device_id.get(), enumerator)) { - ret.preferred = - (cubeb_device_pref)(ret.preferred | CUBEB_DEVICE_PREF_NOTIFICATION); - } if (flow == eRender) { ret.type = CUBEB_DEVICE_TYPE_OUTPUT; @@ -3229,14 +3341,17 @@ wasapi_enumerate_devices(cubeb * context, cubeb_device_type type, return CUBEB_ERROR; } - if (type == CUBEB_DEVICE_TYPE_OUTPUT) + wasapi_default_devices default_devices(enumerator.get()); + + if (type == CUBEB_DEVICE_TYPE_OUTPUT) { flow = eRender; - else if (type == CUBEB_DEVICE_TYPE_INPUT) + } else if (type == CUBEB_DEVICE_TYPE_INPUT) { flow = eCapture; - else if (type & (CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT)) + } else if (type & (CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT)) { flow = eAll; - else + } else { return CUBEB_ERROR; + } hr = enumerator->EnumAudioEndpoints(flow, DEVICE_STATEMASK_ALL, collection.receive()); @@ -3264,7 +3379,7 @@ wasapi_enumerate_devices(cubeb * context, cubeb_device_type type, continue; } if (wasapi_create_device(context, devices[out->count], enumerator.get(), - dev.get()) == CUBEB_OK) { + dev.get(), &default_devices) == CUBEB_OK) { out->count += 1; } } @@ -3294,6 +3409,7 @@ wasapi_register_device_collection_changed( cubeb_device_collection_changed_callback collection_changed_callback, void * user_ptr) { + auto_lock lock(context->lock); if (devtype == CUBEB_DEVICE_TYPE_UNKNOWN) { return CUBEB_ERROR_INVALID_PARAMETER; } diff --git a/externals/cubeb/src/cubeb_winmm.c b/externals/cubeb/src/cubeb_winmm.c index 169b74385..44aec86d2 100755 --- a/externals/cubeb/src/cubeb_winmm.c +++ b/externals/cubeb/src/cubeb_winmm.c @@ -73,17 +73,6 @@ #define CUBEB_STREAM_MAX 32 #define NBUFS 4 -const GUID KSDATAFORMAT_SUBTYPE_PCM = { - 0x00000001, - 0x0000, - 0x0010, - {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}}; -const GUID KSDATAFORMAT_SUBTYPE_IEEE_FLOAT = { - 0x00000003, - 0x0000, - 0x0010, - {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}}; - struct cubeb_stream_item { SLIST_ENTRY head; cubeb_stream * stream; diff --git a/externals/cubeb/test/test_duplex.cpp b/externals/cubeb/test/test_duplex.cpp index 3620bf066..ff0b397e8 100755 --- a/externals/cubeb/test/test_duplex.cpp +++ b/externals/cubeb/test/test_duplex.cpp @@ -137,26 +137,20 @@ void device_collection_changed_callback(cubeb * context, void * user) " called when opening a stream"; } -TEST(cubeb, duplex_collection_change) +void +duplex_collection_change_impl(cubeb * ctx) { - cubeb *ctx; - cubeb_stream *stream; + cubeb_stream * stream; cubeb_stream_params input_params; cubeb_stream_params output_params; int r; uint32_t latency_frames = 0; - r = common_init(&ctx, "Cubeb duplex example with collection change"); - ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; - - r = cubeb_register_device_collection_changed(ctx, - static_cast(CUBEB_DEVICE_TYPE_INPUT), - device_collection_changed_callback, - nullptr); + r = cubeb_register_device_collection_changed( + ctx, static_cast(CUBEB_DEVICE_TYPE_INPUT), + device_collection_changed_callback, nullptr); ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; - std::unique_ptr - cleanup_cubeb_at_exit(ctx, cubeb_destroy); /* typical user-case: mono input, stereo output, low latency. */ input_params.format = STREAM_FORMAT; @@ -173,13 +167,43 @@ TEST(cubeb, duplex_collection_change) r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; - r = cubeb_stream_init(ctx, &stream, "Cubeb duplex", - NULL, &input_params, NULL, &output_params, - latency_frames, data_cb_duplex, state_cb_duplex, nullptr); + r = cubeb_stream_init(ctx, &stream, "Cubeb duplex", NULL, &input_params, NULL, + &output_params, latency_frames, data_cb_duplex, + state_cb_duplex, nullptr); ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; cubeb_stream_destroy(stream); } +TEST(cubeb, duplex_collection_change) +{ + cubeb * ctx; + int r; + + r = common_init(&ctx, "Cubeb duplex example with collection change"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + std::unique_ptr cleanup_cubeb_at_exit( + ctx, cubeb_destroy); + + duplex_collection_change_impl(ctx); + r = cubeb_register_device_collection_changed( + ctx, static_cast(CUBEB_DEVICE_TYPE_INPUT), nullptr, + nullptr); + ASSERT_EQ(r, CUBEB_OK); +} + +TEST(cubeb, duplex_collection_change_no_unregister) +{ + cubeb * ctx; + int r; + + r = common_init(&ctx, "Cubeb duplex example with collection change"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + std::unique_ptr cleanup_cubeb_at_exit( + ctx, [](cubeb * p) noexcept { EXPECT_DEATH(cubeb_destroy(p), ""); }); + + duplex_collection_change_impl(ctx); +} + long data_cb_input(cubeb_stream * stream, void * user, const void * inputbuffer, void * outputbuffer, long nframes) { if (stream == NULL || inputbuffer == NULL || outputbuffer != NULL) { diff --git a/externals/cubeb/test/test_resampler.cpp b/externals/cubeb/test/test_resampler.cpp index 8ac878fc3..b4e148533 100755 --- a/externals/cubeb/test/test_resampler.cpp +++ b/externals/cubeb/test/test_resampler.cpp @@ -338,7 +338,8 @@ void test_resampler_duplex(uint32_t input_channels, uint32_t output_channels, cubeb_resampler * resampler = cubeb_resampler_create((cubeb_stream*)nullptr, &input_params, &output_params, target_rate, - data_cb_resampler, (void*)&state, CUBEB_RESAMPLER_QUALITY_VOIP); + data_cb_resampler, (void*)&state, CUBEB_RESAMPLER_QUALITY_VOIP, + CUBEB_RESAMPLER_RECLOCK_NONE); long latency = cubeb_resampler_latency(resampler); @@ -484,8 +485,8 @@ TEST(cubeb, resampler_output_only_noop) cubeb_resampler * resampler = cubeb_resampler_create((cubeb_stream*)nullptr, nullptr, &output_params, target_rate, test_output_only_noop_data_cb, nullptr, - CUBEB_RESAMPLER_QUALITY_VOIP); - + CUBEB_RESAMPLER_QUALITY_VOIP, + CUBEB_RESAMPLER_RECLOCK_NONE); const long out_frames = 128; float out_buffer[out_frames]; long got; @@ -523,7 +524,8 @@ TEST(cubeb, resampler_drain) cubeb_resampler * resampler = cubeb_resampler_create((cubeb_stream*)nullptr, nullptr, &output_params, target_rate, test_drain_data_cb, &cb_count, - CUBEB_RESAMPLER_QUALITY_VOIP); + CUBEB_RESAMPLER_QUALITY_VOIP, + CUBEB_RESAMPLER_RECLOCK_NONE); const long out_frames = 128; float out_buffer[out_frames]; @@ -572,7 +574,8 @@ TEST(cubeb, resampler_passthrough_output_only) cubeb_resampler * resampler = cubeb_resampler_create((cubeb_stream*)nullptr, nullptr, &output_params, target_rate, cb_passthrough_resampler_output, nullptr, - CUBEB_RESAMPLER_QUALITY_VOIP); + CUBEB_RESAMPLER_QUALITY_VOIP, + CUBEB_RESAMPLER_RECLOCK_NONE); float output_buffer[output_channels * 256]; @@ -616,7 +619,8 @@ TEST(cubeb, resampler_passthrough_input_only) cubeb_resampler * resampler = cubeb_resampler_create((cubeb_stream*)nullptr, &input_params, nullptr, target_rate, cb_passthrough_resampler_input, nullptr, - CUBEB_RESAMPLER_QUALITY_VOIP); + CUBEB_RESAMPLER_QUALITY_VOIP, + CUBEB_RESAMPLER_RECLOCK_NONE); float input_buffer[input_channels * 256]; @@ -737,7 +741,8 @@ TEST(cubeb, resampler_passthrough_duplex_callback_reordering) cubeb_resampler * resampler = cubeb_resampler_create((cubeb_stream*)nullptr, &input_params, &output_params, target_rate, cb_passthrough_resampler_duplex, &c, - CUBEB_RESAMPLER_QUALITY_VOIP); + CUBEB_RESAMPLER_QUALITY_VOIP, + CUBEB_RESAMPLER_RECLOCK_NONE); const long BUF_BASE_SIZE = 256; float input_buffer_prebuffer[input_channels * BUF_BASE_SIZE * 2]; @@ -820,7 +825,7 @@ TEST(cubeb, resampler_drift_drop_data) cubeb_resampler * resampler = cubeb_resampler_create((cubeb_stream*)nullptr, &input_params, &output_params, target_rate, cb_passthrough_resampler_duplex, &c, - CUBEB_RESAMPLER_QUALITY_VOIP); + CUBEB_RESAMPLER_QUALITY_VOIP, CUBEB_RESAMPLER_RECLOCK_NONE); const long BUF_BASE_SIZE = 256; diff --git a/externals/cubeb/tools/cubeb-test.cpp b/externals/cubeb/tools/cubeb-test.cpp index 863e9ee16..ab17123ae 100755 --- a/externals/cubeb/tools/cubeb-test.cpp +++ b/externals/cubeb/tools/cubeb-test.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #ifdef _WIN32 #include // Used by CoInitialize() #endif @@ -35,6 +36,32 @@ static const char* state_to_string(cubeb_state state) { } } +static const char* device_type_to_string(cubeb_device_type type) { + switch (type) { + case CUBEB_DEVICE_TYPE_INPUT: + return "input"; + case CUBEB_DEVICE_TYPE_OUTPUT: + return "output"; + case CUBEB_DEVICE_TYPE_UNKNOWN: + return "unknown"; + default: + assert(false); + } +} + +static const char* device_state_to_string(cubeb_device_state state) { + switch (state) { + case CUBEB_DEVICE_STATE_DISABLED: + return "disabled"; + case CUBEB_DEVICE_STATE_UNPLUGGED: + return "unplugged"; + case CUBEB_DEVICE_STATE_ENABLED: + return "enabled"; + default: + assert(false); + } +} + void print_log(const char* msg, ...) { va_list args; va_start(args, msg); @@ -48,6 +75,7 @@ public: ~cubeb_client() {} bool init(char const * backend_name = nullptr); + cubeb_devid select_device(cubeb_device_type type) const; bool init_stream(); bool start_stream(); bool stop_stream(); @@ -70,7 +98,10 @@ public: bool unregister_device_collection_changed(cubeb_device_type devtype) const; cubeb_stream_params output_params = {}; + cubeb_devid output_device = nullptr; + cubeb_stream_params input_params = {}; + cubeb_devid input_device = nullptr; void force_drain() { _force_drain = true; } @@ -81,8 +112,6 @@ private: cubeb* context = nullptr; cubeb_stream* stream = nullptr; - cubeb_devid output_device = nullptr; - cubeb_devid input_device = nullptr; /* Accessed only from client and audio thread. */ std::atomic _rate = {0}; @@ -492,6 +521,56 @@ bool choose_action(cubeb_client& cl, operation_data * op, int c) { return true; // Loop up } +cubeb_devid cubeb_client::select_device(cubeb_device_type type) const +{ + assert(type == CUBEB_DEVICE_TYPE_INPUT || type == CUBEB_DEVICE_TYPE_OUTPUT); + + cubeb_device_collection collection; + if (cubeb_enumerate_devices(context, type, &collection) == + CUBEB_ERROR_NOT_SUPPORTED) { + fprintf(stderr, + "Not support %s device selection. Force to use default device\n", + device_type_to_string(type)); + return nullptr; + } + + assert(collection.count); + fprintf(stderr, "Found %zu %s devices. Choose one:\n", collection.count, + device_type_to_string(type)); + + std::vector devices; + devices.emplace_back(nullptr); + fprintf(stderr, "# 0\n\tname: system default device\n"); + for (size_t i = 0; i < collection.count; i++) { + assert(collection.device[i].type == type); + fprintf(stderr, + "# %zu %s\n" + "\tname: %s\n" + "\tdevice id: %s\n" + "\tmax channels: %u\n" + "\tstate: %s\n", + devices.size(), + collection.device[i].preferred ? " (PREFERRED)" : "", + collection.device[i].friendly_name, collection.device[i].device_id, + collection.device[i].max_channels, + device_state_to_string(collection.device[i].state)); + devices.emplace_back(collection.device[i].devid); + } + + cubeb_device_collection_destroy(context, &collection); + + size_t number; + std::cout << "Enter device number: "; + std::cin >> number; + while (!std::cin || number >= devices.size()) { + std::cin.clear(); + std::cin.ignore(100, '\n'); + std::cout << "Error: Please enter a valid numeric input. Enter again: "; + std::cin >> number; + } + return devices[number]; +} + int main(int argc, char* argv[]) { #ifdef _WIN32 CoInitialize(nullptr); @@ -523,7 +602,7 @@ int main(int argc, char* argv[]) { bool res = false; cubeb_client cl; - cl.activate_log(CUBEB_LOG_DISABLED); + cl.activate_log(CUBEB_LOG_NORMAL); fprintf(stderr, "Log level is DISABLED\n"); cl.init(/* default backend */); @@ -540,10 +619,12 @@ int main(int argc, char* argv[]) { } } else { if (op.pm == PLAYBACK || op.pm == DUPLEX || op.pm == LATENCY_TESTING) { + cl.output_device = cl.select_device(CUBEB_DEVICE_TYPE_OUTPUT); cl.output_params = {CUBEB_SAMPLE_FLOAT32NE, op.rate, DEFAULT_OUTPUT_CHANNELS, CUBEB_LAYOUT_STEREO, CUBEB_STREAM_PREF_NONE}; } if (op.pm == RECORD || op.pm == DUPLEX || op.pm == LATENCY_TESTING) { + cl.input_device = cl.select_device(CUBEB_DEVICE_TYPE_INPUT); cl.input_params = {CUBEB_SAMPLE_FLOAT32NE, op.rate, DEFAULT_INPUT_CHANNELS, CUBEB_LAYOUT_UNDEFINED, CUBEB_STREAM_PREF_NONE}; } if (op.pm == LATENCY_TESTING) { diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt index 89575a53e..9e9b3f880 100755 --- a/src/audio_core/CMakeLists.txt +++ b/src/audio_core/CMakeLists.txt @@ -1,54 +1,214 @@ add_library(audio_core STATIC - algorithm/filter.cpp - algorithm/filter.h - algorithm/interpolate.cpp - algorithm/interpolate.h - audio_out.cpp - audio_out.h - audio_renderer.cpp - audio_renderer.h - behavior_info.cpp - behavior_info.h - buffer.h - codec.cpp - codec.h - command_generator.cpp - command_generator.h - common.h - delay_line.cpp - delay_line.h - effect_context.cpp - effect_context.h - info_updater.cpp - info_updater.h - memory_pool.cpp - memory_pool.h - mix_context.cpp - mix_context.h - null_sink.h - sink.h - sink_context.cpp - sink_context.h - sink_details.cpp - sink_details.h - sink_stream.h - splitter_context.cpp - splitter_context.h - stream.cpp - stream.h - voice_context.cpp - voice_context.h - - $<$:cubeb_sink.cpp cubeb_sink.h> - $<$:sdl2_sink.cpp sdl2_sink.h> + audio_core.cpp + audio_core.h + audio_event.h + audio_event.cpp + audio_render_manager.cpp + audio_render_manager.h + audio_in_manager.cpp + audio_in_manager.h + audio_out_manager.cpp + audio_out_manager.h + audio_manager.cpp + audio_manager.h + common/audio_renderer_parameter.h + common/common.h + common/feature_support.h + common/wave_buffer.h + common/workbuffer_allocator.h + device/audio_buffer.h + device/audio_buffers.h + device/device_session.cpp + device/device_session.h + in/audio_in.cpp + in/audio_in.h + in/audio_in_system.cpp + in/audio_in_system.h + out/audio_out.cpp + out/audio_out.h + out/audio_out_system.cpp + out/audio_out_system.h + renderer/adsp/adsp.cpp + renderer/adsp/adsp.h + renderer/adsp/audio_renderer.cpp + renderer/adsp/audio_renderer.h + renderer/adsp/command_buffer.h + renderer/adsp/command_list_processor.cpp + renderer/adsp/command_list_processor.h + renderer/audio_device.cpp + renderer/audio_device.h + renderer/audio_renderer.h + renderer/audio_renderer.cpp + renderer/behavior/behavior_info.cpp + renderer/behavior/behavior_info.h + renderer/behavior/info_updater.cpp + renderer/behavior/info_updater.h + renderer/command/data_source/adpcm.cpp + renderer/command/data_source/adpcm.h + renderer/command/data_source/decode.cpp + renderer/command/data_source/decode.h + renderer/command/data_source/pcm_float.cpp + renderer/command/data_source/pcm_float.h + renderer/command/data_source/pcm_int16.cpp + renderer/command/data_source/pcm_int16.h + renderer/command/effect/aux_.cpp + renderer/command/effect/aux_.h + renderer/command/effect/biquad_filter.cpp + renderer/command/effect/biquad_filter.h + renderer/command/effect/capture.cpp + renderer/command/effect/capture.h + renderer/command/effect/delay.cpp + renderer/command/effect/delay.h + renderer/command/effect/i3dl2_reverb.cpp + renderer/command/effect/i3dl2_reverb.h + renderer/command/effect/light_limiter.cpp + renderer/command/effect/light_limiter.h + renderer/command/effect/multi_tap_biquad_filter.cpp + renderer/command/effect/multi_tap_biquad_filter.h + renderer/command/effect/reverb.cpp + renderer/command/effect/reverb.h + renderer/command/mix/clear_mix.cpp + renderer/command/mix/clear_mix.h + renderer/command/mix/copy_mix.cpp + renderer/command/mix/copy_mix.h + renderer/command/mix/depop_for_mix_buffers.cpp + renderer/command/mix/depop_for_mix_buffers.h + renderer/command/mix/depop_prepare.cpp + renderer/command/mix/depop_prepare.h + renderer/command/mix/mix.cpp + renderer/command/mix/mix.h + renderer/command/mix/mix_ramp.cpp + renderer/command/mix/mix_ramp.h + renderer/command/mix/mix_ramp_grouped.cpp + renderer/command/mix/mix_ramp_grouped.h + renderer/command/mix/volume.cpp + renderer/command/mix/volume.h + renderer/command/mix/volume_ramp.cpp + renderer/command/mix/volume_ramp.h + renderer/command/performance/performance.cpp + renderer/command/performance/performance.h + renderer/command/resample/downmix_6ch_to_2ch.cpp + renderer/command/resample/downmix_6ch_to_2ch.h + renderer/command/resample/resample.h + renderer/command/resample/resample.cpp + renderer/command/resample/upsample.cpp + renderer/command/resample/upsample.h + renderer/command/sink/device.cpp + renderer/command/sink/device.h + renderer/command/sink/circular_buffer.cpp + renderer/command/sink/circular_buffer.h + renderer/command/command_buffer.cpp + renderer/command/command_buffer.h + renderer/command/command_generator.cpp + renderer/command/command_generator.h + renderer/command/command_list_header.h + renderer/command/command_processing_time_estimator.cpp + renderer/command/command_processing_time_estimator.h + renderer/command/commands.h + renderer/command/icommand.h + renderer/effect/effect_aux_info.cpp + renderer/effect/effect_aux_info.h + renderer/effect/effect_biquad_filter_info.cpp + renderer/effect/effect_biquad_filter_info.h + renderer/effect/effect_buffer_mixer_info.cpp + renderer/effect/effect_buffer_mixer_info.h + renderer/effect/effect_capture_info.cpp + renderer/effect/effect_capture_info.h + renderer/effect/effect_context.cpp + renderer/effect/effect_context.h + renderer/effect/effect_delay_info.cpp + renderer/effect/effect_delay_info.h + renderer/effect/effect_i3dl2_info.cpp + renderer/effect/effect_i3dl2_info.h + renderer/effect/effect_reset.h + renderer/effect/effect_info_base.h + renderer/effect/effect_light_limiter_info.cpp + renderer/effect/effect_light_limiter_info.h + renderer/effect/effect_result_state.h + renderer/effect/effect_reverb_info.h + renderer/effect/effect_reverb_info.cpp + renderer/mix/mix_context.cpp + renderer/mix/mix_context.h + renderer/mix/mix_info.cpp + renderer/mix/mix_info.h + renderer/memory/address_info.h + renderer/memory/memory_pool_info.cpp + renderer/memory/memory_pool_info.h + renderer/memory/pool_mapper.cpp + renderer/memory/pool_mapper.h + renderer/nodes/bit_array.h + renderer/nodes/edge_matrix.cpp + renderer/nodes/edge_matrix.h + renderer/nodes/node_states.cpp + renderer/nodes/node_states.h + renderer/performance/detail_aspect.cpp + renderer/performance/detail_aspect.h + renderer/performance/entry_aspect.cpp + renderer/performance/entry_aspect.h + renderer/performance/performance_detail.h + renderer/performance/performance_entry.h + renderer/performance/performance_entry_addresses.h + renderer/performance/performance_frame_header.h + renderer/performance/performance_manager.cpp + renderer/performance/performance_manager.h + renderer/sink/circular_buffer_sink_info.cpp + renderer/sink/circular_buffer_sink_info.h + renderer/sink/device_sink_info.cpp + renderer/sink/device_sink_info.h + renderer/sink/sink_context.cpp + renderer/sink/sink_context.h + renderer/sink/sink_info_base.cpp + renderer/sink/sink_info_base.h + renderer/splitter/splitter_context.cpp + renderer/splitter/splitter_context.h + renderer/splitter/splitter_destinations_data.cpp + renderer/splitter/splitter_destinations_data.h + renderer/splitter/splitter_info.cpp + renderer/splitter/splitter_info.h + renderer/system.cpp + renderer/system.h + renderer/system_manager.cpp + renderer/system_manager.h + renderer/upsampler/upsampler_info.h + renderer/upsampler/upsampler_manager.cpp + renderer/upsampler/upsampler_manager.h + renderer/upsampler/upsampler_state.h + renderer/voice/voice_channel_resource.h + renderer/voice/voice_context.cpp + renderer/voice/voice_context.h + renderer/voice/voice_info.cpp + renderer/voice/voice_info.h + renderer/voice/voice_state.h + sink/cubeb_sink.cpp + sink/cubeb_sink.h + sink/null_sink.h + sink/sdl2_sink.cpp + sink/sdl2_sink.h + sink/sink.h + sink/sink_details.cpp + sink/sink_details.h + sink/sink_stream.h ) create_target_directory_groups(audio_core) -if (NOT MSVC) +if (MSVC) + target_compile_options(audio_core PRIVATE + /we4242 # 'identifier': conversion from 'type1' to 'type2', possible loss of data + /we4244 # 'conversion': conversion from 'type1' to 'type2', possible loss of data + /we4245 # 'conversion': conversion from 'type1' to 'type2', signed/unsigned mismatch + /we4254 # 'operator': conversion from 'type1:field_bits' to 'type2:field_bits', possible loss of data + /we4456 # Declaration of 'identifier' hides previous local declaration + /we4457 # Declaration of 'identifier' hides function parameter + /we4458 # Declaration of 'identifier' hides class member + /we4459 # Declaration of 'identifier' hides global declaration + ) +else() target_compile_options(audio_core PRIVATE -Werror=conversion -Werror=ignored-qualifiers + -Werror=shadow + -Werror=unused-variable $<$:-Werror=unused-but-set-parameter> $<$:-Werror=unused-but-set-variable> @@ -58,6 +218,9 @@ if (NOT MSVC) endif() target_link_libraries(audio_core PUBLIC common core) +if (ARCHITECTURE_x86_64) + target_link_libraries(audio_core PRIVATE dynarmic) +endif() if(ENABLE_CUBEB) target_link_libraries(audio_core PRIVATE cubeb) diff --git a/src/audio_core/audio_core.cpp b/src/audio_core/audio_core.cpp new file mode 100755 index 000000000..cf7e763e6 --- /dev/null +++ b/src/audio_core/audio_core.cpp @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_core.h" +#include "audio_core/sink/sink_details.h" +#include "common/settings.h" +#include "core/core.h" + +namespace AudioCore { + +AudioCore::AudioCore(Core::System& system) : audio_manager{std::make_unique(system)} { + CreateSinks(); + // Must be created after the sinks + adsp = std::make_unique(system, *output_sink); +} + +AudioCore ::~AudioCore() { + Shutdown(); +} + +void AudioCore::CreateSinks() { + const auto& sink_id{Settings::values.sink_id}; + const auto& audio_output_device_id{Settings::values.audio_output_device_id}; + const auto& audio_input_device_id{Settings::values.audio_input_device_id}; + + output_sink = Sink::CreateSinkFromID(sink_id.GetValue(), audio_output_device_id.GetValue()); + input_sink = Sink::CreateSinkFromID(sink_id.GetValue(), audio_input_device_id.GetValue()); +} + +void AudioCore::Shutdown() { + audio_manager->Shutdown(); +} + +AudioManager& AudioCore::GetAudioManager() { + return *audio_manager; +} + +Sink::Sink& AudioCore::GetOutputSink() { + return *output_sink; +} + +Sink::Sink& AudioCore::GetInputSink() { + return *input_sink; +} + +AudioRenderer::ADSP::ADSP& AudioCore::GetADSP() { + return *adsp; +} + +void AudioCore::PauseSinks(const bool pausing) const { + if (pausing) { + output_sink->PauseStreams(); + input_sink->PauseStreams(); + } else { + output_sink->UnpauseStreams(); + input_sink->UnpauseStreams(); + } +} + +} // namespace AudioCore diff --git a/src/audio_core/audio_core.h b/src/audio_core/audio_core.h new file mode 100755 index 000000000..fd1e43356 --- /dev/null +++ b/src/audio_core/audio_core.h @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/audio_manager.h" +#include "audio_core/renderer/adsp/adsp.h" +#include "audio_core/sink/sink.h" + +namespace Core { +class System; +} + +namespace AudioCore { + +class AudioManager; +/** + * Main audio class, sotred inside the core, and holding the audio manager, all sinks, and the ADSP. + */ +class AudioCore { +public: + explicit AudioCore(Core::System& system); + ~AudioCore(); + + /** + * Shutdown the audio core. + */ + void Shutdown(); + + /** + * Get a reference to the audio manager. + * + * @return Ref to the audio manager. + */ + AudioManager& GetAudioManager(); + + /** + * Get the audio output sink currently in use. + * + * @return Ref to the sink. + */ + Sink::Sink& GetOutputSink(); + + /** + * Get the audio input sink currently in use. + * + * @return Ref to the sink. + */ + Sink::Sink& GetInputSink(); + + /** + * Get the ADSP. + * + * @return Ref to the ADSP. + */ + AudioRenderer::ADSP::ADSP& GetADSP(); + + /** + * Pause the sink. Called from the core. + * + * @param pausing - Is this pause due to an actual pause, or shutdown? + * Unfortunately, shutdown also pauses streams, which can cause issues. + */ + void PauseSinks(bool pausing) const; + +private: + /** + * Create the sinks on startup. + */ + void CreateSinks(); + + /// Main audio manager for audio in/out + std::unique_ptr audio_manager; + /// Sink used for audio renderer and audio out + std::unique_ptr output_sink; + /// Sink used for audio input + std::unique_ptr input_sink; + /// The ADSP in the sysmodule + std::unique_ptr adsp; +}; + +} // namespace AudioCore diff --git a/src/audio_core/audio_event.cpp b/src/audio_core/audio_event.cpp new file mode 100755 index 000000000..424049c7a --- /dev/null +++ b/src/audio_core/audio_event.cpp @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_event.h" +#include "common/assert.h" + +namespace AudioCore { + +size_t Event::GetManagerIndex(const Type type) const { + switch (type) { + case Type::AudioInManager: + return 0; + case Type::AudioOutManager: + return 1; + case Type::FinalOutputRecorderManager: + return 2; + case Type::Max: + return 3; + default: + UNREACHABLE(); + } + return 3; +} + +void Event::SetAudioEvent(const Type type, const bool signalled) { + events_signalled[GetManagerIndex(type)] = signalled; + if (signalled) { + manager_event.notify_one(); + } +} + +bool Event::CheckAudioEventSet(const Type type) const { + return events_signalled[GetManagerIndex(type)]; +} + +std::mutex& Event::GetAudioEventLock() { + return event_lock; +} + +std::condition_variable_any& Event::GetAudioEvent() { + return manager_event; +} + +bool Event::Wait(std::unique_lock& l, const std::chrono::seconds timeout) { + bool timed_out{false}; + if (!manager_event.wait_for(l, timeout, [&]() { + return std::ranges::any_of(events_signalled, [](bool x) { return x; }); + })) { + timed_out = true; + } + return timed_out; +} + +void Event::ClearEvents() { + events_signalled[0] = false; + events_signalled[1] = false; + events_signalled[2] = false; + events_signalled[3] = false; +} + +} // namespace AudioCore diff --git a/src/audio_core/audio_event.h b/src/audio_core/audio_event.h new file mode 100755 index 000000000..82dd32dca --- /dev/null +++ b/src/audio_core/audio_event.h @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +namespace AudioCore { +/** + * Responsible for the input/output events, set by the stream backend when buffers are consumed, and + * waited on by the audio manager. These callbacks signal the game's events to keep the audio buffer + * recycling going. + * In a real Switch this is not a seprate class, and exists entirely within the audio manager. + * On the Switch it's implemented more simply through a MultiWaitEventHolder, where it can + * wait on multiple events at once, and the events are not needed by the backend. + */ +class Event { +public: + enum class Type { + AudioInManager, + AudioOutManager, + FinalOutputRecorderManager, + Max, + }; + + /** + * Convert a manager type to an index. + * + * @param type - The manager type to convert + * @return The index of the type. + */ + size_t GetManagerIndex(Type type) const; + + /** + * Set an audio event to true or false. + * + * @param type - The manager type to signal. + * @param signalled - Its signal state. + */ + void SetAudioEvent(Type type, bool signalled); + + /** + * Check if the given manager type is signalled. + * + * @param type - The manager type to check. + * @return True if the event is signalled, otherwise false. + */ + bool CheckAudioEventSet(Type type) const; + + /** + * Get the lock for audio events. + * + * @return Reference to the lock. + */ + std::mutex& GetAudioEventLock(); + + /** + * Get the manager event, this signals the audio manager to release buffers and signal the game + * for more. + * + * @return Reference to the condition variable. + */ + std::condition_variable_any& GetAudioEvent(); + + /** + * Wait on the manager_event. + * + * @param l - Lock held by the wait. + * @param timeout - Timeout for the wait. This is 2 seconds by default. + * @return True if the wait timed out, otherwise false if signalled. + */ + bool Wait(std::unique_lock& l, std::chrono::seconds timeout); + + /** + * Reset all manager events. + */ + void ClearEvents(); + +private: + /// Lock, used bythe audio manager + std::mutex event_lock; + /// Array of events, one per system type (see Type), last event is used to terminate + std::array, 4> events_signalled; + /// Event to signal the audio manager + std::condition_variable_any manager_event; +}; + +} // namespace AudioCore diff --git a/src/audio_core/audio_in_manager.cpp b/src/audio_core/audio_in_manager.cpp new file mode 100755 index 000000000..a9e9acd90 --- /dev/null +++ b/src/audio_core/audio_in_manager.cpp @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_core.h" +#include "audio_core/audio_in_manager.h" +#include "audio_core/audio_manager.h" +#include "audio_core/in/audio_in.h" +#include "core/core.h" +#include "core/hle/service/audio/errors.h" + +namespace AudioCore::AudioIn { + +Manager::Manager(Core::System& system_) : system{system_} { + std::iota(session_ids.begin(), session_ids.end(), 0); + num_free_sessions = MaxInSessions; +} + +Result Manager::AcquireSessionId(size_t& session_id) { + if (num_free_sessions == 0) { + LOG_ERROR(Service_Audio, "All 4 AudioIn sessions are in use, cannot create any more"); + return Service::Audio::ERR_MAXIMUM_SESSIONS_REACHED; + } + session_id = session_ids[next_session_id]; + next_session_id = (next_session_id + 1) % MaxInSessions; + num_free_sessions--; + return ResultSuccess; +} + +void Manager::ReleaseSessionId(const size_t session_id) { + std::scoped_lock l{mutex}; + LOG_DEBUG(Service_Audio, "Freeing AudioIn session {}", session_id); + session_ids[free_session_id] = session_id; + num_free_sessions++; + free_session_id = (free_session_id + 1) % MaxInSessions; + sessions[session_id].reset(); + applet_resource_user_ids[session_id] = 0; +} + +Result Manager::LinkToManager() { + std::scoped_lock l{mutex}; + if (!linked_to_manager) { + AudioManager& manager{system.AudioCore().GetAudioManager()}; + manager.SetInManager(std::bind(&Manager::BufferReleaseAndRegister, this)); + linked_to_manager = true; + } + + return ResultSuccess; +} + +void Manager::Start() { + if (sessions_started) { + return; + } + + std::scoped_lock l{mutex}; + for (auto& session : sessions) { + if (session) { + session->StartSession(); + } + } + + sessions_started = true; +} + +void Manager::BufferReleaseAndRegister() { + std::scoped_lock l{mutex}; + for (auto& session : sessions) { + if (session != nullptr) { + session->ReleaseAndRegisterBuffers(); + } + } +} + +u32 Manager::GetDeviceNames(std::vector& names, + [[maybe_unused]] const u32 max_count, + [[maybe_unused]] const bool filter) { + std::scoped_lock l{mutex}; + + LinkToManager(); + + names.push_back(AudioRenderer::AudioDevice::AudioDeviceName("Uac")); + + return 1; +} + +} // namespace AudioCore::AudioIn diff --git a/src/audio_core/audio_in_manager.h b/src/audio_core/audio_in_manager.h new file mode 100755 index 000000000..75b73a0b6 --- /dev/null +++ b/src/audio_core/audio_in_manager.h @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "audio_core/renderer/audio_device.h" + +namespace Core { +class System; +} + +namespace AudioCore::AudioIn { +class In; + +constexpr size_t MaxInSessions = 4; +/** + * Manages all audio in sessions. + */ +class Manager { +public: + explicit Manager(Core::System& system); + + /** + * Acquire a free session id for opening a new audio in. + * + * @param session_id - Output session_id. + * @return Result code. + */ + Result AcquireSessionId(size_t& session_id); + + /** + * Release a session id on close. + * + * @param session_id - Session id to free. + */ + void ReleaseSessionId(size_t session_id); + + /** + * Link the audio in manager to the main audio manager. + * + * @return Result code. + */ + Result LinkToManager(); + + /** + * Start the audio in manager. + */ + void Start(); + + /** + * Callback function, called by the audio manager when the audio in event is signalled. + */ + void BufferReleaseAndRegister(); + + /** + * Get a list of audio in device names. + * + * @oaram names - Output container to write names to. + * @param max_count - Maximum numebr of deivce names to write. Unused + * @param filter - Should the list be filtered? Unused. + * @return Number of names written. + */ + u32 GetDeviceNames(std::vector& names, + u32 max_count, bool filter); + + /// Core system + Core::System& system; + /// Array of session ids + std::array session_ids{}; + /// Array of resource user ids + std::array applet_resource_user_ids{}; + /// Pointer to each open session + std::array, MaxInSessions> sessions{}; + /// The number of free sessions + size_t num_free_sessions{}; + /// The next session id to be taken + size_t next_session_id{}; + /// The next session id to be freed + size_t free_session_id{}; + /// Whether this is linked to the audio manager + bool linked_to_manager{}; + /// Whether the sessions have been started + bool sessions_started{}; + /// Protect state due to audio manager callback + std::recursive_mutex mutex{}; +}; + +} // namespace AudioCore::AudioIn diff --git a/src/audio_core/audio_manager.cpp b/src/audio_core/audio_manager.cpp new file mode 100755 index 000000000..2f1bba9c3 --- /dev/null +++ b/src/audio_core/audio_manager.cpp @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_in_manager.h" +#include "audio_core/audio_manager.h" +#include "audio_core/audio_out_manager.h" +#include "core/core.h" + +namespace AudioCore { + +AudioManager::AudioManager(Core::System& system_) : system{system_} { + thread = std::jthread([this]() { ThreadFunc(); }); +} + +void AudioManager::Shutdown() { + running = false; + events.SetAudioEvent(Event::Type::Max, true); + thread.join(); +} + +Result AudioManager::SetOutManager(BufferEventFunc buffer_func) { + if (!running) { + return Service::Audio::ERR_OPERATION_FAILED; + } + + std::scoped_lock l{lock}; + + const auto index{events.GetManagerIndex(Event::Type::AudioOutManager)}; + if (buffer_events[index] == nullptr) { + buffer_events[index] = buffer_func; + needs_update = true; + events.SetAudioEvent(Event::Type::AudioOutManager, true); + } + return ResultSuccess; +} + +Result AudioManager::SetInManager(BufferEventFunc buffer_func) { + if (!running) { + return Service::Audio::ERR_OPERATION_FAILED; + } + + std::scoped_lock l{lock}; + + const auto index{events.GetManagerIndex(Event::Type::AudioInManager)}; + if (buffer_events[index] == nullptr) { + buffer_events[index] = buffer_func; + needs_update = true; + events.SetAudioEvent(Event::Type::AudioInManager, true); + } + return ResultSuccess; +} + +void AudioManager::SetEvent(const Event::Type type, const bool signalled) { + events.SetAudioEvent(type, signalled); +} + +void AudioManager::ThreadFunc() { + std::unique_lock l{events.GetAudioEventLock()}; + events.ClearEvents(); + running = true; + + while (running) { + auto timed_out{events.Wait(l, std::chrono::seconds(2))}; + + if (events.CheckAudioEventSet(Event::Type::Max)) { + break; + } + + for (size_t i = 0; i < buffer_events.size(); i++) { + if (events.CheckAudioEventSet(Event::Type(i)) || timed_out) { + if (buffer_events[i]) { + buffer_events[i](); + } + } + events.SetAudioEvent(Event::Type(i), false); + } + } +} + +} // namespace AudioCore diff --git a/src/audio_core/audio_manager.h b/src/audio_core/audio_manager.h new file mode 100755 index 000000000..70316e9cb --- /dev/null +++ b/src/audio_core/audio_manager.h @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#include "audio_core/audio_event.h" +#include "core/hle/service/audio/errors.h" + +namespace Core { +class System; +} + +namespace AudioCore { + +namespace AudioOut { +class Manager; +} + +namespace AudioIn { +class Manager; +} + +/** + * The AudioManager's main purpose is to wait for buffer events for the audio in and out managers, + * and call an associated callback to release buffers. + * + * Execution pattern is: + * Buffers appended -> + * Buffers queued and played by the backend stream -> + * When consumed, set the corresponding manager event and signal the audio manager -> + * Consumed buffers are released, game is signalled -> + * Game appends more buffers. + * + * This is only used by audio in and audio out. + */ +class AudioManager { + using BufferEventFunc = std::function; + +public: + explicit AudioManager(Core::System& system); + + /** + * Shutdown the audio manager. + */ + void Shutdown(); + + /** + * Register the out manager, keeping a function to be called when the out event is signalled. + * + * @param buffer_func - Function to be called on signal. + * @return Result code. + */ + Result SetOutManager(BufferEventFunc buffer_func); + + /** + * Register the in manager, keeping a function to be called when the in event is signalled. + * + * @param buffer_func - Function to be called on signal. + * @return Result code. + */ + Result SetInManager(BufferEventFunc buffer_func); + + /** + * Set an event to signalled, and signal the thread. + * + * @param type - Manager type to set. + * @param signalled - Set the event to true or false? + */ + void SetEvent(Event::Type type, bool signalled); + +private: + /** + * Main thread, waiting on a manager signal and calling the registered fucntion. + */ + void ThreadFunc(); + + /// Core system + Core::System& system; + /// Have sessions started palying? + bool sessions_started{}; + /// Is the main thread running? + std::atomic running{}; + /// Unused + bool needs_update{}; + /// Events to be set and signalled + Event events{}; + /// Callbacks for each manager + std::array buffer_events{}; + /// General lock + std::mutex lock{}; + /// Main thread for waiting and callbacks + std::jthread thread; +}; + +} // namespace AudioCore diff --git a/src/audio_core/audio_out_manager.cpp b/src/audio_core/audio_out_manager.cpp new file mode 100755 index 000000000..71d67de64 --- /dev/null +++ b/src/audio_core/audio_out_manager.cpp @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_core.h" +#include "audio_core/audio_manager.h" +#include "audio_core/audio_out_manager.h" +#include "audio_core/out/audio_out.h" +#include "core/core.h" +#include "core/hle/kernel/k_event.h" +#include "core/hle/service/audio/errors.h" + +namespace AudioCore::AudioOut { + +Manager::Manager(Core::System& system_) : system{system_} { + std::iota(session_ids.begin(), session_ids.end(), 0); + num_free_sessions = MaxOutSessions; +} + +Result Manager::AcquireSessionId(size_t& session_id) { + if (num_free_sessions == 0) { + LOG_ERROR(Service_Audio, "All 12 Audio Out sessions are in use, cannot create any more"); + return Service::Audio::ERR_MAXIMUM_SESSIONS_REACHED; + } + session_id = session_ids[next_session_id]; + next_session_id = (next_session_id + 1) % MaxOutSessions; + num_free_sessions--; + return ResultSuccess; +} + +void Manager::ReleaseSessionId(const size_t session_id) { + std::scoped_lock l{mutex}; + LOG_DEBUG(Service_Audio, "Freeing AudioOut session {}", session_id); + session_ids[free_session_id] = session_id; + num_free_sessions++; + free_session_id = (free_session_id + 1) % MaxOutSessions; + sessions[session_id].reset(); + applet_resource_user_ids[session_id] = 0; +} + +Result Manager::LinkToManager() { + std::scoped_lock l{mutex}; + if (!linked_to_manager) { + AudioManager& manager{system.AudioCore().GetAudioManager()}; + manager.SetOutManager(std::bind(&Manager::BufferReleaseAndRegister, this)); + linked_to_manager = true; + } + + return ResultSuccess; +} + +void Manager::Start() { + if (sessions_started) { + return; + } + + std::scoped_lock l{mutex}; + for (auto& session : sessions) { + if (session) { + session->StartSession(); + } + } + + sessions_started = true; +} + +void Manager::BufferReleaseAndRegister() { + std::scoped_lock l{mutex}; + for (auto& session : sessions) { + if (session != nullptr) { + session->ReleaseAndRegisterBuffers(); + } + } +} + +u32 Manager::GetAudioOutDeviceNames( + std::vector& names) const { + names.push_back({"DeviceOut"}); + return 1; +} + +} // namespace AudioCore::AudioOut diff --git a/src/audio_core/audio_out_manager.h b/src/audio_core/audio_out_manager.h new file mode 100755 index 000000000..24981e08f --- /dev/null +++ b/src/audio_core/audio_out_manager.h @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/audio_device.h" + +namespace Core { +class System; +} + +namespace AudioCore::AudioOut { +class Out; + +constexpr size_t MaxOutSessions = 12; +/** + * Manages all audio out sessions. + */ +class Manager { +public: + explicit Manager(Core::System& system); + + /** + * Acquire a free session id for opening a new audio out. + * + * @param session_id - Output session_id. + * @return Result code. + */ + Result AcquireSessionId(size_t& session_id); + + /** + * Release a session id on close. + * + * @param session_id - Session id to free. + */ + void ReleaseSessionId(size_t session_id); + + /** + * Link this manager to the main audio manager. + * + * @return Result code. + */ + Result LinkToManager(); + + /** + * Start the audio out manager. + */ + void Start(); + + /** + * Callback function, called by the audio manager when the audio out event is signalled. + */ + void BufferReleaseAndRegister(); + + /** + * Get a list of audio out device names. + * + * @oaram names - Output container to write names to. + * @return Number of names written. + */ + u32 GetAudioOutDeviceNames( + std::vector& names) const; + + /// Core system + Core::System& system; + /// Array of session ids + std::array session_ids{}; + /// Array of resource user ids + std::array applet_resource_user_ids{}; + /// Pointer to each open session + std::array, MaxOutSessions> sessions{}; + /// The number of free sessions + size_t num_free_sessions{}; + /// The next session id to be taken + size_t next_session_id{}; + /// The next session id to be freed + size_t free_session_id{}; + /// Whether this is linked to the audio manager + bool linked_to_manager{}; + /// Whether the sessions have been started + bool sessions_started{}; + /// Protect state due to audio manager callback + std::recursive_mutex mutex{}; +}; + +} // namespace AudioCore::AudioOut diff --git a/src/audio_core/audio_render_manager.cpp b/src/audio_core/audio_render_manager.cpp new file mode 100755 index 000000000..7a846835b --- /dev/null +++ b/src/audio_core/audio_render_manager.cpp @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_render_manager.h" +#include "audio_core/common/audio_renderer_parameter.h" +#include "audio_core/common/feature_support.h" +#include "core/core.h" + +namespace AudioCore::AudioRenderer { + +Manager::Manager(Core::System& system_) + : system{system_}, system_manager{std::make_unique(system)} { + std::iota(session_ids.begin(), session_ids.end(), 0); +} + +Manager::~Manager() { + Stop(); +} + +void Manager::Stop() { + system_manager->Stop(); +} + +SystemManager& Manager::GetSystemManager() { + return *system_manager; +} + +auto Manager::GetWorkBufferSize(const AudioRendererParameterInternal& params, u64& out_count) + -> Result { + if (!CheckValidRevision(params.revision)) { + return Service::Audio::ERR_INVALID_REVISION; + } + + out_count = System::GetWorkBufferSize(params); + + return ResultSuccess; +} + +s32 Manager::GetSessionId() { + std::scoped_lock l{session_lock}; + auto session_id{session_ids[session_count]}; + + if (session_id == -1) { + return -1; + } + + session_ids[session_count] = -1; + session_count++; + return session_id; +} + +void Manager::ReleaseSessionId(const s32 session_id) { + std::scoped_lock l{session_lock}; + session_ids[--session_count] = session_id; +} + +u32 Manager::GetSessionCount() { + std::scoped_lock l{session_lock}; + return session_count; +} + +bool Manager::AddSystem(System& system_) { + return system_manager->Add(system_); +} + +bool Manager::RemoveSystem(System& system_) { + return system_manager->Remove(system_); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/audio_render_manager.h b/src/audio_core/audio_render_manager.h new file mode 100755 index 000000000..6a508ec56 --- /dev/null +++ b/src/audio_core/audio_render_manager.h @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/system_manager.h" +#include "core/hle/service/audio/errors.h" + +namespace Core { +class System; +} + +namespace AudioCore { +struct AudioRendererParameterInternal; + +namespace AudioRenderer { +/** + * Wrapper for the audio system manager, handles service calls. + */ +class Manager { +public: + explicit Manager(Core::System& system); + ~Manager(); + + /** + * Stop the manager. + */ + void Stop(); + + /** + * Get the system manager. + * + * @return The system manager. + */ + SystemManager& GetSystemManager(); + + /** + * Get required size for the audio renderer workbuffer. + * + * @param params - Input parameters with the numbers of voices/mixes/sinks etc. + * @param out_count - Output size of the required workbuffer. + * @return Result code. + */ + Result GetWorkBufferSize(const AudioRendererParameterInternal& params, u64& out_count); + + /** + * Get a new session id. + * + * @return The new session id. -1 if invalid, otherwise 0-MaxRendererSessions. + */ + s32 GetSessionId(); + + /** + * Get the number of currently active sessions. + * + * @return The number of active sessions. + */ + u32 GetSessionCount(); + + /** + * Add a renderer system to the manager. + * The system will be reguarly called to generate commands for the AudioRenderer. + * + * @param system - The system to add. + * @return True if the system was sucessfully added, otherwise false. + */ + bool AddSystem(System& system); + + /** + * Remove a renderer system from the manager. + * + * @param system - The system to remove. + * @return True if the system was sucessfully removed, otherwise false. + */ + bool RemoveSystem(System& system); + + /** + * Free a session id when the system wants to shut down. + * + * @param session_id - The session id to free. + */ + void ReleaseSessionId(s32 session_id); + +private: + /// Core system + Core::System& system; + /// Session ids, -1 when in use + std::array session_ids{}; + /// Number of active renderers + u32 session_count{}; + /// Lock for interacting with the sessions + std::mutex session_lock{}; + /// Regularly generates commands from the registered systems for the AudioRenderer + std::unique_ptr system_manager{}; +}; + +} // namespace AudioRenderer +} // namespace AudioCore diff --git a/src/audio_core/common/audio_renderer_parameter.h b/src/audio_core/common/audio_renderer_parameter.h new file mode 100755 index 000000000..2f62c383b --- /dev/null +++ b/src/audio_core/common/audio_renderer_parameter.h @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/memory/memory_pool_info.h" +#include "audio_core/renderer/upsampler/upsampler_manager.h" +#include "common/common_types.h" + +namespace AudioCore { +/** + * Execution mode of the audio renderer. + * Only Auto is currently supported. + */ +enum class ExecutionMode : u8 { + Auto, + Manual, +}; + +/** + * Parameters from the game, passed to the audio renderer for initialisation. + */ +struct AudioRendererParameterInternal { + /* 0x00 */ u32 sample_rate; + /* 0x04 */ u32 sample_count; + /* 0x08 */ u32 mixes; + /* 0x0C */ u32 sub_mixes; + /* 0x10 */ u32 voices; + /* 0x14 */ u32 sinks; + /* 0x18 */ u32 effects; + /* 0x1C */ u32 perf_frames; + /* 0x20 */ u16 voice_drop_enabled; + /* 0x22 */ u8 rendering_device; + /* 0x23 */ ExecutionMode execution_mode; + /* 0x24 */ u32 splitter_infos; + /* 0x28 */ s32 splitter_destinations; + /* 0x2C */ u32 external_context_size; + /* 0x30 */ u32 revision; + /* 0x34 */ char unk34[0x4]; +}; +static_assert(sizeof(AudioRendererParameterInternal) == 0x38, + "AudioRendererParameterInternal has the wrong size!"); + +/** + * Context for rendering, contains a bunch of useful fields for the command generator. + */ +struct AudioRendererSystemContext { + s32 session_id; + s8 channels; + s16 mix_buffer_count; + AudioRenderer::BehaviorInfo* behavior; + std::span depop_buffer; + AudioRenderer::UpsamplerManager* upsampler_manager; + AudioRenderer::MemoryPoolInfo* memory_pool_info; +}; + +} // namespace AudioCore diff --git a/src/audio_core/common/common.h b/src/audio_core/common/common.h new file mode 100755 index 000000000..ce3b09480 --- /dev/null +++ b/src/audio_core/common/common.h @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "common/assert.h" +#include "common/common_funcs.h" +#include "common/common_types.h" + +namespace AudioCore { +using CpuAddr = std::uintptr_t; + +enum class PlayState : u8 { + Started, + Stopped, + Paused, +}; + +enum class SrcQuality : u8 { + Medium, + High, + Low, +}; + +enum class SampleFormat : u8 { + Invalid, + PcmInt8, + PcmInt16, + PcmInt24, + PcmInt32, + PcmFloat, + Adpcm, +}; + +enum class SessionTypes { + AudioIn, + AudioOut, + FinalOutputRecorder, +}; + +constexpr u32 BufferCount = 32; + +constexpr u32 MaxRendererSessions = 2; +constexpr u32 TargetSampleCount = 240; +constexpr u32 TargetSampleRate = 48'000; +constexpr u32 MaxChannels = 6; +constexpr u32 MaxMixBuffers = 24; +constexpr u32 MaxWaveBuffers = 4; +constexpr s32 LowestVoicePriority = 0xFF; +constexpr s32 HighestVoicePriority = 0; +constexpr u32 BufferAlignment = 0x40; +constexpr u32 WorkbufferAlignment = 0x1000; +constexpr s32 FinalMixId = 0; +constexpr s32 InvalidDistanceFromFinalMix = std::numeric_limits::min(); +constexpr s32 UnusedSplitterId = -1; +constexpr s32 UnusedMixId = std::numeric_limits::max(); +constexpr u32 InvalidNodeId = 0xF0000000; +constexpr s32 InvalidProcessOrder = -1; +constexpr u32 MaxBiquadFilters = 2; +constexpr u32 MaxEffects = 256; + +constexpr bool IsChannelCountValid(u16 channel_count) { + return channel_count <= 6 && + (channel_count == 1 || channel_count == 2 || channel_count == 4 || channel_count == 6); +} + +constexpr u32 GetSplitterInParamHeaderMagic() { + return Common::MakeMagic('S', 'N', 'D', 'H'); +} + +constexpr u32 GetSplitterInfoMagic() { + return Common::MakeMagic('S', 'N', 'D', 'I'); +} + +constexpr u32 GetSplitterSendDataMagic() { + return Common::MakeMagic('S', 'N', 'D', 'D'); +} + +constexpr size_t GetSampleFormatByteSize(SampleFormat format) { + switch (format) { + case SampleFormat::PcmInt8: + return 1; + case SampleFormat::PcmInt16: + return 2; + case SampleFormat::PcmInt24: + return 3; + case SampleFormat::PcmInt32: + case SampleFormat::PcmFloat: + return 4; + default: + return 2; + } +} + +} // namespace AudioCore diff --git a/src/audio_core/common/feature_support.h b/src/audio_core/common/feature_support.h new file mode 100755 index 000000000..ba9e977fd --- /dev/null +++ b/src/audio_core/common/feature_support.h @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "common/assert.h" +#include "common/common_funcs.h" +#include "common/common_types.h" + +namespace AudioCore { +constexpr u32 CurrentRevision = 10; + +enum class SupportTags { + CommandProcessingTimeEstimatorVersion4, + CommandProcessingTimeEstimatorVersion3, + CommandProcessingTimeEstimatorVersion2, + MultiTapBiquadFilterProcessing, + EffectInfoVer2, + WaveBufferVer2, + BiquadFilterFloatProcessing, + VolumeMixParameterPrecisionQ23, + MixInParameterDirtyOnlyUpdate, + BiquadFilterEffectStateClearBugFix, + VoicePlayedSampleCountResetAtLoopPoint, + VoicePitchAndSrcSkipped, + SplitterBugFix, + FlushVoiceWaveBuffers, + ElapsedFrameCount, + AudioRendererVariadicCommandBufferSize, + PerformanceMetricsDataFormatVersion2, + AudioRendererProcessingTimeLimit80Percent, + AudioRendererProcessingTimeLimit75Percent, + AudioRendererProcessingTimeLimit70Percent, + AdpcmLoopContextBugFix, + Splitter, + LongSizePreDelay, + AudioUsbDeviceOutput, + DeviceApiVersion2, + + // Not a real tag, just here to get the count. + Size +}; + +constexpr u32 GetRevisionNum(u32 user_revision) { + if (user_revision >= 0x100) { + user_revision -= Common::MakeMagic('R', 'E', 'V', '0'); + user_revision >>= 24; + } + return user_revision; +}; + +constexpr bool CheckFeatureSupported(SupportTags tag, u32 user_revision) { + constexpr std::array, static_cast(SupportTags::Size)> features{ + { + {SupportTags::AudioRendererProcessingTimeLimit70Percent, 1}, + {SupportTags::Splitter, 2}, + {SupportTags::AdpcmLoopContextBugFix, 2}, + {SupportTags::LongSizePreDelay, 3}, + {SupportTags::AudioUsbDeviceOutput, 4}, + {SupportTags::AudioRendererProcessingTimeLimit75Percent, 4}, + {SupportTags::VoicePlayedSampleCountResetAtLoopPoint, 5}, + {SupportTags::VoicePitchAndSrcSkipped, 5}, + {SupportTags::SplitterBugFix, 5}, + {SupportTags::FlushVoiceWaveBuffers, 5}, + {SupportTags::ElapsedFrameCount, 5}, + {SupportTags::AudioRendererProcessingTimeLimit80Percent, 5}, + {SupportTags::AudioRendererVariadicCommandBufferSize, 5}, + {SupportTags::PerformanceMetricsDataFormatVersion2, 5}, + {SupportTags::CommandProcessingTimeEstimatorVersion2, 5}, + {SupportTags::BiquadFilterEffectStateClearBugFix, 6}, + {SupportTags::BiquadFilterFloatProcessing, 7}, + {SupportTags::VolumeMixParameterPrecisionQ23, 7}, + {SupportTags::MixInParameterDirtyOnlyUpdate, 7}, + {SupportTags::WaveBufferVer2, 8}, + {SupportTags::CommandProcessingTimeEstimatorVersion3, 8}, + {SupportTags::EffectInfoVer2, 9}, + {SupportTags::CommandProcessingTimeEstimatorVersion4, 10}, + {SupportTags::MultiTapBiquadFilterProcessing, 10}, + }}; + + const auto& feature = + std::ranges::find_if(features, [tag](const auto& entry) { return entry.first == tag; }); + if (feature == features.cend()) { + LOG_ERROR(Service_Audio, "Invalid SupportTag {}!", static_cast(tag)); + return false; + } + user_revision = GetRevisionNum(user_revision); + return (*feature).second <= user_revision; +} + +constexpr bool CheckValidRevision(u32 user_revision) { + return GetRevisionNum(user_revision) <= CurrentRevision; +}; + +} // namespace AudioCore diff --git a/src/audio_core/common/wave_buffer.h b/src/audio_core/common/wave_buffer.h new file mode 100755 index 000000000..fc478ef79 --- /dev/null +++ b/src/audio_core/common/wave_buffer.h @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/common_types.h" + +namespace AudioCore { + +struct WaveBufferVersion1 { + CpuAddr buffer; + u64 buffer_size; + u32 start_offset; + u32 end_offset; + bool loop; + bool stream_ended; + CpuAddr context; + u64 context_size; +}; + +struct WaveBufferVersion2 { + CpuAddr buffer; + CpuAddr context; + u64 buffer_size; + u64 context_size; + u32 start_offset; + u32 end_offset; + u32 loop_start_offset; + u32 loop_end_offset; + s32 loop_count; + bool loop; + bool stream_ended; +}; + +} // namespace AudioCore diff --git a/src/audio_core/common/workbuffer_allocator.h b/src/audio_core/common/workbuffer_allocator.h new file mode 100755 index 000000000..fb89f97fe --- /dev/null +++ b/src/audio_core/common/workbuffer_allocator.h @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "common/alignment.h" +#include "common/assert.h" +#include "common/common_types.h" + +namespace AudioCore { +/** + * Responsible for allocating up a workbuffer into multiple pieces. + * Takes in a buffer and size (it does not own them), and allocates up the buffer via Allocate. + */ +class WorkbufferAllocator { +public: + explicit WorkbufferAllocator(std::span buffer_, u64 size_) + : buffer{reinterpret_cast(buffer_.data())}, size{size_} {} + + /** + * Allocate the given count of T elements, aligned to alignment. + * + * @param count - The number of elements to allocate. + * @param alignment - The required starting alignment. + * @return Non-owning container of allocated elements. + */ + template + std::span Allocate(u64 count, u64 alignment) { + u64 out{0}; + u64 byte_size{count * sizeof(T)}; + + if (byte_size > 0) { + auto current{buffer + offset}; + auto aligned_buffer{Common::AlignUp(current, alignment)}; + if (aligned_buffer + byte_size <= buffer + size) { + out = aligned_buffer; + offset = byte_size - buffer + aligned_buffer; + } else { + LOG_ERROR( + Service_Audio, + "Allocated buffer was too small to hold new alloc.\nAllocator size={:08X}, " + "offset={:08X}.\nAttempting to allocate {:08X} with alignment={:02X}", + size, offset, byte_size, alignment); + count = 0; + } + } + + return std::span(reinterpret_cast(out), count); + } + + /** + * Align the current offset to the given alignment. + * + * @param alignment - The required starting alignment. + */ + void Align(u64 alignment) { + auto current{buffer + offset}; + auto aligned_buffer{Common::AlignUp(current, alignment)}; + offset = 0 - buffer + aligned_buffer; + } + + /** + * Get the current buffer offset. + * + * @return The current allocating offset. + */ + u64 GetCurrentOffset() const { + return offset; + } + + /** + * Get the current buffer size. + * + * @return The size of the current buffer. + */ + u64 GetSize() const { + return size; + } + + /** + * Get the remaining size that can be allocated. + * + * @return The remaining size left in the buffer. + */ + u64 GetRemainingSize() const { + return size - offset; + } + +private: + /// The buffer into which we are allocating. + u64 buffer; + /// Size of the buffer we're allocating to. + u64 size; + /// Current offset into the buffer, an error will be thrown if it exceeds size. + u64 offset{}; +}; + +} // namespace AudioCore diff --git a/src/audio_core/device/audio_buffer.h b/src/audio_core/device/audio_buffer.h new file mode 100755 index 000000000..cae7fa970 --- /dev/null +++ b/src/audio_core/device/audio_buffer.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/common_types.h" + +namespace AudioCore { + +struct AudioBuffer { + /// Timestamp this buffer completed playing. + s64 played_timestamp; + /// Game memory address for these samples. + VAddr samples; + /// Unqiue identifier for this buffer. + u64 tag; + /// Size of the samples buffer. + u64 size; +}; + +} // namespace AudioCore diff --git a/src/audio_core/device/audio_buffers.h b/src/audio_core/device/audio_buffers.h new file mode 100755 index 000000000..5d1979ea0 --- /dev/null +++ b/src/audio_core/device/audio_buffers.h @@ -0,0 +1,304 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "audio_buffer.h" +#include "audio_core/device/device_session.h" +#include "core/core_timing.h" + +namespace AudioCore { + +constexpr s32 BufferAppendLimit = 4; + +/** + * A ringbuffer of N audio buffers. + * The buffer contains 3 sections: + * Appended - Buffers added to the ring, but have yet to be sent to the audio backend. + * Registered - Buffers sent to the backend and queued for playback. + * Released - Buffers which have been played, and can now be recycled. + * Any others are free/untracked. + * + * @tparam N - Maximum number of buffers in the ring. + */ +template +class AudioBuffers { +public: + explicit AudioBuffers(size_t limit) : append_limit{static_cast(limit)} {} + + /** + * Append a new audio buffer to the ring. + * + * @param buffer - The new buffer. + */ + void AppendBuffer(AudioBuffer& buffer) { + std::scoped_lock l{lock}; + buffers[appended_index] = buffer; + appended_count++; + appended_index = (appended_index + 1) % append_limit; + } + + /** + * Register waiting buffers, up to a maximum of BufferAppendLimit. + * + * @param out_buffers - The buffers which were registered. + */ + void RegisterBuffers(std::vector& out_buffers) { + std::scoped_lock l{lock}; + const s32 to_register{std::min(std::min(appended_count, BufferAppendLimit), + BufferAppendLimit - registered_count)}; + + for (s32 i = 0; i < to_register; i++) { + s32 index{appended_index - appended_count}; + if (index < 0) { + index += N; + } + out_buffers.push_back(buffers[index]); + registered_count++; + registered_index = (registered_index + 1) % append_limit; + + appended_count--; + if (appended_count == 0) { + break; + } + } + } + + /** + * Release a single buffer. Must be already registered. + * + * @param index - The buffer index to release. + * @param timestamp - The released timestamp for this buffer. + */ + void ReleaseBuffer(s32 index, s64 timestamp) { + std::scoped_lock l{lock}; + buffers[index].played_timestamp = timestamp; + + registered_count--; + released_count++; + released_index = (released_index + 1) % append_limit; + } + + /** + * Release all registered buffers. + * + * @param timestamp - The released timestamp for this buffer. + * @return Is the buffer was released. + */ + bool ReleaseBuffers(Core::Timing::CoreTiming& core_timing, DeviceSession& session) { + std::scoped_lock l{lock}; + bool buffer_released{false}; + while (registered_count > 0) { + auto index{registered_index - registered_count}; + if (index < 0) { + index += N; + } + + // Check with the backend if this buffer can be released yet. + if (!session.IsBufferConsumed(buffers[index].tag)) { + break; + } + + ReleaseBuffer(index, core_timing.GetGlobalTimeNs().count()); + buffer_released = true; + } + + return buffer_released || registered_count == 0; + } + + /** + * Get all released buffers. + * + * @param tags - Container to be filled with the released buffers' tags. + * @return The number of buffers released. + */ + u32 GetReleasedBuffers(std::span tags) { + std::scoped_lock l{lock}; + u32 released{0}; + + while (released_count > 0) { + auto index{released_index - released_count}; + if (index < 0) { + index += N; + } + + auto& buffer{buffers[index]}; + released_count--; + + auto tag{buffer.tag}; + buffer.played_timestamp = 0; + buffer.samples = 0; + buffer.tag = 0; + buffer.size = 0; + + if (tag == 0) { + break; + } + + tags[released++] = tag; + + if (released >= tags.size()) { + break; + } + } + + return released; + } + + /** + * Get all appended and registered buffers. + * + * @param buffers_flushed - Output vector for the buffers which are released. + * @param max_buffers - Maximum number of buffers to released. + * @return The number of buffers released. + */ + u32 GetRegisteredAppendedBuffers(std::vector& buffers_flushed, u32 max_buffers) { + std::scoped_lock l{lock}; + if (registered_count + appended_count == 0) { + return 0; + } + + size_t buffers_to_flush{ + std::min(static_cast(registered_count + appended_count), max_buffers)}; + if (buffers_to_flush == 0) { + return 0; + } + + while (registered_count > 0) { + auto index{registered_index - registered_count}; + if (index < 0) { + index += N; + } + + buffers_flushed.push_back(buffers[index]); + + registered_count--; + released_count++; + released_index = (released_index + 1) % append_limit; + + if (buffers_flushed.size() >= buffers_to_flush) { + break; + } + } + + while (appended_count > 0) { + auto index{appended_index - appended_count}; + if (index < 0) { + index += N; + } + + buffers_flushed.push_back(buffers[index]); + + appended_count--; + released_count++; + released_index = (released_index + 1) % append_limit; + + if (buffers_flushed.size() >= buffers_to_flush) { + break; + } + } + + return static_cast(buffers_flushed.size()); + } + + /** + * Check if the given tag is in the buffers. + * + * @param tag - Unique tag of the buffer to search for. + * @return True if the buffer is still in the ring, otherwise false. + */ + bool ContainsBuffer(const u64 tag) const { + std::scoped_lock l{lock}; + const auto registered_buffers{appended_count + registered_count + released_count}; + + if (registered_buffers == 0) { + return false; + } + + auto index{released_index - released_count}; + if (index < 0) { + index += append_limit; + } + + for (s32 i = 0; i < registered_buffers; i++) { + if (buffers[index].tag == tag) { + return true; + } + index = (index + 1) % append_limit; + } + + return false; + } + + /** + * Get the number of active buffers in the ring. + * That is, appended, registered and released buffers. + * + * @return Number of active buffers. + */ + u32 GetAppendedRegisteredCount() const { + std::scoped_lock l{lock}; + return appended_count + registered_count; + } + + /** + * Get the total number of active buffers in the ring. + * That is, appended, registered and released buffers. + * + * @return Number of active buffers. + */ + u32 GetTotalBufferCount() const { + std::scoped_lock l{lock}; + return static_cast(appended_count + registered_count + released_count); + } + + /** + * Flush all of the currently appended and registered buffers + * + * @param buffers_released - Output count for the number of buffers released. + * @return True if buffers were successfully flushed, otherwise false. + */ + bool FlushBuffers(u32& buffers_released) { + std::scoped_lock l{lock}; + std::vector buffers_flushed{}; + + buffers_released = GetRegisteredAppendedBuffers(buffers_flushed, append_limit); + + if (registered_count > 0) { + return false; + } + + if (static_cast(released_count + appended_count) > append_limit) { + return false; + } + + return true; + } + +private: + /// Buffer lock + mutable std::recursive_mutex lock{}; + /// The audio buffers + std::array buffers{}; + /// Current released index + s32 released_index{}; + /// Number of released buffers + s32 released_count{}; + /// Current registered index + s32 registered_index{}; + /// Number of registered buffers + s32 registered_count{}; + /// Current appended index + s32 appended_index{}; + /// Number of appended buffers + s32 appended_count{}; + /// Maximum number of buffers (default 32) + u32 append_limit{}; +}; + +} // namespace AudioCore diff --git a/src/audio_core/device/device_session.cpp b/src/audio_core/device/device_session.cpp new file mode 100755 index 000000000..9b211116d --- /dev/null +++ b/src/audio_core/device/device_session.cpp @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_core.h" +#include "audio_core/audio_manager.h" +#include "audio_core/device/audio_buffer.h" +#include "audio_core/device/device_session.h" +#include "audio_core/sink/sink_stream.h" +#include "core/core.h" +#include "core/memory.h" + +namespace AudioCore { + +DeviceSession::DeviceSession(Core::System& system_) : system{system_} {} + +DeviceSession::~DeviceSession() { + Finalize(); +} + +Result DeviceSession::Initialize(std::string_view name_, SampleFormat sample_format_, + u16 channel_count_, size_t session_id_, u32 handle_, + u64 applet_resource_user_id_, Sink::StreamType type_) { + if (stream) { + Finalize(); + } + name = fmt::format("{}-{}", name_, session_id_); + type = type_; + sample_format = sample_format_; + channel_count = channel_count_; + session_id = session_id_; + handle = handle_; + applet_resource_user_id = applet_resource_user_id_; + + if (type == Sink::StreamType::In) { + sink = &system.AudioCore().GetInputSink(); + } else { + sink = &system.AudioCore().GetOutputSink(); + } + stream = sink->AcquireSinkStream(system, channel_count, name, type); + return ResultSuccess; +} + +void DeviceSession::Finalize() { + Stop(); + sink->CloseStream(stream); + stream = nullptr; +} + +void DeviceSession::Start() { + stream->SetPlayedSampleCount(played_sample_count); + stream->Start(); +} + +void DeviceSession::Stop() { + if (stream) { + played_sample_count = stream->GetPlayedSampleCount(); + stream->Stop(); + } +} + +void DeviceSession::AppendBuffers(std::span buffers) const { + auto& memory{system.Memory()}; + + for (size_t i = 0; i < buffers.size(); i++) { + Sink::SinkBuffer new_buffer{ + .frames = buffers[i].size / (channel_count * sizeof(s16)), + .frames_played = 0, + .tag = buffers[i].tag, + .consumed = false, + }; + + if (type == Sink::StreamType::In) { + std::vector samples{}; + stream->AppendBuffer(new_buffer, samples); + } else { + std::vector samples(buffers[i].size / sizeof(s16)); + memory.ReadBlockUnsafe(buffers[i].samples, samples.data(), buffers[i].size); + stream->AppendBuffer(new_buffer, samples); + } + } +} + +void DeviceSession::ReleaseBuffer(AudioBuffer& buffer) const { + if (type == Sink::StreamType::In) { + auto& memory{system.Memory()}; + auto samples{stream->ReleaseBuffer(buffer.size / sizeof(s16))}; + memory.WriteBlockUnsafe(buffer.samples, samples.data(), buffer.size); + } +} + +bool DeviceSession::IsBufferConsumed(u64 tag) const { + if (stream) { + return stream->IsBufferConsumed(tag); + } + return true; +} + +void DeviceSession::SetVolume(f32 volume) const { + if (stream) { + stream->SetSystemVolume(volume); + } +} + +u64 DeviceSession::GetPlayedSampleCount() const { + if (stream) { + return stream->GetPlayedSampleCount(); + } + return 0; +} + +} // namespace AudioCore diff --git a/src/audio_core/device/device_session.h b/src/audio_core/device/device_session.h new file mode 100755 index 000000000..24a02f41f --- /dev/null +++ b/src/audio_core/device/device_session.h @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "audio_core/sink/sink.h" +#include "core/hle/service/audio/errors.h" + +namespace Core { +class System; +} + +namespace AudioCore { +namespace Sink { +class SinkStream; +struct SinkBuffer; +} // namespace Sink + +struct AudioBuffer; + +/** + * Represents an input or output device stream for audio in and audio out (not used for render). + **/ +class DeviceSession { +public: + explicit DeviceSession(Core::System& system); + ~DeviceSession(); + + /** + * Initialize this device session. + * + * @param name - Name of this device. + * @param sample_format - Sample format for this device's output. + * @param channel_count - Number of channels for this device (2 or 6). + * @param session_id - This session's id. + * @param handle - Handle for this device session (unused). + * @param applet_resource_user_id - Applet resource user id for this device session (unused). + * @param type - Type of this stream (Render, In, Out). + * @return Result code for this call. + */ + Result Initialize(std::string_view name, SampleFormat sample_format, u16 channel_count, + size_t session_id, u32 handle, u64 applet_resource_user_id, + Sink::StreamType type); + + /** + * Finalize this device session. + */ + void Finalize(); + + /** + * Append audio buffers to this device session to be played back. + * + * @param buffers - The buffers to play. + */ + void AppendBuffers(std::span buffers) const; + + /** + * (Audio In only) Pop samples from the backend, and write them back to this buffer's address. + * + * @param buffer - The buffer to write to. + */ + void ReleaseBuffer(AudioBuffer& buffer) const; + + /** + * Check if the buffer for the given tag has been consumed by the backend. + * + * @param tag - Unqiue tag of the buffer to check. + * @return true if the buffer has been consumed, otherwise false. + */ + bool IsBufferConsumed(u64 tag) const; + + /** + * Start this device session, starting the backend stream. + */ + void Start(); + + /** + * Stop this device session, stopping the backend stream. + */ + void Stop(); + + /** + * Set this device session's volume. + * + * @param volume - New volume for this session. + */ + void SetVolume(f32 volume) const; + + /** + * Get this device session's total played sample count. + * + * @return Samples played by this session. + */ + u64 GetPlayedSampleCount() const; + +private: + /// System + Core::System& system; + /// Output sink this device will use + Sink::Sink* sink; + /// The backend stream for this device session to send samples to + Sink::SinkStream* stream{}; + /// Name of this device session + std::string name{}; + /// Type of this device session (render/in/out) + Sink::StreamType type{}; + /// Sample format for this device. + SampleFormat sample_format{SampleFormat::PcmInt16}; + /// Channel count for this device session + u16 channel_count{}; + /// Session id of this device session + size_t session_id{}; + /// Handle of this device session + u32 handle{}; + /// Applet resource user id of this device session + u64 applet_resource_user_id{}; + /// Total number of samples played by this device session + u64 played_sample_count{}; +}; + +} // namespace AudioCore diff --git a/src/audio_core/in/audio_in.cpp b/src/audio_core/in/audio_in.cpp new file mode 100755 index 000000000..c946895d6 --- /dev/null +++ b/src/audio_core/in/audio_in.cpp @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_in_manager.h" +#include "audio_core/in/audio_in.h" +#include "core/hle/kernel/k_event.h" + +namespace AudioCore::AudioIn { + +In::In(Core::System& system_, Manager& manager_, Kernel::KEvent* event_, size_t session_id_) + : manager{manager_}, parent_mutex{manager.mutex}, event{event_}, system{system_, event, + session_id_} {} + +void In::Free() { + std::scoped_lock l{parent_mutex}; + manager.ReleaseSessionId(system.GetSessionId()); +} + +System& In::GetSystem() { + return system; +} + +AudioIn::State In::GetState() { + std::scoped_lock l{parent_mutex}; + return system.GetState(); +} + +Result In::StartSystem() { + std::scoped_lock l{parent_mutex}; + return system.Start(); +} + +void In::StartSession() { + std::scoped_lock l{parent_mutex}; + system.StartSession(); +} + +Result In::StopSystem() { + std::scoped_lock l{parent_mutex}; + return system.Stop(); +} + +Result In::AppendBuffer(const AudioInBuffer& buffer, u64 tag) { + std::scoped_lock l{parent_mutex}; + + if (system.AppendBuffer(buffer, tag)) { + return ResultSuccess; + } + return Service::Audio::ERR_BUFFER_COUNT_EXCEEDED; +} + +void In::ReleaseAndRegisterBuffers() { + std::scoped_lock l{parent_mutex}; + if (system.GetState() == State::Started) { + system.ReleaseBuffers(); + system.RegisterBuffers(); + } +} + +bool In::FlushAudioInBuffers() { + std::scoped_lock l{parent_mutex}; + return system.FlushAudioInBuffers(); +} + +u32 In::GetReleasedBuffers(std::span tags) { + std::scoped_lock l{parent_mutex}; + return system.GetReleasedBuffers(tags); +} + +Kernel::KReadableEvent& In::GetBufferEvent() { + std::scoped_lock l{parent_mutex}; + return event->GetReadableEvent(); +} + +f32 In::GetVolume() { + std::scoped_lock l{parent_mutex}; + return system.GetVolume(); +} + +void In::SetVolume(f32 volume) { + std::scoped_lock l{parent_mutex}; + system.SetVolume(volume); +} + +bool In::ContainsAudioBuffer(u64 tag) { + std::scoped_lock l{parent_mutex}; + return system.ContainsAudioBuffer(tag); +} + +u32 In::GetBufferCount() { + std::scoped_lock l{parent_mutex}; + return system.GetBufferCount(); +} + +u64 In::GetPlayedSampleCount() { + std::scoped_lock l{parent_mutex}; + return system.GetPlayedSampleCount(); +} + +} // namespace AudioCore::AudioIn diff --git a/src/audio_core/in/audio_in.h b/src/audio_core/in/audio_in.h new file mode 100755 index 000000000..6253891d5 --- /dev/null +++ b/src/audio_core/in/audio_in.h @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/in/audio_in_system.h" + +namespace Core { +class System; +} + +namespace Kernel { +class KEvent; +class KReadableEvent; +} // namespace Kernel + +namespace AudioCore::AudioIn { +class Manager; + +/** + * Interface between the service and audio in system. Mainly responsible for forwarding service + * calls to the system. + */ +class In { +public: + explicit In(Core::System& system, Manager& manager, Kernel::KEvent* event, size_t session_id); + + /** + * Free this audio in from the audio in manager. + */ + void Free(); + + /** + * Get this audio in's system. + */ + System& GetSystem(); + + /** + * Get the current state. + * + * @return Started or Stopped. + */ + AudioIn::State GetState(); + + /** + * Start the system + * + * @return Result code + */ + Result StartSystem(); + + /** + * Start the system's device session. + */ + void StartSession(); + + /** + * Stop the system. + * + * @return Result code + */ + Result StopSystem(); + + /** + * Append a new buffer to the system, the buffer event will be signalled when it is filled. + * + * @param buffer - The new buffer to append. + * @param tag - Unique tag for this buffer. + * @return Result code. + */ + Result AppendBuffer(const AudioInBuffer& buffer, u64 tag); + + /** + * Release all completed buffers, and register any appended. + */ + void ReleaseAndRegisterBuffers(); + + /** + * Flush all buffers. + */ + bool FlushAudioInBuffers(); + + /** + * Get all of the currently released buffers. + * + * @param tags - Output container for the buffer tags which were released. + * @return The number of buffers released. + */ + u32 GetReleasedBuffers(std::span tags); + + /** + * Get the buffer event for this audio in, this event will be signalled when a buffer is filled. + * + * @return The buffer event. + */ + Kernel::KReadableEvent& GetBufferEvent(); + + /** + * Get the current system volume. + * + * @return The current volume. + */ + f32 GetVolume(); + + /** + * Set the system volume. + * + * @param volume - The volume to set. + */ + void SetVolume(f32 volume); + + /** + * Check if a buffer is in the system. + * + * @param tag - The tag to search for. + * @return True if the buffer is in the system, otherwise false. + */ + bool ContainsAudioBuffer(u64 tag); + + /** + * Get the maximum number of buffers. + * + * @return The maximum number of buffers. + */ + u32 GetBufferCount(); + + /** + * Get the total played sample count for this audio in. + * + * @return The played sample count. + */ + u64 GetPlayedSampleCount(); + +private: + /// The AudioIn::Manager this audio in is registered with + Manager& manager; + /// Manager's mutex + std::recursive_mutex& parent_mutex; + /// Buffer event, signalled when buffers are ready to be released + Kernel::KEvent* event; + /// Main audio in system + System system; +}; + +} // namespace AudioCore::AudioIn diff --git a/src/audio_core/in/audio_in_system.cpp b/src/audio_core/in/audio_in_system.cpp new file mode 100755 index 000000000..ec5d37ed4 --- /dev/null +++ b/src/audio_core/in/audio_in_system.cpp @@ -0,0 +1,213 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include "audio_core/audio_event.h" +#include "audio_core/audio_manager.h" +#include "audio_core/in/audio_in_system.h" +#include "common/logging/log.h" +#include "core/core.h" +#include "core/core_timing.h" +#include "core/hle/kernel/k_event.h" + +namespace AudioCore::AudioIn { + +System::System(Core::System& system_, Kernel::KEvent* event_, const size_t session_id_) + : system{system_}, buffer_event{event_}, + session_id{session_id_}, session{std::make_unique(system_)} {} + +System::~System() { + Finalize(); +} + +void System::Finalize() { + Stop(); + session->Finalize(); + buffer_event->GetWritableEvent().Signal(); +} + +void System::StartSession() { + session->Start(); +} + +size_t System::GetSessionId() const { + return session_id; +} + +std::string_view System::GetDefaultDeviceName() { + return "BuiltInHeadset"; +} + +std::string_view System::GetDefaultUacDeviceName() { + return "Uac"; +} + +Result System::IsConfigValid(const std::string_view device_name, + const AudioInParameter& in_params) { + if ((device_name.size() > 0) && + (device_name != GetDefaultDeviceName() && device_name != GetDefaultUacDeviceName())) { + return Service::Audio::ERR_INVALID_DEVICE_NAME; + } + + if (in_params.sample_rate != TargetSampleRate && in_params.sample_rate > 0) { + return Service::Audio::ERR_INVALID_SAMPLE_RATE; + } + + return ResultSuccess; +} + +Result System::Initialize(std::string& device_name, const AudioInParameter& in_params, + const u32 handle_, const u64 applet_resource_user_id_) { + auto result{IsConfigValid(device_name, in_params)}; + if (result.IsError()) { + return result; + } + + handle = handle_; + applet_resource_user_id = applet_resource_user_id_; + if (device_name.empty() || device_name[0] == '\0') { + name = std::string(GetDefaultDeviceName()); + } else { + name = std::move(device_name); + } + + sample_rate = TargetSampleRate; + sample_format = SampleFormat::PcmInt16; + channel_count = in_params.channel_count <= 2 ? 2 : 6; + volume = 1.0f; + is_uac = name == "Uac"; + return ResultSuccess; +} + +Result System::Start() { + if (state != State::Stopped) { + return Service::Audio::ERR_OPERATION_FAILED; + } + + session->Initialize(name, sample_format, channel_count, session_id, handle, + applet_resource_user_id, Sink::StreamType::In); + session->SetVolume(volume); + session->Start(); + state = State::Started; + + std::vector buffers_to_flush{}; + buffers.RegisterBuffers(buffers_to_flush); + session->AppendBuffers(buffers_to_flush); + + return ResultSuccess; +} + +Result System::Stop() { + if (state == State::Started) { + session->Stop(); + session->SetVolume(0.0f); + state = State::Stopped; + } + + return ResultSuccess; +} + +bool System::AppendBuffer(const AudioInBuffer& buffer, const u64 tag) { + if (buffers.GetTotalBufferCount() == BufferCount) { + return false; + } + + AudioBuffer new_buffer{ + .played_timestamp = 0, .samples = buffer.samples, .tag = tag, .size = buffer.size}; + + buffers.AppendBuffer(new_buffer); + RegisterBuffers(); + + return true; +} + +void System::RegisterBuffers() { + if (state == State::Started) { + std::vector registered_buffers{}; + buffers.RegisterBuffers(registered_buffers); + session->AppendBuffers(registered_buffers); + } +} + +void System::ReleaseBuffers() { + bool signal{buffers.ReleaseBuffers(system.CoreTiming(), *session)}; + + if (signal) { + // Signal if any buffer was released, or if none are registered, we need more. + buffer_event->GetWritableEvent().Signal(); + } +} + +u32 System::GetReleasedBuffers(std::span tags) { + return buffers.GetReleasedBuffers(tags); +} + +bool System::FlushAudioInBuffers() { + if (state != State::Started) { + return false; + } + + u32 buffers_released{}; + buffers.FlushBuffers(buffers_released); + + if (buffers_released > 0) { + buffer_event->GetWritableEvent().Signal(); + } + return true; +} + +u16 System::GetChannelCount() const { + return channel_count; +} + +u32 System::GetSampleRate() const { + return sample_rate; +} + +SampleFormat System::GetSampleFormat() const { + return sample_format; +} + +State System::GetState() { + switch (state) { + case State::Started: + case State::Stopped: + return state; + default: + LOG_ERROR(Service_Audio, "AudioIn invalid state!"); + state = State::Stopped; + break; + } + return state; +} + +std::string System::GetName() const { + return name; +} + +f32 System::GetVolume() const { + return volume; +} + +void System::SetVolume(const f32 volume_) { + volume = volume_; + session->SetVolume(volume_); +} + +bool System::ContainsAudioBuffer(const u64 tag) { + return buffers.ContainsBuffer(tag); +} + +u32 System::GetBufferCount() { + return buffers.GetAppendedRegisteredCount(); +} + +u64 System::GetPlayedSampleCount() const { + return session->GetPlayedSampleCount(); +} + +bool System::IsUac() const { + return is_uac; +} + +} // namespace AudioCore::AudioIn diff --git a/src/audio_core/in/audio_in_system.h b/src/audio_core/in/audio_in_system.h new file mode 100755 index 000000000..165e35d83 --- /dev/null +++ b/src/audio_core/in/audio_in_system.h @@ -0,0 +1,275 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "audio_core/common/common.h" +#include "audio_core/device/audio_buffers.h" +#include "audio_core/device/device_session.h" +#include "core/hle/service/audio/errors.h" + +namespace Core { +class System; +} + +namespace Kernel { +class KEvent; +} + +namespace AudioCore::AudioIn { + +constexpr SessionTypes SessionType = SessionTypes::AudioIn; + +struct AudioInParameter { + /* 0x0 */ s32_le sample_rate; + /* 0x4 */ u16_le channel_count; + /* 0x6 */ u16_le reserved; +}; +static_assert(sizeof(AudioInParameter) == 0x8, "AudioInParameter is an invalid size"); + +struct AudioInParameterInternal { + /* 0x0 */ u32_le sample_rate; + /* 0x4 */ u32_le channel_count; + /* 0x8 */ u32_le sample_format; + /* 0xC */ u32_le state; +}; +static_assert(sizeof(AudioInParameterInternal) == 0x10, + "AudioInParameterInternal is an invalid size"); + +struct AudioInBuffer { + /* 0x00 */ AudioInBuffer* next; + /* 0x08 */ VAddr samples; + /* 0x10 */ u64 capacity; + /* 0x18 */ u64 size; + /* 0x20 */ u64 offset; +}; +static_assert(sizeof(AudioInBuffer) == 0x28, "AudioInBuffer is an invalid size"); + +enum class State { + Started, + Stopped, +}; + +/** + * Controls and drives audio input. + */ +class System { +public: + explicit System(Core::System& system, Kernel::KEvent* event, size_t session_id); + ~System(); + + /** + * Get the default audio input device name. + * + * @return The default audio input device name. + */ + std::string_view GetDefaultDeviceName(); + + /** + * Get the default USB audio input device name. + * This is preferred over non-USB as some games refuse to work with the BuiltInHeadset + * (e.g Let's Sing). + * + * @return The default USB audio input device name. + */ + std::string_view GetDefaultUacDeviceName(); + + /** + * Is the given initialize config valid? + * + * @param device_name - The name of the requested input device. + * @param in_params - Input parameters, see AudioInParameter. + * @return Result code. + */ + Result IsConfigValid(std::string_view device_name, const AudioInParameter& in_params); + + /** + * Initialize this system. + * + * @param device_name - The name of the requested input device. + * @param in_params - Input parameters, see AudioInParameter. + * @param handle - Unused. + * @param applet_resource_user_id - Unused. + * @return Result code. + */ + Result Initialize(std::string& device_name, const AudioInParameter& in_params, u32 handle, + u64 applet_resource_user_id); + + /** + * Start this system. + * + * @return Result code. + */ + Result Start(); + + /** + * Stop this system. + * + * @return Result code. + */ + Result Stop(); + + /** + * Finalize this system. + */ + void Finalize(); + + /** + * Start this system's device session. + */ + void StartSession(); + + /** + * Get this system's id. + */ + size_t GetSessionId() const; + + /** + * Append a new buffer to the device. + * + * @param buffer - New buffer to append. + * @param tag - Unique tag of the buffer. + * @return True if the buffer was appended, otherwise false. + */ + bool AppendBuffer(const AudioInBuffer& buffer, u64 tag); + + /** + * Register all appended buffers. + */ + void RegisterBuffers(); + + /** + * Release all registered buffers. + */ + void ReleaseBuffers(); + + /** + * Get all released buffers. + * + * @param tags - Container to be filled with the released buffers' tags. + * @return The number of buffers released. + */ + u32 GetReleasedBuffers(std::span tags); + + /** + * Flush all appended and registered buffers. + * + * @return True if buffers were successfully flushed, otherwise false. + */ + bool FlushAudioInBuffers(); + + /** + * Get this system's current channel count. + * + * @return The channel count. + */ + u16 GetChannelCount() const; + + /** + * Get this system's current sample rate. + * + * @return The sample rate. + */ + u32 GetSampleRate() const; + + /** + * Get this system's current sample format. + * + * @return The sample format. + */ + SampleFormat GetSampleFormat() const; + + /** + * Get this system's current state. + * + * @return The current state. + */ + State GetState(); + + /** + * Get this system's name. + * + * @return The system's name. + */ + std::string GetName() const; + + /** + * Get this system's current volume. + * + * @return The system's current volume. + */ + f32 GetVolume() const; + + /** + * Set this system's current volume. + * + * @param The new volume. + */ + void SetVolume(f32 volume); + + /** + * Does the system contain this buffer? + * + * @param tag - Unique tag to search for. + * @return True if the buffer is in the system, otherwise false. + */ + bool ContainsAudioBuffer(u64 tag); + + /** + * Get the maximum number of usable buffers (default 32). + * + * @return The number of buffers. + */ + u32 GetBufferCount(); + + /** + * Get the total number of samples played by this system. + * + * @return The number of samples. + */ + u64 GetPlayedSampleCount() const; + + /** + * Is this system using a USB device? + * + * @return True if using a USB device, otherwise false. + */ + bool IsUac() const; + +private: + /// Core system + Core::System& system; + /// (Unused) + u32 handle{}; + /// (Unused) + u64 applet_resource_user_id{}; + /// Buffer event, signalled when a buffer is ready + Kernel::KEvent* buffer_event; + /// Session id of this system + size_t session_id{}; + /// Device session for this system + std::unique_ptr session; + /// Audio buffers in use by this system + AudioBuffers buffers{BufferCount}; + /// Sample rate of this system + u32 sample_rate{}; + /// Sample format of this system + SampleFormat sample_format{SampleFormat::PcmInt16}; + /// Channel count of this system + u16 channel_count{}; + /// State of this system + std::atomic state{State::Stopped}; + /// Name of this system + std::string name{}; + /// Volume of this system + f32 volume{1.0f}; + /// Is this system's device USB? + bool is_uac{false}; +}; + +} // namespace AudioCore::AudioIn diff --git a/src/audio_core/out/audio_out.cpp b/src/audio_core/out/audio_out.cpp new file mode 100755 index 000000000..9a8d8a742 --- /dev/null +++ b/src/audio_core/out/audio_out.cpp @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_out_manager.h" +#include "audio_core/out/audio_out.h" +#include "core/hle/kernel/k_event.h" + +namespace AudioCore::AudioOut { + +Out::Out(Core::System& system_, Manager& manager_, Kernel::KEvent* event_, size_t session_id_) + : manager{manager_}, parent_mutex{manager.mutex}, event{event_}, system{system_, event, + session_id_} {} + +void Out::Free() { + std::scoped_lock l{parent_mutex}; + manager.ReleaseSessionId(system.GetSessionId()); +} + +System& Out::GetSystem() { + return system; +} + +AudioOut::State Out::GetState() { + std::scoped_lock l{parent_mutex}; + return system.GetState(); +} + +Result Out::StartSystem() { + std::scoped_lock l{parent_mutex}; + return system.Start(); +} + +void Out::StartSession() { + std::scoped_lock l{parent_mutex}; + system.StartSession(); +} + +Result Out::StopSystem() { + std::scoped_lock l{parent_mutex}; + return system.Stop(); +} + +Result Out::AppendBuffer(const AudioOutBuffer& buffer, const u64 tag) { + std::scoped_lock l{parent_mutex}; + + if (system.AppendBuffer(buffer, tag)) { + return ResultSuccess; + } + return Service::Audio::ERR_BUFFER_COUNT_EXCEEDED; +} + +void Out::ReleaseAndRegisterBuffers() { + std::scoped_lock l{parent_mutex}; + if (system.GetState() == State::Started) { + system.ReleaseBuffers(); + system.RegisterBuffers(); + } +} + +bool Out::FlushAudioOutBuffers() { + std::scoped_lock l{parent_mutex}; + return system.FlushAudioOutBuffers(); +} + +u32 Out::GetReleasedBuffers(std::span tags) { + std::scoped_lock l{parent_mutex}; + return system.GetReleasedBuffers(tags); +} + +Kernel::KReadableEvent& Out::GetBufferEvent() { + std::scoped_lock l{parent_mutex}; + return event->GetReadableEvent(); +} + +f32 Out::GetVolume() { + std::scoped_lock l{parent_mutex}; + return system.GetVolume(); +} + +void Out::SetVolume(const f32 volume) { + std::scoped_lock l{parent_mutex}; + system.SetVolume(volume); +} + +bool Out::ContainsAudioBuffer(const u64 tag) { + std::scoped_lock l{parent_mutex}; + return system.ContainsAudioBuffer(tag); +} + +u32 Out::GetBufferCount() { + std::scoped_lock l{parent_mutex}; + return system.GetBufferCount(); +} + +u64 Out::GetPlayedSampleCount() { + std::scoped_lock l{parent_mutex}; + return system.GetPlayedSampleCount(); +} + +} // namespace AudioCore::AudioOut diff --git a/src/audio_core/out/audio_out.h b/src/audio_core/out/audio_out.h new file mode 100755 index 000000000..f6b921645 --- /dev/null +++ b/src/audio_core/out/audio_out.h @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/out/audio_out_system.h" + +namespace Core { +class System; +} + +namespace Kernel { +class KEvent; +class KReadableEvent; +} // namespace Kernel + +namespace AudioCore::AudioOut { +class Manager; + +/** + * Interface between the service and audio out system. Mainly responsible for forwarding service + * calls to the system. + */ +class Out { +public: + explicit Out(Core::System& system, Manager& manager, Kernel::KEvent* event, size_t session_id); + + /** + * Free this audio out from the audio out manager. + */ + void Free(); + + /** + * Get this audio out's system. + */ + System& GetSystem(); + + /** + * Get the current state. + * + * @return Started or Stopped. + */ + AudioOut::State GetState(); + + /** + * Start the system + * + * @return Result code + */ + Result StartSystem(); + + /** + * Start the system's device session. + */ + void StartSession(); + + /** + * Stop the system. + * + * @return Result code + */ + Result StopSystem(); + + /** + * Append a new buffer to the system, the buffer event will be signalled when it is filled. + * + * @param buffer - The new buffer to append. + * @param tag - Unique tag for this buffer. + * @return Result code. + */ + Result AppendBuffer(const AudioOutBuffer& buffer, u64 tag); + + /** + * Release all completed buffers, and register any appended. + */ + void ReleaseAndRegisterBuffers(); + + /** + * Flush all buffers. + */ + bool FlushAudioOutBuffers(); + + /** + * Get all of the currently released buffers. + * + * @param tags - Output container for the buffer tags which were released. + * @return The number of buffers released. + */ + u32 GetReleasedBuffers(std::span tags); + + /** + * Get the buffer event for this audio out, this event will be signalled when a buffer is + * filled. + * @return The buffer event. + */ + Kernel::KReadableEvent& GetBufferEvent(); + + /** + * Get the current system volume. + * + * @return The current volume. + */ + f32 GetVolume(); + + /** + * Set the system volume. + * + * @param volume - The volume to set. + */ + void SetVolume(f32 volume); + + /** + * Check if a buffer is in the system. + * + * @param tag - The tag to search for. + * @return True if the buffer is in the system, otherwise false. + */ + bool ContainsAudioBuffer(u64 tag); + + /** + * Get the maximum number of buffers. + * + * @return The maximum number of buffers. + */ + u32 GetBufferCount(); + + /** + * Get the total played sample count for this audio out. + * + * @return The played sample count. + */ + u64 GetPlayedSampleCount(); + +private: + /// The AudioOut::Manager this audio out is registered with + Manager& manager; + /// Manager's mutex + std::recursive_mutex& parent_mutex; + /// Buffer event, signalled when buffers are ready to be released + Kernel::KEvent* event; + /// Main audio out system + System system; +}; + +} // namespace AudioCore::AudioOut diff --git a/src/audio_core/out/audio_out_system.cpp b/src/audio_core/out/audio_out_system.cpp new file mode 100755 index 000000000..35afddf06 --- /dev/null +++ b/src/audio_core/out/audio_out_system.cpp @@ -0,0 +1,207 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/audio_event.h" +#include "audio_core/audio_manager.h" +#include "audio_core/out/audio_out_system.h" +#include "common/logging/log.h" +#include "core/core.h" +#include "core/core_timing.h" +#include "core/hle/kernel/k_event.h" + +namespace AudioCore::AudioOut { + +System::System(Core::System& system_, Kernel::KEvent* event_, size_t session_id_) + : system{system_}, buffer_event{event_}, + session_id{session_id_}, session{std::make_unique(system_)} {} + +System::~System() { + Finalize(); +} + +void System::Finalize() { + Stop(); + session->Finalize(); + buffer_event->GetWritableEvent().Signal(); +} + +std::string_view System::GetDefaultOutputDeviceName() { + return "DeviceOut"; +} + +Result System::IsConfigValid(std::string_view device_name, const AudioOutParameter& in_params) { + if ((device_name.size() > 0) && (device_name != GetDefaultOutputDeviceName())) { + return Service::Audio::ERR_INVALID_DEVICE_NAME; + } + + if (in_params.sample_rate != TargetSampleRate && in_params.sample_rate > 0) { + return Service::Audio::ERR_INVALID_SAMPLE_RATE; + } + + if (in_params.channel_count == 0 || in_params.channel_count == 2 || + in_params.channel_count == 6) { + return ResultSuccess; + } + + return Service::Audio::ERR_INVALID_CHANNEL_COUNT; +} + +Result System::Initialize(std::string& device_name, const AudioOutParameter& in_params, u32 handle_, + u64& applet_resource_user_id_) { + auto result = IsConfigValid(device_name, in_params); + if (result.IsError()) { + return result; + } + + handle = handle_; + applet_resource_user_id = applet_resource_user_id_; + if (device_name.empty() || device_name[0] == '\0') { + name = std::string(GetDefaultOutputDeviceName()); + } else { + name = std::move(device_name); + } + + sample_rate = TargetSampleRate; + sample_format = SampleFormat::PcmInt16; + channel_count = in_params.channel_count <= 2 ? 2 : 6; + volume = 1.0f; + return ResultSuccess; +} + +void System::StartSession() { + session->Start(); +} + +size_t System::GetSessionId() const { + return session_id; +} + +Result System::Start() { + if (state != State::Stopped) { + return Service::Audio::ERR_OPERATION_FAILED; + } + + session->Initialize(name, sample_format, channel_count, session_id, handle, + applet_resource_user_id, Sink::StreamType::Out); + session->SetVolume(volume); + session->Start(); + state = State::Started; + + std::vector buffers_to_flush{}; + buffers.RegisterBuffers(buffers_to_flush); + session->AppendBuffers(buffers_to_flush); + + return ResultSuccess; +} + +Result System::Stop() { + if (state == State::Started) { + session->Stop(); + session->SetVolume(0.0f); + state = State::Stopped; + } + + return ResultSuccess; +} + +bool System::AppendBuffer(const AudioOutBuffer& buffer, u64 tag) { + if (buffers.GetTotalBufferCount() == BufferCount) { + return false; + } + + AudioBuffer new_buffer{ + .played_timestamp = 0, .samples = buffer.samples, .tag = tag, .size = buffer.size}; + + buffers.AppendBuffer(new_buffer); + RegisterBuffers(); + + return true; +} + +void System::RegisterBuffers() { + if (state == State::Started) { + std::vector registered_buffers{}; + buffers.RegisterBuffers(registered_buffers); + session->AppendBuffers(registered_buffers); + } +} + +void System::ReleaseBuffers() { + bool signal{buffers.ReleaseBuffers(system.CoreTiming(), *session)}; + if (signal) { + // Signal if any buffer was released, or if none are registered, we need more. + buffer_event->GetWritableEvent().Signal(); + } +} + +u32 System::GetReleasedBuffers(std::span tags) { + return buffers.GetReleasedBuffers(tags); +} + +bool System::FlushAudioOutBuffers() { + if (state != State::Started) { + return false; + } + + u32 buffers_released{}; + buffers.FlushBuffers(buffers_released); + + if (buffers_released > 0) { + buffer_event->GetWritableEvent().Signal(); + } + return true; +} + +u16 System::GetChannelCount() const { + return channel_count; +} + +u32 System::GetSampleRate() const { + return sample_rate; +} + +SampleFormat System::GetSampleFormat() const { + return sample_format; +} + +State System::GetState() { + switch (state) { + case State::Started: + case State::Stopped: + return state; + default: + LOG_ERROR(Service_Audio, "AudioOut invalid state!"); + state = State::Stopped; + break; + } + return state; +} + +std::string System::GetName() const { + return name; +} + +f32 System::GetVolume() const { + return volume; +} + +void System::SetVolume(const f32 volume_) { + volume = volume_; + session->SetVolume(volume_); +} + +bool System::ContainsAudioBuffer(const u64 tag) { + return buffers.ContainsBuffer(tag); +} + +u32 System::GetBufferCount() { + return buffers.GetAppendedRegisteredCount(); +} + +u64 System::GetPlayedSampleCount() const { + return session->GetPlayedSampleCount(); +} + +} // namespace AudioCore::AudioOut diff --git a/src/audio_core/out/audio_out_system.h b/src/audio_core/out/audio_out_system.h new file mode 100755 index 000000000..4ca2f3417 --- /dev/null +++ b/src/audio_core/out/audio_out_system.h @@ -0,0 +1,257 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "audio_core/common/common.h" +#include "audio_core/device/audio_buffers.h" +#include "audio_core/device/device_session.h" +#include "core/hle/service/audio/errors.h" + +namespace Core { +class System; +} + +namespace Kernel { +class KEvent; +} + +namespace AudioCore::AudioOut { + +constexpr SessionTypes SessionType = SessionTypes::AudioOut; + +struct AudioOutParameter { + /* 0x0 */ s32_le sample_rate; + /* 0x4 */ u16_le channel_count; + /* 0x6 */ u16_le reserved; +}; +static_assert(sizeof(AudioOutParameter) == 0x8, "AudioOutParameter is an invalid size"); + +struct AudioOutParameterInternal { + /* 0x0 */ u32_le sample_rate; + /* 0x4 */ u32_le channel_count; + /* 0x8 */ u32_le sample_format; + /* 0xC */ u32_le state; +}; +static_assert(sizeof(AudioOutParameterInternal) == 0x10, + "AudioOutParameterInternal is an invalid size"); + +struct AudioOutBuffer { + /* 0x00 */ AudioOutBuffer* next; + /* 0x08 */ VAddr samples; + /* 0x10 */ u64 capacity; + /* 0x18 */ u64 size; + /* 0x20 */ u64 offset; +}; +static_assert(sizeof(AudioOutBuffer) == 0x28, "AudioOutBuffer is an invalid size"); + +enum class State { + Started, + Stopped, +}; + +/** + * Controls and drives audio output. + */ +class System { +public: + explicit System(Core::System& system, Kernel::KEvent* event, size_t session_id); + ~System(); + + /** + * Get the default audio output device name. + * + * @return The default audio output device name. + */ + std::string_view GetDefaultOutputDeviceName(); + + /** + * Is the given initialize config valid? + * + * @param device_name - The name of the requested output device. + * @param in_params - Input parameters, see AudioOutParameter. + * @return Result code. + */ + Result IsConfigValid(std::string_view device_name, const AudioOutParameter& in_params); + + /** + * Initialize this system. + * + * @param device_name - The name of the requested output device. + * @param in_params - Input parameters, see AudioOutParameter. + * @param handle - Unused. + * @param applet_resource_user_id - Unused. + * @return Result code. + */ + Result Initialize(std::string& device_name, const AudioOutParameter& in_params, u32 handle, + u64& applet_resource_user_id); + + /** + * Start this system. + * + * @return Result code. + */ + Result Start(); + + /** + * Stop this system. + * + * @return Result code. + */ + Result Stop(); + + /** + * Finalize this system. + */ + void Finalize(); + + /** + * Start this system's device session. + */ + void StartSession(); + + /** + * Get this system's id. + */ + size_t GetSessionId() const; + + /** + * Append a new buffer to the device. + * + * @param buffer - New buffer to append. + * @param tag - Unique tag of the buffer. + * @return True if the buffer was appended, otherwise false. + */ + bool AppendBuffer(const AudioOutBuffer& buffer, u64 tag); + + /** + * Register all appended buffers. + */ + void RegisterBuffers(); + + /** + * Release all registered buffers. + */ + void ReleaseBuffers(); + + /** + * Get all released buffers. + * + * @param tags - Container to be filled with the released buffers' tags. + * @return The number of buffers released. + */ + u32 GetReleasedBuffers(std::span tags); + + /** + * Flush all appended and registered buffers. + * + * @return True if buffers were successfully flushed, otherwise false. + */ + bool FlushAudioOutBuffers(); + + /** + * Get this system's current channel count. + * + * @return The channel count. + */ + u16 GetChannelCount() const; + + /** + * Get this system's current sample rate. + * + * @return The sample rate. + */ + u32 GetSampleRate() const; + + /** + * Get this system's current sample format. + * + * @return The sample format. + */ + SampleFormat GetSampleFormat() const; + + /** + * Get this system's current state. + * + * @return The current state. + */ + State GetState(); + + /** + * Get this system's name. + * + * @return The system's name. + */ + std::string GetName() const; + + /** + * Get this system's current volume. + * + * @return The system's current volume. + */ + f32 GetVolume() const; + + /** + * Set this system's current volume. + * + * @param The new volume. + */ + void SetVolume(f32 volume); + + /** + * Does the system contain this buffer? + * + * @param tag - Unique tag to search for. + * @return True if the buffer is in the system, otherwise false. + */ + bool ContainsAudioBuffer(u64 tag); + + /** + * Get the maximum number of usable buffers (default 32). + * + * @return The number of buffers. + */ + u32 GetBufferCount(); + + /** + * Get the total number of samples played by this system. + * + * @return The number of samples. + */ + u64 GetPlayedSampleCount() const; + +private: + /// Core system + Core::System& system; + /// (Unused) + u32 handle{}; + /// (Unused) + u64 applet_resource_user_id{}; + /// Buffer event, signalled when a buffer is ready + Kernel::KEvent* buffer_event; + /// Session id of this system + size_t session_id{}; + /// Device session for this system + std::unique_ptr session; + /// Audio buffers in use by this system + AudioBuffers buffers{BufferCount}; + /// Sample rate of this system + u32 sample_rate{}; + /// Sample format of this system + SampleFormat sample_format{SampleFormat::PcmInt16}; + /// Channel count of this system + u16 channel_count{}; + /// State of this system + std::atomic state{State::Stopped}; + /// Name of this system + std::string name{}; + /// Volume of this system + f32 volume{1.0f}; +}; + +} // namespace AudioCore::AudioOut diff --git a/src/audio_core/renderer/adsp/adsp.cpp b/src/audio_core/renderer/adsp/adsp.cpp new file mode 100755 index 000000000..e05a22d86 --- /dev/null +++ b/src/audio_core/renderer/adsp/adsp.cpp @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/adsp.h" +#include "audio_core/renderer/adsp/command_buffer.h" +#include "audio_core/sink/sink.h" +#include "common/logging/log.h" +#include "core/core.h" +#include "core/core_timing.h" +#include "core/core_timing_util.h" +#include "core/memory.h" + +namespace AudioCore::AudioRenderer::ADSP { + +ADSP::ADSP(Core::System& system_, Sink::Sink& sink_) + : system{system_}, memory{system.Memory()}, sink{sink_} {} + +ADSP::~ADSP() { + ClearCommandBuffers(); +} + +State ADSP::GetState() const { + if (running) { + return State::Started; + } + return State::Stopped; +} + +AudioRenderer_Mailbox* ADSP::GetRenderMailbox() { + return &render_mailbox; +} + +void ADSP::ClearRemainCount(const u32 session_id) { + render_mailbox.ClearRemainCount(session_id); +} + +u64 ADSP::GetSignalledTick() const { + return render_mailbox.GetSignalledTick(); +} + +u64 ADSP::GetTimeTaken() const { + return render_mailbox.GetRenderTimeTaken(); +} + +u64 ADSP::GetRenderTimeTaken(const u32 session_id) { + return render_mailbox.GetCommandBuffer(session_id).render_time_taken; +} + +u32 ADSP::GetRemainCommandCount(const u32 session_id) const { + return render_mailbox.GetRemainCommandCount(session_id); +} + +void ADSP::SendCommandBuffer(const u32 session_id, CommandBuffer& command_buffer) { + render_mailbox.SetCommandBuffer(session_id, command_buffer); +} + +u64 ADSP::GetRenderingStartTick(const u32 session_id) { + return render_mailbox.GetSignalledTick() + + render_mailbox.GetCommandBuffer(session_id).render_time_taken; +} + +bool ADSP::Start() { + if (running) { + return running; + } + + running = true; + systems_active++; + audio_renderer = std::make_unique(system); + audio_renderer->Start(&render_mailbox); + render_mailbox.HostSendMessage(RenderMessage::AudioRenderer_InitializeOK); + if (render_mailbox.HostWaitMessage() != RenderMessage::AudioRenderer_InitializeOK) { + LOG_ERROR( + Service_Audio, + "Host Audio Renderer -- Failed to receive initialize message response from ADSP!"); + } + return running; +} + +void ADSP::Stop() { + systems_active--; + if (running && systems_active == 0) { + { + std::scoped_lock l{mailbox_lock}; + render_mailbox.HostSendMessage(RenderMessage::AudioRenderer_Shutdown); + if (render_mailbox.HostWaitMessage() != RenderMessage::AudioRenderer_Shutdown) { + LOG_ERROR(Service_Audio, "Host Audio Renderer -- Failed to receive shutdown " + "message response from ADSP!"); + } + } + audio_renderer->Stop(); + running = false; + } +} + +void ADSP::Signal() { + const auto signalled_tick{system.CoreTiming().GetClockTicks()}; + render_mailbox.SetSignalledTick(signalled_tick); + render_mailbox.HostSendMessage(RenderMessage::AudioRenderer_Render); +} + +void ADSP::Wait() { + std::scoped_lock l{mailbox_lock}; + auto response{render_mailbox.HostWaitMessage()}; + if (response != RenderMessage::AudioRenderer_RenderResponse) { + LOG_ERROR(Service_Audio, "Invalid ADSP response message, expected 0x{:02X}, got 0x{:02X}", + static_cast(RenderMessage::AudioRenderer_RenderResponse), + static_cast(response)); + } + + ClearCommandBuffers(); +} + +void ADSP::ClearCommandBuffers() { + render_mailbox.ClearCommandBuffers(); +} + +} // namespace AudioCore::AudioRenderer::ADSP diff --git a/src/audio_core/renderer/adsp/adsp.h b/src/audio_core/renderer/adsp/adsp.h new file mode 100755 index 000000000..4dfcef4a5 --- /dev/null +++ b/src/audio_core/renderer/adsp/adsp.h @@ -0,0 +1,173 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/adsp/audio_renderer.h" +#include "common/common_types.h" + +namespace Core { +namespace Memory { +class Memory; +} +class System; +} // namespace Core + +namespace AudioCore { +namespace Sink { +class Sink; +} + +namespace AudioRenderer::ADSP { +struct CommandBuffer; + +enum class State { + Started, + Stopped, +}; + +/** + * Represents the ADSP embedded within the audio sysmodule. + * This is a 32-bit Linux4Tegra kernel from nVidia, which is launched with the sysmodule on boot. + * + * The kernel will run apps you program for it, Nintendo have the following: + * + * Gmix - Responsible for mixing final audio and sending it out to hardware. This is last place all + * audio samples end up, and we skip it entirely, since we have very different backends and + * mixing is implicitly handled by the OS (but also due to lack of research/simplicity). + * + * AudioRenderer - Receives command lists generated by the audio render + * system, processes them, and sends the samples to Gmix. + * + * OpusDecoder - Contains libopus, and controls processing Opus audio and sends it to Gmix. + * Not much research done here, TODO if needed. + * + * We only implement the AudioRenderer for now. + * + * Communication for the apps is done through mailboxes, and some shared memory. + */ +class ADSP { +public: + explicit ADSP(Core::System& system, Sink::Sink& sink); + ~ADSP(); + + /** + * Start the ADSP. + * + * @return True if started or already running, otherwise false. + */ + bool Start(); + + /** + * Stop the ADSP. + * + * @return True if started or already running, otherwise false. + */ + void Stop(); + + /** + * Get the ADSP's state. + * + * @return Started or Stopped. + */ + State GetState() const; + + /** + * Get the AudioRenderer mailbox to communicate with it. + * + * @return The AudioRenderer mailbox. + */ + AudioRenderer_Mailbox* GetRenderMailbox(); + + /** + * Get the tick the ADSP was signalled. + * + * @return The tick the ADSP was signalled. + */ + u64 GetSignalledTick() const; + + /** + * Get the total time it took for the ADSP to run the last command lists (both command lists). + * + * @return The tick the ADSP was signalled. + */ + u64 GetTimeTaken() const; + + /** + * Get the last time a given command list took to run. + * + * @param session_id - The session id to check (0 or 1). + * @return The time it took. + */ + u64 GetRenderTimeTaken(u32 session_id); + + /** + * Clear the remaining command count for a given session. + * + * @param session_id - The session id to check (0 or 1). + */ + void ClearRemainCount(u32 session_id); + + /** + * Get the remaining number of commands left to process for a command list. + * + * @param session_id - The session id to check (0 or 1). + * @return The number of commands remaining. + */ + u32 GetRemainCommandCount(u32 session_id) const; + + /** + * Get the last tick a command list started processing. + * + * @param session_id - The session id to check (0 or 1). + * @return The last tick the given command list started. + */ + u64 GetRenderingStartTick(u32 session_id); + + /** + * Set a command buffer to be processed. + * + * @param session_id - The session id to check (0 or 1). + * @param command_buffer - The command buffer to process. + */ + void SendCommandBuffer(u32 session_id, CommandBuffer& command_buffer); + + /** + * Clear the command buffers (does not clear the time taken or the remaining command count) + */ + void ClearCommandBuffers(); + + /** + * Signal the AudioRenderer to begin processing. + */ + void Signal(); + + /** + * Wait for the AudioRenderer to finish processing. + */ + void Wait(); + +private: + /// Core system + Core::System& system; + /// Core memory + Core::Memory::Memory& memory; + /// Number of systems active, used to prevent accidental shutdowns + u8 systems_active{0}; + /// ADSP running state + std::atomic running{false}; + /// Output sink used by the ADSP + Sink::Sink& sink; + /// AudioRenderer app + std::unique_ptr audio_renderer{}; + /// Communication for the AudioRenderer + AudioRenderer_Mailbox render_mailbox{}; + /// Mailbox lock ffor the render mailbox + std::mutex mailbox_lock; +}; + +} // namespace AudioRenderer::ADSP +} // namespace AudioCore diff --git a/src/audio_core/renderer/adsp/audio_renderer.cpp b/src/audio_core/renderer/adsp/audio_renderer.cpp new file mode 100755 index 000000000..85112fcd7 --- /dev/null +++ b/src/audio_core/renderer/adsp/audio_renderer.cpp @@ -0,0 +1,230 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "audio_core/audio_core.h" +#include "audio_core/common/common.h" +#include "audio_core/renderer/adsp/audio_renderer.h" +#include "audio_core/sink/sink.h" +#include "common/logging/log.h" +#include "common/microprofile.h" +#include "common/thread.h" +#include "core/core.h" +#include "core/core_timing.h" +#include "core/core_timing_util.h" + +MICROPROFILE_DEFINE(Audio_Renderer, "Audio", "DSP", MP_RGB(60, 19, 97)); + +namespace AudioCore::AudioRenderer::ADSP { + +void AudioRenderer_Mailbox::HostSendMessage(RenderMessage message_) { + adsp_messages.enqueue(message_); + adsp_event.Set(); +} + +RenderMessage AudioRenderer_Mailbox::HostWaitMessage() { + host_event.Wait(); + RenderMessage msg{RenderMessage::Invalid}; + if (!host_messages.try_dequeue(msg)) { + LOG_ERROR(Service_Audio, "Failed to dequeue host message!"); + } + return msg; +} + +void AudioRenderer_Mailbox::ADSPSendMessage(const RenderMessage message_) { + host_messages.enqueue(message_); + host_event.Set(); +} + +RenderMessage AudioRenderer_Mailbox::ADSPWaitMessage() { + adsp_event.Wait(); + RenderMessage msg{RenderMessage::Invalid}; + if (!adsp_messages.try_dequeue(msg)) { + LOG_ERROR(Service_Audio, "Failed to dequeue ADSP message!"); + } + return msg; +} + +CommandBuffer& AudioRenderer_Mailbox::GetCommandBuffer(const s32 session_id) { + return command_buffers[session_id]; +} + +void AudioRenderer_Mailbox::SetCommandBuffer(const u32 session_id, CommandBuffer& buffer) { + command_buffers[session_id] = buffer; +} + +u64 AudioRenderer_Mailbox::GetRenderTimeTaken() const { + return command_buffers[0].render_time_taken + command_buffers[1].render_time_taken; +} + +u64 AudioRenderer_Mailbox::GetSignalledTick() const { + return signalled_tick; +} + +void AudioRenderer_Mailbox::SetSignalledTick(const u64 tick) { + signalled_tick = tick; +} + +void AudioRenderer_Mailbox::ClearRemainCount(const u32 session_id) { + command_buffers[session_id].remaining_command_count = 0; +} + +u32 AudioRenderer_Mailbox::GetRemainCommandCount(const u32 session_id) const { + return command_buffers[session_id].remaining_command_count; +} + +void AudioRenderer_Mailbox::ClearCommandBuffers() { + command_buffers[0].buffer = 0; + command_buffers[0].size = 0; + command_buffers[0].reset_buffers = false; + command_buffers[1].buffer = 0; + command_buffers[1].size = 0; + command_buffers[1].reset_buffers = false; +} + +AudioRenderer::AudioRenderer(Core::System& system_) + : system{system_}, sink{system.AudioCore().GetOutputSink()} { + CreateSinkStreams(); +} + +AudioRenderer::~AudioRenderer() { + Stop(); + for (auto& stream : streams) { + if (stream) { + sink.CloseStream(stream); + } + stream = nullptr; + } +} + +void AudioRenderer::Start(AudioRenderer_Mailbox* mailbox_) { + if (running) { + return; + } + + mailbox = mailbox_; + thread = std::thread(&AudioRenderer::ThreadFunc, this); + for (auto& stream : streams) { + stream->Start(); + } + running = true; +} + +void AudioRenderer::Stop() { + if (!running) { + return; + } + + for (auto& stream : streams) { + stream->Stop(); + } + thread.join(); + running = false; +} + +void AudioRenderer::CreateSinkStreams() { + u32 channels{sink.GetDeviceChannels()}; + for (u32 i = 0; i < MaxRendererSessions; i++) { + std::string name{fmt::format("ADSP_RenderStream-{}", i)}; + streams[i] = sink.AcquireSinkStream(system, channels, name, + ::AudioCore::Sink::StreamType::Render, &render_event); + streams[i]->SetSystemChannels(streams[i]->GetDeviceChannels()); + } +} + +void AudioRenderer::ThreadFunc() { + constexpr char name[]{"yuzu:AudioRenderer"}; + MicroProfileOnThreadCreate(name); + Common::SetCurrentThreadName(name); + Common::SetCurrentThreadPriority(Common::ThreadPriority::Critical); + if (mailbox->ADSPWaitMessage() != RenderMessage::AudioRenderer_InitializeOK) { + LOG_ERROR(Service_Audio, + "ADSP Audio Renderer -- Failed to receive initialize message from host!"); + return; + } + + mailbox->ADSPSendMessage(RenderMessage::AudioRenderer_InitializeOK); + + constexpr u64 max_process_time{2'304'000ULL}; + + while (true) { + auto message{mailbox->ADSPWaitMessage()}; + switch (message) { + case RenderMessage::AudioRenderer_Shutdown: + mailbox->ADSPSendMessage(RenderMessage::AudioRenderer_Shutdown); + return; + + case RenderMessage::AudioRenderer_Render: { + std::array buffers_reset{}; + std::array render_times_taken{}; + const auto start_time{system.CoreTiming().GetClockTicks()}; + + for (u32 index = 0; index < 2; index++) { + auto& command_buffer{mailbox->GetCommandBuffer(index)}; + auto& command_list_processor{command_list_processors[index]}; + + // Check this buffer is valid, as it may not be used. + if (command_buffer.buffer != 0) { + // If there are no remaining commands (from the previous list), + // this is a new command list, initalize it. + if (command_buffer.remaining_command_count == 0) { + command_list_processor.Initialize(system, command_buffer.buffer, + command_buffer.size, streams[index]); + } + + if (command_buffer.reset_buffers && !buffers_reset[index]) { + streams[index]->ClearQueue(); + buffers_reset[index] = true; + } + + u64 max_time{max_process_time}; + if (index == 1 && command_buffer.applet_resource_user_id == + mailbox->GetCommandBuffer(0).applet_resource_user_id) { + max_time = max_process_time - + Core::Timing::CyclesToNs(render_times_taken[0]).count(); + if (render_times_taken[0] > max_process_time) { + max_time = 0; + } + } + + max_time = std::min(command_buffer.time_limit, max_time); + command_list_processor.SetProcessTimeMax(max_time); + + // Process the command list + { + MICROPROFILE_SCOPE(Audio_Renderer); + render_times_taken[index] = + command_list_processor.Process(index) - start_time; + } + + // If the stream queue is building up too much, wait for a signal + // from the backend that a buffer was consumed. + // In practice this will wait longer than 1 buffer due to timing. + auto stream{command_list_processor.GetOutputSinkStream()}; + if (stream->GetQueueSize() >= 4) { + render_event.WaitFor(std::chrono::milliseconds(5)); + } + + const auto end_time{system.CoreTiming().GetClockTicks()}; + + command_buffer.remaining_command_count = + command_list_processor.GetRemainingCommandCount(); + command_buffer.render_time_taken = end_time - start_time; + } + } + + mailbox->ADSPSendMessage(RenderMessage::AudioRenderer_RenderResponse); + } break; + + default: + LOG_WARNING(Service_Audio, + "ADSP AudioRenderer received an invalid message, msg={:02X}!", + static_cast(message)); + break; + } + } +} + +} // namespace AudioCore::AudioRenderer::ADSP diff --git a/src/audio_core/renderer/adsp/audio_renderer.h b/src/audio_core/renderer/adsp/audio_renderer.h new file mode 100755 index 000000000..3b2abd437 --- /dev/null +++ b/src/audio_core/renderer/adsp/audio_renderer.h @@ -0,0 +1,202 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "audio_core/renderer/adsp/command_buffer.h" +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "common/common_types.h" +#include "common/reader_writer_queue.h" +#include "common/thread.h" + +namespace Core { +class System; +} + +namespace AudioCore { +namespace Sink { +class Sink; +} + +namespace AudioRenderer::ADSP { + +enum class RenderMessage { + /* 0x00 */ Invalid, + /* 0x01 */ AudioRenderer_MapUnmap_Map, + /* 0x02 */ AudioRenderer_MapUnmap_MapResponse, + /* 0x03 */ AudioRenderer_MapUnmap_Unmap, + /* 0x04 */ AudioRenderer_MapUnmap_UnmapResponse, + /* 0x05 */ AudioRenderer_MapUnmap_InvalidateCache, + /* 0x06 */ AudioRenderer_MapUnmap_InvalidateCacheResponse, + /* 0x07 */ AudioRenderer_MapUnmap_Shutdown, + /* 0x08 */ AudioRenderer_MapUnmap_ShutdownResponse, + /* 0x16 */ AudioRenderer_InitializeOK = 0x16, + /* 0x20 */ AudioRenderer_RenderResponse = 0x20, + /* 0x2A */ AudioRenderer_Render = 0x2A, + /* 0x34 */ AudioRenderer_Shutdown = 0x34, +}; + +/** + * A mailbox for the AudioRenderer, allowing communication between the host and the AudioRenderer + * running on the ADSP. + */ +class AudioRenderer_Mailbox { +public: + /** + * Send a message from the host to the AudioRenderer. + * + * @param message_ - The message to send to the AudioRenderer. + */ + void HostSendMessage(RenderMessage message); + + /** + * Host wait for a message from the AudioRenderer. + * + * @return The message returned from the AudioRenderer. + */ + RenderMessage HostWaitMessage(); + + /** + * Send a message from the AudioRenderer to the host. + * + * @param message_ - The message to send to the host. + */ + void ADSPSendMessage(RenderMessage message); + + /** + * AudioRenderer wait for a message from the host. + * + * @return The message returned from the AudioRenderer. + */ + RenderMessage ADSPWaitMessage(); + + /** + * Get the command buffer with the given session id (0 or 1). + * + * @param session_id - The session id to get (0 or 1). + * @return The command buffer. + */ + CommandBuffer& GetCommandBuffer(s32 session_id); + + /** + * Set the command buffer with the given session id (0 or 1). + * + * @param session_id - The session id to get (0 or 1). + * @param buffer - The command buffer to set. + */ + void SetCommandBuffer(u32 session_id, CommandBuffer& buffer); + + /** + * Get the total render time taken for the last command lists sent. + * + * @return Total render time taken for the last command lists. + */ + u64 GetRenderTimeTaken() const; + + /** + * Get the tick the AudioRenderer was signalled. + * + * @return The tick the AudioRenderer was signalled. + */ + u64 GetSignalledTick() const; + + /** + * Set the tick the AudioRenderer was signalled. + * + * @param tick - The tick the AudioRenderer was signalled. + */ + void SetSignalledTick(u64 tick); + + /** + * Clear the remaining command count. + * + * @param session_id - Index for which command list to clear (0 or 1). + */ + void ClearRemainCount(u32 session_id); + + /** + * Get the remaining command count for a given command list. + * + * @param session_id - Index for which command list to clear (0 or 1). + * @return The remaining command count. + */ + u32 GetRemainCommandCount(u32 session_id) const; + + /** + * Clear the command buffers (does not clear the time taken or the remaining command count). + */ + void ClearCommandBuffers(); + +private: + /// Host signalling event + Common::Event host_event{}; + /// AudioRenderer signalling event + Common::Event adsp_event{}; + /// Host message queue + + Common::ReaderWriterQueue host_messages{}; + /// AudioRenderer message queue + + Common::ReaderWriterQueue adsp_messages{}; + /// Command buffers + + std::array command_buffers{}; + /// Tick the AudioRnederer was signalled + u64 signalled_tick{}; +}; + +/** + * The AudioRenderer application running on the ADSP. + */ +class AudioRenderer { +public: + explicit AudioRenderer(Core::System& system); + ~AudioRenderer(); + + /** + * Start the AudioRenderer. + * + * @param The mailbox to use for this session. + */ + void Start(AudioRenderer_Mailbox* mailbox); + + /** + * Stop the AudioRenderer. + */ + void Stop(); + +private: + /** + * Main AudioRenderer thread, responsible for processing the command lists. + */ + void ThreadFunc(); + + /** + * Creates the streams which will receive the processed samples. + */ + void CreateSinkStreams(); + + /// Core system + Core::System& system; + /// Main thread + std::thread thread{}; + /// The current state + std::atomic running{}; + /// The active mailbox + AudioRenderer_Mailbox* mailbox{}; + /// The command lists to process + std::array command_list_processors{}; + /// The output sink the AudioRenderer will use + Sink::Sink& sink; + /// The streams which will receive the processed samples + std::array streams; + /// An event signalled from the backend when a buffer is consumed, used for timing. + Common::Event render_event{}; +}; + +} // namespace AudioRenderer::ADSP +} // namespace AudioCore diff --git a/src/audio_core/renderer/adsp/command_buffer.h b/src/audio_core/renderer/adsp/command_buffer.h new file mode 100755 index 000000000..880b279d8 --- /dev/null +++ b/src/audio_core/renderer/adsp/command_buffer.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/common/common.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer::ADSP { + +struct CommandBuffer { + CpuAddr buffer; + u64 size; + u64 time_limit; + u32 remaining_command_count; + bool reset_buffers; + u64 applet_resource_user_id; + u64 render_time_taken; +}; + +} // namespace AudioCore::AudioRenderer::ADSP diff --git a/src/audio_core/renderer/adsp/command_list_processor.cpp b/src/audio_core/renderer/adsp/command_list_processor.cpp new file mode 100755 index 000000000..e3bf2d7ec --- /dev/null +++ b/src/audio_core/renderer/adsp/command_list_processor.cpp @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/command_list_header.h" +#include "audio_core/renderer/command/commands.h" +#include "common/settings.h" +#include "core/core.h" +#include "core/core_timing.h" +#include "core/core_timing_util.h" +#include "core/memory.h" + +namespace AudioCore::AudioRenderer::ADSP { + +void CommandListProcessor::Initialize(Core::System& system_, CpuAddr buffer, u64 size, + Sink::SinkStream* stream_) { + system = &system_; + memory = &system->Memory(); + stream = stream_; + header = reinterpret_cast(buffer); + commands = reinterpret_cast(buffer + sizeof(CommandListHeader)); + commands_buffer_size = size; + command_count = header->command_count; + sample_count = header->sample_count; + target_sample_rate = header->sample_rate; + mix_buffers = header->samples_buffer; + buffer_count = header->buffer_count; + processed_command_count = 0; +} + +void CommandListProcessor::SetProcessTimeMax(const u64 time) { + max_process_time = time; +} + +u32 CommandListProcessor::GetRemainingCommandCount() const { + return command_count - processed_command_count; +} + +void CommandListProcessor::SetBuffer(const CpuAddr buffer, const u64 size) { + commands = reinterpret_cast(buffer + sizeof(CommandListHeader)); + commands_buffer_size = size; +} + +Sink::SinkStream* CommandListProcessor::GetOutputSinkStream() const { + return stream; +} + +u64 CommandListProcessor::Process(u32 session_id) { + const auto start_time_{system->CoreTiming().GetClockTicks()}; + const auto command_base{CpuAddr(commands)}; + + if (processed_command_count > 0) { + current_processing_time += start_time_ - end_time; + } else { + start_time = start_time_; + current_processing_time = 0; + } + + std::string dump{fmt::format("\nSession {}\n", session_id)}; + + for (u32 index = 0; index < command_count; index++) { + auto& command{*reinterpret_cast(commands)}; + + if (command.magic != 0xCAFEBABE) { + LOG_ERROR(Service_Audio, "Command has invalid magic! Expected 0xCAFEBABE, got {:08X}", + command.magic); + return system->CoreTiming().GetClockTicks() - start_time_; + } + + auto current_offset{CpuAddr(commands) - command_base}; + + if (current_offset + command.size > commands_buffer_size) { + LOG_ERROR(Service_Audio, + "Command exceeded command buffer, buffer size {:08X}, command ends at {:08X}", + commands_buffer_size, + CpuAddr(commands) + command.size - sizeof(CommandListHeader)); + return system->CoreTiming().GetClockTicks() - start_time_; + } + + if (Settings::values.dump_audio_commands) { + command.Dump(*this, dump); + } + + if (!command.Verify(*this)) { + break; + } + + if (command.enabled) { + command.Process(*this); + } else { + dump += fmt::format("\tDisabled!\n"); + } + + processed_command_count++; + commands += command.size; + } + + if (Settings::values.dump_audio_commands && dump != last_dump) { + LOG_WARNING(Service_Audio, "{}", dump); + last_dump = dump; + } + + end_time = system->CoreTiming().GetClockTicks(); + return end_time - start_time_; +} + +} // namespace AudioCore::AudioRenderer::ADSP diff --git a/src/audio_core/renderer/adsp/command_list_processor.h b/src/audio_core/renderer/adsp/command_list_processor.h new file mode 100755 index 000000000..3f99173e3 --- /dev/null +++ b/src/audio_core/renderer/adsp/command_list_processor.h @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "common/common_types.h" + +namespace Core { +namespace Memory { +class Memory; +} +class System; +} // namespace Core + +namespace AudioCore { +namespace Sink { +class SinkStream; +} + +namespace AudioRenderer { +struct CommandListHeader; + +namespace ADSP { + +/** + * A processor for command lists given to the AudioRenderer. + */ +class CommandListProcessor { +public: + /** + * Initialize the processor. + * + * @param system_ - The core system. + * @param buffer - The command buffer to process. + * @param size - The size of the buffer. + * @param stream_ - The stream to be used for sending the samples. + */ + void Initialize(Core::System& system, CpuAddr buffer, u64 size, Sink::SinkStream* stream); + + /** + * Set the maximum processing time for this command list. + * + * @param time - The maximum process time. + */ + void SetProcessTimeMax(u64 time); + + /** + * Get the remaining command count for this list. + * + * @return The remaining command count. + */ + u32 GetRemainingCommandCount() const; + + /** + * Set the command buffer. + * + * @param buffer - The buffer to use. + * @param size - The size of the buffer. + */ + void SetBuffer(CpuAddr buffer, u64 size); + + /** + * Get the stream for this command list. + * + * @return The stream associated with this command list. + */ + Sink::SinkStream* GetOutputSinkStream() const; + + /** + * Process the command list. + * + * @param index - Index of the current command list. + * @return The time taken to process. + */ + u64 Process(u32 session_id); + + /// Core system + Core::System* system{}; + /// Core memory + Core::Memory::Memory* memory{}; + /// Stream for the processed samples + Sink::SinkStream* stream{}; + /// Header info for this command list + CommandListHeader* header{}; + /// The command buffer + u8* commands{}; + /// The command buffer size + u64 commands_buffer_size{}; + /// The maximum processing time alloted + u64 max_process_time{}; + /// The number of commands in the buffer + u32 command_count{}; + /// The target sample count for output + u32 sample_count{}; + /// The target sample rate for output + u32 target_sample_rate{}; + /// The mixing buffers used by the commands + std::span mix_buffers{}; + /// The number of mix buffers + u32 buffer_count{}; + /// The number of processed commands so far + u32 processed_command_count{}; + /// The processing start time of this list + u64 start_time{}; + /// The current processing time for this list + u64 current_processing_time{}; + /// The end processing time for this list + u64 end_time{}; + /// Last command list string generated, used for dumping audio commands to console + std::string last_dump{}; +}; + +} // namespace ADSP +} // namespace AudioRenderer +} // namespace AudioCore diff --git a/src/audio_core/renderer/audio_device.cpp b/src/audio_core/renderer/audio_device.cpp new file mode 100755 index 000000000..d5886e55e --- /dev/null +++ b/src/audio_core/renderer/audio_device.cpp @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_core.h" +#include "audio_core/common/feature_support.h" +#include "audio_core/renderer/audio_device.h" +#include "audio_core/sink/sink.h" +#include "core/core.h" + +namespace AudioCore::AudioRenderer { + +AudioDevice::AudioDevice(Core::System& system, const u64 applet_resource_user_id_, + const u32 revision) + : output_sink{system.AudioCore().GetOutputSink()}, + applet_resource_user_id{applet_resource_user_id_}, user_revision{revision} {} + +u32 AudioDevice::ListAudioDeviceName(std::vector& out_buffer, + const size_t max_count) { + std::span names{}; + + if (CheckFeatureSupported(SupportTags::AudioUsbDeviceOutput, user_revision)) { + names = usb_device_names; + } else { + names = device_names; + } + + u32 out_count{static_cast(std::min(max_count, names.size()))}; + for (u32 i = 0; i < out_count; i++) { + out_buffer.push_back(names[i]); + } + return out_count; +} + +u32 AudioDevice::ListAudioOutputDeviceName(std::vector& out_buffer, + const size_t max_count) { + u32 out_count{static_cast(std::min(max_count, output_device_names.size()))}; + + for (u32 i = 0; i < out_count; i++) { + out_buffer.push_back(output_device_names[i]); + } + return out_count; +} + +void AudioDevice::SetDeviceVolumes(const f32 volume) { + output_sink.SetDeviceVolume(volume); +} + +f32 AudioDevice::GetDeviceVolume([[maybe_unused]] std::string_view name) { + return output_sink.GetDeviceVolume(); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/audio_device.h b/src/audio_core/renderer/audio_device.h new file mode 100755 index 000000000..1f449f261 --- /dev/null +++ b/src/audio_core/renderer/audio_device.h @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/audio_render_manager.h" + +namespace Core { +class System; +} + +namespace AudioCore { +namespace Sink { +class Sink; +} + +namespace AudioRenderer { +/** + * An interface to an output audio device available to the Switch. + */ +class AudioDevice { +public: + struct AudioDeviceName { + std::array name; + + AudioDeviceName(const char* name_) { + std::strncpy(name.data(), name_, name.size()); + } + }; + + std::array usb_device_names{"AudioStereoJackOutput", + "AudioBuiltInSpeakerOutput", "AudioTvOutput", + "AudioUsbDeviceOutput"}; + std::array device_names{"AudioStereoJackOutput", + "AudioBuiltInSpeakerOutput", "AudioTvOutput"}; + std::array output_device_names{"AudioBuiltInSpeakerOutput", "AudioTvOutput", + "AudioExternalOutput"}; + + explicit AudioDevice(Core::System& system, u64 applet_resource_user_id, u32 revision); + + /** + * Get a list of the available output devices. + * + * @param out_buffer - Output buffer to write the available device names. + * @param max_count - Maximum number of devices to write (count of out_buffer). + * @return Number of device names written. + */ + u32 ListAudioDeviceName(std::vector& out_buffer, size_t max_count); + + /** + * Get a list of the available output devices. + * Different to above somehow... + * + * @param out_buffer - Output buffer to write the available device names. + * @param max_count - Maximum number of devices to write (count of out_buffer). + * @return Number of device names written. + */ + u32 ListAudioOutputDeviceName(std::vector& out_buffer, size_t max_count); + + /** + * Set the volume of all streams in the backend sink. + * + * @param volume - Volume to set. + */ + void SetDeviceVolumes(f32 volume); + + /** + * Get the volume for a given device name. + * Note: This is not fully implemented, we only assume 1 device for all streams. + * + * @param name - Name of the device to check. Unused. + * @return Volume of the device. + */ + f32 GetDeviceVolume(std::string_view name); + +private: + /// Backend output sink for the device + Sink::Sink& output_sink; + /// Resource id this device is used for + const u64 applet_resource_user_id; + /// User audio renderer revision + const u32 user_revision; +}; + +} // namespace AudioRenderer +} // namespace AudioCore diff --git a/src/audio_core/renderer/audio_renderer.cpp b/src/audio_core/renderer/audio_renderer.cpp new file mode 100755 index 000000000..51aa17599 --- /dev/null +++ b/src/audio_core/renderer/audio_renderer.cpp @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_render_manager.h" +#include "audio_core/common/audio_renderer_parameter.h" +#include "audio_core/renderer/audio_renderer.h" +#include "audio_core/renderer/system_manager.h" +#include "core/core.h" +#include "core/hle/kernel/k_transfer_memory.h" +#include "core/hle/service/audio/errors.h" + +namespace AudioCore::AudioRenderer { + +Renderer::Renderer(Core::System& system_, Manager& manager_, Kernel::KEvent* rendered_event) + : core{system_}, manager{manager_}, system{system_, rendered_event} {} + +Result Renderer::Initialize(const AudioRendererParameterInternal& params, + Kernel::KTransferMemory* transfer_memory, + const u64 transfer_memory_size, const u32 process_handle, + const u64 applet_resource_user_id, const s32 session_id) { + if (params.execution_mode == ExecutionMode::Auto) { + if (!manager.AddSystem(system)) { + LOG_ERROR(Service_Audio, + "Both Audio Render sessions are in use, cannot create any more"); + return Service::Audio::ERR_MAXIMUM_SESSIONS_REACHED; + } + system_registered = true; + } + + initialized = true; + system.Initialize(params, transfer_memory, transfer_memory_size, process_handle, + applet_resource_user_id, session_id); + + return ResultSuccess; +} + +void Renderer::Finalize() { + auto session_id{system.GetSessionId()}; + + system.Finalize(); + + if (system_registered) { + manager.RemoveSystem(system); + system_registered = false; + } + + manager.ReleaseSessionId(session_id); +} + +System& Renderer::GetSystem() { + return system; +} + +void Renderer::Start() { + system.Start(); +} + +void Renderer::Stop() { + system.Stop(); +} + +Result Renderer::RequestUpdate(std::span input, std::span performance, + std::span output) { + return system.Update(input, performance, output); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/audio_renderer.h b/src/audio_core/renderer/audio_renderer.h new file mode 100755 index 000000000..90c6f9727 --- /dev/null +++ b/src/audio_core/renderer/audio_renderer.h @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/system.h" +#include "core/hle/service/audio/errors.h" + +namespace Core { +class System; +} + +namespace Kernel { +class KTransferMemory; +} + +namespace AudioCore { +struct AudioRendererParameterInternal; + +namespace AudioRenderer { +class Manager; + +/** + * Audio Renderer, wraps the main audio system and is mainly responsible for handling service calls. + */ +class Renderer { +public: + explicit Renderer(Core::System& system, Manager& manager, Kernel::KEvent* rendered_event); + + /** + * Initialize the renderer. + * Registers the system with the AudioRenderer::Manager, allocates workbuffers and initializes + * everything to a default state. + * + * @param params - Input parameters to initialize the system with. + * @param transfer_memory - Game-supplied memory for all workbuffers. Unused. + * @param transfer_memory_size - Size of the transfer memory. Unused. + * @param process_handle - Process handle, also used for memory. Unused. + * @param applet_resource_user_id - Applet id for this renderer. Unused. + * @param session_id - Session id of this renderer. + * @return Result code. + */ + Result Initialize(const AudioRendererParameterInternal& params, + Kernel::KTransferMemory* transfer_memory, u64 transfer_memory_size, + u32 process_handle, u64 applet_resource_user_id, s32 session_id); + + /** + * Finalize the renderer for shutdown. + */ + void Finalize(); + + /** + * Get the renderer's system. + * + * @return Reference to the system. + */ + System& GetSystem(); + + /** + * Start the renderer. + */ + void Start(); + + /** + * Stop the renderer. + */ + void Stop(); + + /** + * Update the audio renderer with new information. + * Called via RequestUpdate from the AudRen:U service. + * + * @param input - Input buffer containing the new data. + * @param performance - Optional performance buffer for outputting performance metrics. + * @param output - Output data from the renderer. + * @return Result code. + */ + Result RequestUpdate(std::span input, std::span performance, + std::span output); + +private: + /// System core + Core::System& core; + /// Manager this renderer is registered with + Manager& manager; + /// Is the audio renderer initialized? + bool initialized{}; + /// Is the system registered with the manager? + bool system_registered{}; + /// Audio render system, main driver of audio rendering + System system; +}; + +} // namespace AudioRenderer +} // namespace AudioCore diff --git a/src/audio_core/renderer/behavior/behavior_info.cpp b/src/audio_core/renderer/behavior/behavior_info.cpp new file mode 100755 index 000000000..f4e06ab26 --- /dev/null +++ b/src/audio_core/renderer/behavior/behavior_info.cpp @@ -0,0 +1,174 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/common/feature_support.h" +#include "audio_core/renderer/behavior/behavior_info.h" + +namespace AudioCore::AudioRenderer { + +BehaviorInfo::BehaviorInfo() : process_revision{CurrentRevision} {} + +u32 BehaviorInfo::GetProcessRevisionNum() const { + return process_revision; +} + +u32 BehaviorInfo::GetProcessRevision() const { + return Common::MakeMagic('R', 'E', 'V', + static_cast(static_cast('0') + process_revision)); +} + +u32 BehaviorInfo::GetUserRevisionNum() const { + return user_revision; +} + +u32 BehaviorInfo::GetUserRevision() const { + return Common::MakeMagic('R', 'E', 'V', + static_cast(static_cast('0') + user_revision)); +} + +void BehaviorInfo::SetUserLibRevision(const u32 user_revision_) { + user_revision = GetRevisionNum(user_revision_); +} + +void BehaviorInfo::ClearError() { + error_count = 0; +} + +void BehaviorInfo::AppendError(ErrorInfo& error) { + LOG_ERROR(Service_Audio, "Error during RequestUpdate, reporting code {:04X} address {:08X}", + error.error_code.raw, error.address); + if (error_count < MaxErrors) { + errors[error_count++] = error; + } +} + +void BehaviorInfo::CopyErrorInfo(std::span out_errors, u32& out_count) { + auto error_count_{std::min(error_count, MaxErrors)}; + std::memset(out_errors.data(), 0, MaxErrors * sizeof(ErrorInfo)); + + for (size_t i = 0; i < error_count_; i++) { + out_errors[i] = errors[i]; + } + out_count = error_count_; +} + +void BehaviorInfo::UpdateFlags(const Flags flags_) { + flags = flags_; +} + +bool BehaviorInfo::IsMemoryForceMappingEnabled() const { + return flags.IsMemoryForceMappingEnabled; +} + +bool BehaviorInfo::IsAdpcmLoopContextBugFixed() const { + return CheckFeatureSupported(SupportTags::AdpcmLoopContextBugFix, user_revision); +} + +bool BehaviorInfo::IsSplitterSupported() const { + return CheckFeatureSupported(SupportTags::Splitter, user_revision); +} + +bool BehaviorInfo::IsSplitterBugFixed() const { + return CheckFeatureSupported(SupportTags::SplitterBugFix, user_revision); +} + +bool BehaviorInfo::IsEffectInfoVersion2Supported() const { + return CheckFeatureSupported(SupportTags::EffectInfoVer2, user_revision); +} + +bool BehaviorInfo::IsVariadicCommandBufferSizeSupported() const { + return CheckFeatureSupported(SupportTags::AudioRendererVariadicCommandBufferSize, + user_revision); +} + +bool BehaviorInfo::IsWaveBufferVer2Supported() const { + return CheckFeatureSupported(SupportTags::WaveBufferVer2, user_revision); +} + +bool BehaviorInfo::IsLongSizePreDelaySupported() const { + return CheckFeatureSupported(SupportTags::LongSizePreDelay, user_revision); +} + +bool BehaviorInfo::IsCommandProcessingTimeEstimatorVersion2Supported() const { + return CheckFeatureSupported(SupportTags::CommandProcessingTimeEstimatorVersion2, + user_revision); +} + +bool BehaviorInfo::IsCommandProcessingTimeEstimatorVersion3Supported() const { + return CheckFeatureSupported(SupportTags::CommandProcessingTimeEstimatorVersion3, + user_revision); +} + +bool BehaviorInfo::IsCommandProcessingTimeEstimatorVersion4Supported() const { + return CheckFeatureSupported(SupportTags::CommandProcessingTimeEstimatorVersion4, + user_revision); +} + +bool BehaviorInfo::IsAudioRenererProcessingTimeLimit70PercentSupported() const { + return CheckFeatureSupported(SupportTags::AudioRendererProcessingTimeLimit70Percent, + user_revision); +} + +bool BehaviorInfo::IsAudioRenererProcessingTimeLimit75PercentSupported() const { + return CheckFeatureSupported(SupportTags::AudioRendererProcessingTimeLimit75Percent, + user_revision); +} + +bool BehaviorInfo::IsAudioRenererProcessingTimeLimit80PercentSupported() const { + return CheckFeatureSupported(SupportTags::AudioRendererProcessingTimeLimit80Percent, + user_revision); +} + +bool BehaviorInfo::IsFlushVoiceWaveBuffersSupported() const { + return CheckFeatureSupported(SupportTags::FlushVoiceWaveBuffers, user_revision); +} + +bool BehaviorInfo::IsElapsedFrameCountSupported() const { + return CheckFeatureSupported(SupportTags::ElapsedFrameCount, user_revision); +} + +bool BehaviorInfo::IsPerformanceMetricsDataFormatVersion2Supported() const { + return CheckFeatureSupported(SupportTags::PerformanceMetricsDataFormatVersion2, user_revision); +} + +size_t BehaviorInfo::GetPerformanceMetricsDataFormat() const { + if (CheckFeatureSupported(SupportTags::PerformanceMetricsDataFormatVersion2, user_revision)) { + return 2; + } + return 1; +} + +bool BehaviorInfo::IsVoicePitchAndSrcSkippedSupported() const { + return CheckFeatureSupported(SupportTags::VoicePitchAndSrcSkipped, user_revision); +} + +bool BehaviorInfo::IsVoicePlayedSampleCountResetAtLoopPointSupported() const { + return CheckFeatureSupported(SupportTags::VoicePlayedSampleCountResetAtLoopPoint, + user_revision); +} + +bool BehaviorInfo::IsBiquadFilterEffectStateClaerBugFixed() const { + return CheckFeatureSupported(SupportTags::BiquadFilterEffectStateClearBugFix, user_revision); +} + +bool BehaviorInfo::IsVolumeMixParameterPrecisionQ23Supported() const { + return CheckFeatureSupported(SupportTags::VolumeMixParameterPrecisionQ23, user_revision); +} + +bool BehaviorInfo::UseBiquadFilterFloatProcessing() const { + return CheckFeatureSupported(SupportTags::BiquadFilterFloatProcessing, user_revision); +} + +bool BehaviorInfo::IsMixInParameterDirtyOnlyUpdateSupported() const { + return CheckFeatureSupported(SupportTags::MixInParameterDirtyOnlyUpdate, user_revision); +} + +bool BehaviorInfo::UseMultiTapBiquadFilterProcessing() const { + return CheckFeatureSupported(SupportTags::MultiTapBiquadFilterProcessing, user_revision); +} + +bool BehaviorInfo::IsDeviceApiVersion2Supported() const { + return CheckFeatureSupported(SupportTags::DeviceApiVersion2, user_revision); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/behavior/behavior_info.h b/src/audio_core/renderer/behavior/behavior_info.h new file mode 100755 index 000000000..9962ab9b9 --- /dev/null +++ b/src/audio_core/renderer/behavior/behavior_info.h @@ -0,0 +1,337 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/common/common.h" +#include "common/common_types.h" +#include "core/hle/service/audio/errors.h" + +namespace AudioCore::AudioRenderer { +/** + * Holds host and user revisions, checks whether render features can be enabled, and reports errors. + */ +class BehaviorInfo { + static constexpr u32 MaxErrors = 10; + +public: + struct ErrorInfo { + /* 0x00 */ Result error_code{0}; + /* 0x04 */ u32 unk_04; + /* 0x08 */ CpuAddr address; + }; + static_assert(sizeof(ErrorInfo) == 0x10, "BehaviorInfo::ErrorInfo has the wrong size!"); + + struct Flags { + u64 IsMemoryForceMappingEnabled : 1; + }; + + struct InParameter { + /* 0x00 */ u32 revision; + /* 0x08 */ Flags flags; + }; + static_assert(sizeof(InParameter) == 0x10, "BehaviorInfo::InParameter has the wrong size!"); + + struct OutStatus { + /* 0x00 */ std::array errors; + /* 0xA0 */ u32 error_count; + /* 0xA4 */ char unkA4[0xC]; + }; + static_assert(sizeof(OutStatus) == 0xB0, "BehaviorInfo::OutStatus has the wrong size!"); + + BehaviorInfo(); + + /** + * Get the host revision as a number. + * + * @return The host revision. + */ + u32 GetProcessRevisionNum() const; + + /** + * Get the host revision in chars, e.g REV8. + * Rev 10 and higher use the ascii characters above 9. + * E.g: + * Rev 10 = REV: + * Rev 11 = REV; + * + * @return The host revision. + */ + u32 GetProcessRevision() const; + + /** + * Get the user revision as a number. + * + * @return The user revision. + */ + u32 GetUserRevisionNum() const; + + /** + * Get the user revision in chars, e.g REV8. + * Rev 10 and higher use the ascii characters above 9. REV: REV; etc. + * + * @return The user revision. + */ + u32 GetUserRevision() const; + + /** + * Set the user revision. + * + * @param user_revision - The user's revision. + */ + void SetUserLibRevision(u32 user_revision); + + /** + * Clear the current error count. + */ + void ClearError(); + + /** + * Append an error to the error list. + * + * @param error - The new error. + */ + void AppendError(ErrorInfo& error); + + /** + * Copy errors to the given output container. + * + * @param out_errors - Output container to receive the errors. + * @param out_count - The number of errors written. + */ + void CopyErrorInfo(std::span out_errors, u32& out_count); + + /** + * Update the behaviour flags. + * + * @param flags - New flags to use. + */ + void UpdateFlags(Flags flags); + + /** + * Check if memory pools can be forcibly mapped. + * + * @return True if enabled, otherwise false. + */ + bool IsMemoryForceMappingEnabled() const; + + /** + * Check if the ADPCM context bug is fixed. + * The ADPCM context was not being sent to the AudioRenderer, leading to incorrect scaling being + * used. + * + * @return True if fixed, otherwise false. + */ + bool IsAdpcmLoopContextBugFixed() const; + + /** + * Check if the splitter is supported. + * + * @return True if supported, otherwise false. + */ + bool IsSplitterSupported() const; + + /** + * Check if the splitter bug is fixed. + * Update is given the wrong number of splitter destinations, leading to invalid data + * being processed. + * + * @return True if supported, otherwise false. + */ + bool IsSplitterBugFixed() const; + + /** + * Check if effects version 2 are supported. + * This gives support for returning effect states from the AudioRenderer, currently only used + * for Limiter statistics. + * + * @return True if supported, otherwise false. + */ + bool IsEffectInfoVersion2Supported() const; + + /** + * Check if a variadic command buffer is supported. + * As of Rev 5 with the added optional performance metric logging, the command + * buffer can be a variable size, so take that into account for calcualting its size. + * + * @return True if supported, otherwise false. + */ + bool IsVariadicCommandBufferSizeSupported() const; + + /** + * Check if wave buffers version 2 are supported. + * See WaveBufferVersion1 and WaveBufferVersion2. + * + * @return True if supported, otherwise false. + */ + bool IsWaveBufferVer2Supported() const; + + /** + * Check if long size pre delay is supported. + * This allows a longer initial delay time for the Reverb command. + * + * @return True if supported, otherwise false. + */ + bool IsLongSizePreDelaySupported() const; + + /** + * Check if the command time estimator version 2 is supported. + * + * @return True if supported, otherwise false. + */ + bool IsCommandProcessingTimeEstimatorVersion2Supported() const; + + /** + * Check if the command time estimator version 3 is supported. + * + * @return True if supported, otherwise false. + */ + bool IsCommandProcessingTimeEstimatorVersion3Supported() const; + + /** + * Check if the command time estimator version 4 is supported. + * + * @return True if supported, otherwise false. + */ + bool IsCommandProcessingTimeEstimatorVersion4Supported() const; + + /** + * Check if the AudioRenderer can use up to 70% of the allocated processing timeslice. + * Note: Name is correct, Nintendo have the typo here + * + * @return True if supported, otherwise false. + */ + bool IsAudioRenererProcessingTimeLimit70PercentSupported() const; + + /** + * Check if the AudioRenderer can use up to 75% of the allocated processing timeslice. + * Note: Name is correct, Nintendo have the typo here + * + * @return True if supported, otherwise false. + */ + bool IsAudioRenererProcessingTimeLimit75PercentSupported() const; + + /** + * Check if the AudioRenderer can use up to 80% of the allocated processing timeslice. + * Note: Name is correct, Nintendo have the typo here + * + * @return True if supported, otherwise false. + */ + bool IsAudioRenererProcessingTimeLimit80PercentSupported() const; + + /** + * Check if voice flushing is supported + * This allowws low-priority voices to be dropped if the AudioRenderer is running behind. + * + * @return True if supported, otherwise false. + */ + bool IsFlushVoiceWaveBuffersSupported() const; + + /** + * Check if counting the number of elapsed frames is supported. + * This adds extra output to RequestUpdate, returning the number of times the AudioRenderer + * processed a command list. + * + * @return True if supported, otherwise false. + */ + bool IsElapsedFrameCountSupported() const; + + /** + * Check if performance metrics version 2 are supported. + * This adds extra output to RequestUpdate, returning the number of times the AudioRenderer + * (Unused?). + * + * @return True if supported, otherwise false. + */ + bool IsPerformanceMetricsDataFormatVersion2Supported() const; + + /** + * Get the supported performance metrics version. + * Version 2 logs some extra fields in output, such as number of voices dropped, + * processing start time, if the AudioRenderer exceeded its time, etc. + * + * @return Version supported, either 1 or 2. + */ + size_t GetPerformanceMetricsDataFormat() const; + + /** + * Check if skipping voice pitch and sample rate conversion is supported. + * This speeds up the data source commands by skipping resampling if unwanted. + * See AudioCore::AudioRenderer::DecodeFromWaveBuffers + * + * @return True if supported, otherwise false. + */ + bool IsVoicePitchAndSrcSkippedSupported() const; + + /** + * Check if resetting played sample count at loop points is supported. + * This resets the number of samples played in a voice state when a loop point is reached. + * See AudioCore::AudioRenderer::DecodeFromWaveBuffers + * + * @return True if supported, otherwise false. + */ + bool IsVoicePlayedSampleCountResetAtLoopPointSupported() const; + + /** + * Check if the clear state bug for biquad filters is fixed. + * The biquad state was not marked as needing re-initialisation when the effect was updated, it + * was only initialized once with a new effect. + * Note: Name is correct, Nintendo have the typo here + * + * @return True if fixed, otherwise false. + */ + bool IsBiquadFilterEffectStateClaerBugFixed() const; + + /** + * Check if Q23 precision is supported for fixed point. + * + * @return True if supported, otherwise false. + */ + bool IsVolumeMixParameterPrecisionQ23Supported() const; + + /** + * Check if float processing for biuad filters is supported. + * + * @return True if supported, otherwise false. + */ + bool UseBiquadFilterFloatProcessing() const; + + /** + * Check if dirty-only mix updates are supported. + * This saves a lot of buffer size as mixes can be large and not change much. + * + * @return True if supported, otherwise false. + */ + bool IsMixInParameterDirtyOnlyUpdateSupported() const; + + /** + * Check if multi-tap biquad filters are supported. + * + * @return True if supported, otherwise false. + */ + bool UseMultiTapBiquadFilterProcessing() const; + + /** + * Check if device api version 2 is supported. + * In the SDK but not in any sysmodule? Not sure, left here for completeness anyway. + * + * @return True if supported, otherwise false. + */ + bool IsDeviceApiVersion2Supported() const; + + /// Host version + u32 process_revision; + /// User version + u32 user_revision{}; + /// Behaviour flags + Flags flags{}; + /// Errors generated and reported during Update + std::array errors{}; + /// Error count + u32 error_count{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/behavior/info_updater.cpp b/src/audio_core/renderer/behavior/info_updater.cpp new file mode 100755 index 000000000..06a37e1a6 --- /dev/null +++ b/src/audio_core/renderer/behavior/info_updater.cpp @@ -0,0 +1,539 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/common/feature_support.h" +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/behavior/info_updater.h" +#include "audio_core/renderer/effect/effect_context.h" +#include "audio_core/renderer/effect/effect_reset.h" +#include "audio_core/renderer/memory/memory_pool_info.h" +#include "audio_core/renderer/mix/mix_context.h" +#include "audio_core/renderer/performance/performance_manager.h" +#include "audio_core/renderer/sink/circular_buffer_sink_info.h" +#include "audio_core/renderer/sink/device_sink_info.h" +#include "audio_core/renderer/sink/sink_context.h" +#include "audio_core/renderer/splitter/splitter_context.h" +#include "audio_core/renderer/voice/voice_context.h" + +namespace AudioCore::AudioRenderer { + +InfoUpdater::InfoUpdater(std::span input_, std::span output_, + const u32 process_handle_, BehaviorInfo& behaviour_) + : input{input_.data() + sizeof(UpdateDataHeader)}, + input_origin{input_}, output{output_.data() + sizeof(UpdateDataHeader)}, + output_origin{output_}, in_header{reinterpret_cast( + input_origin.data())}, + out_header{reinterpret_cast(output_origin.data())}, + expected_input_size{input_.size()}, expected_output_size{output_.size()}, + process_handle{process_handle_}, behaviour{behaviour_} { + std::construct_at(out_header, behaviour.GetProcessRevision()); +} + +Result InfoUpdater::UpdateVoiceChannelResources(VoiceContext& voice_context) { + const auto voice_count{voice_context.GetCount()}; + std::span in_params{ + reinterpret_cast(input), voice_count}; + + for (u32 i = 0; i < voice_count; i++) { + auto& resource{voice_context.GetChannelResource(i)}; + resource.in_use = in_params[i].in_use; + if (in_params[i].in_use) { + resource.mix_volumes = in_params[i].mix_volumes; + } + } + + const auto consumed_input_size{voice_count * + static_cast(sizeof(VoiceChannelResource::InParameter))}; + if (consumed_input_size != in_header->voice_resources_size) { + LOG_ERROR(Service_Audio, + "Consumed an incorrect voice resource size, header size={}, consumed={}", + in_header->voice_resources_size, consumed_input_size); + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + input += consumed_input_size; + return ResultSuccess; +} + +Result InfoUpdater::UpdateVoices(VoiceContext& voice_context, + std::span memory_pools, + const u32 memory_pool_count) { + const PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count, + behaviour.IsMemoryForceMappingEnabled()); + const auto voice_count{voice_context.GetCount()}; + std::span in_params{ + reinterpret_cast(input), voice_count}; + std::span out_params{reinterpret_cast(output), + voice_count}; + + for (u32 i = 0; i < voice_count; i++) { + auto& voice_info{voice_context.GetInfo(i)}; + voice_info.in_use = false; + } + + u32 new_voice_count{0}; + + for (u32 i = 0; i < voice_count; i++) { + const auto& in_param{in_params[i]}; + std::array voice_states{}; + + if (!in_param.in_use) { + continue; + } + + auto& voice_info{voice_context.GetInfo(in_param.id)}; + + for (u32 channel = 0; channel < in_param.channel_count; channel++) { + voice_states[channel] = &voice_context.GetState(in_param.channel_resource_ids[channel]); + } + + if (in_param.is_new) { + voice_info.Initialize(); + + for (u32 channel = 0; channel < in_param.channel_count; channel++) { + std::memset(voice_states[channel], 0, sizeof(VoiceState)); + } + } + + BehaviorInfo::ErrorInfo update_error{}; + voice_info.UpdateParameters(update_error, in_param, pool_mapper, behaviour); + + if (!update_error.error_code.IsSuccess()) { + behaviour.AppendError(update_error); + } + + std::array, MaxWaveBuffers> wavebuffer_errors{}; + voice_info.UpdateWaveBuffers(wavebuffer_errors, MaxWaveBuffers * 2, in_param, voice_states, + pool_mapper, behaviour); + + for (auto& wavebuffer_error : wavebuffer_errors) { + for (auto& error : wavebuffer_error) { + if (error.error_code.IsError()) { + behaviour.AppendError(error); + } + } + } + + voice_info.WriteOutStatus(out_params[i], in_param, voice_states); + new_voice_count += in_param.channel_count; + } + + auto consumed_input_size{voice_count * static_cast(sizeof(VoiceInfo::InParameter))}; + auto consumed_output_size{voice_count * static_cast(sizeof(VoiceInfo::OutStatus))}; + if (consumed_input_size != in_header->voices_size) { + LOG_ERROR(Service_Audio, "Consumed an incorrect voices size, header size={}, consumed={}", + in_header->voices_size, consumed_input_size); + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + out_header->voices_size = consumed_output_size; + out_header->size += consumed_output_size; + input += consumed_input_size; + output += consumed_output_size; + + voice_context.SetActiveCount(new_voice_count); + + return ResultSuccess; +} + +Result InfoUpdater::UpdateEffects(EffectContext& effect_context, const bool renderer_active, + std::span memory_pools, + const u32 memory_pool_count) { + if (behaviour.IsEffectInfoVersion2Supported()) { + return UpdateEffectsVersion2(effect_context, renderer_active, memory_pools, + memory_pool_count); + } else { + return UpdateEffectsVersion1(effect_context, renderer_active, memory_pools, + memory_pool_count); + } +} + +Result InfoUpdater::UpdateEffectsVersion1(EffectContext& effect_context, const bool renderer_active, + std::span memory_pools, + const u32 memory_pool_count) { + PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count, + behaviour.IsMemoryForceMappingEnabled()); + + const auto effect_count{effect_context.GetCount()}; + + std::span in_params{ + reinterpret_cast(input), effect_count}; + std::span out_params{ + reinterpret_cast(output), effect_count}; + + for (u32 i = 0; i < effect_count; i++) { + auto effect_info{&effect_context.GetInfo(i)}; + if (effect_info->GetType() != in_params[i].type) { + effect_info->ForceUnmapBuffers(pool_mapper); + ResetEffect(effect_info, in_params[i].type); + } + + BehaviorInfo::ErrorInfo error_info{}; + effect_info->Update(error_info, in_params[i], pool_mapper); + if (error_info.error_code.IsError()) { + behaviour.AppendError(error_info); + } + + effect_info->StoreStatus(out_params[i], renderer_active); + } + + auto consumed_input_size{effect_count * + static_cast(sizeof(EffectInfoBase::InParameterVersion1))}; + auto consumed_output_size{effect_count * + static_cast(sizeof(EffectInfoBase::OutStatusVersion1))}; + if (consumed_input_size != in_header->effects_size) { + LOG_ERROR(Service_Audio, "Consumed an incorrect effects size, header size={}, consumed={}", + in_header->effects_size, consumed_input_size); + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + out_header->effects_size = consumed_output_size; + out_header->size += consumed_output_size; + input += consumed_input_size; + output += consumed_output_size; + + return ResultSuccess; +} + +Result InfoUpdater::UpdateEffectsVersion2(EffectContext& effect_context, const bool renderer_active, + std::span memory_pools, + const u32 memory_pool_count) { + PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count, + behaviour.IsMemoryForceMappingEnabled()); + + const auto effect_count{effect_context.GetCount()}; + + std::span in_params{ + reinterpret_cast(input), effect_count}; + std::span out_params{ + reinterpret_cast(output), effect_count}; + + for (u32 i = 0; i < effect_count; i++) { + auto effect_info{&effect_context.GetInfo(i)}; + if (effect_info->GetType() != in_params[i].type) { + effect_info->ForceUnmapBuffers(pool_mapper); + ResetEffect(effect_info, in_params[i].type); + } + + BehaviorInfo::ErrorInfo error_info{}; + effect_info->Update(error_info, in_params[i], pool_mapper); + + if (error_info.error_code.IsError()) { + behaviour.AppendError(error_info); + } + + effect_info->StoreStatus(out_params[i], renderer_active); + + if (in_params[i].is_new) { + effect_info->InitializeResultState(effect_context.GetDspSharedResultState(i)); + effect_info->InitializeResultState(effect_context.GetResultState(i)); + } + effect_info->UpdateResultState(out_params[i].result_state, + effect_context.GetResultState(i)); + } + + auto consumed_input_size{effect_count * + static_cast(sizeof(EffectInfoBase::InParameterVersion2))}; + auto consumed_output_size{effect_count * + static_cast(sizeof(EffectInfoBase::OutStatusVersion2))}; + if (consumed_input_size != in_header->effects_size) { + LOG_ERROR(Service_Audio, "Consumed an incorrect effects size, header size={}, consumed={}", + in_header->effects_size, consumed_input_size); + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + out_header->effects_size = consumed_output_size; + out_header->size += consumed_output_size; + input += consumed_input_size; + output += consumed_output_size; + + return ResultSuccess; +} + +Result InfoUpdater::UpdateMixes(MixContext& mix_context, const u32 mix_buffer_count, + EffectContext& effect_context, SplitterContext& splitter_context) { + s32 mix_count{0}; + u32 consumed_input_size{0}; + + if (behaviour.IsMixInParameterDirtyOnlyUpdateSupported()) { + auto in_dirty_params{reinterpret_cast(input)}; + mix_count = in_dirty_params->count; + input += sizeof(MixInfo::InDirtyParameter); + consumed_input_size = static_cast(sizeof(MixInfo::InDirtyParameter) + + mix_count * sizeof(MixInfo::InParameter)); + } else { + mix_count = mix_context.GetCount(); + consumed_input_size = static_cast(mix_count * sizeof(MixInfo::InParameter)); + } + + if (mix_buffer_count == 0) { + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + std::span in_params{ + reinterpret_cast(input), static_cast(mix_count)}; + + u32 total_buffer_count{0}; + for (s32 i = 0; i < mix_count; i++) { + const auto& params{in_params[i]}; + + if (params.in_use) { + total_buffer_count += params.buffer_count; + if (params.dest_mix_id > static_cast(mix_context.GetCount()) && + params.dest_mix_id != UnusedMixId && params.mix_id != FinalMixId) { + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + } + } + + if (total_buffer_count > mix_buffer_count) { + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + bool mix_dirty{false}; + for (s32 i = 0; i < mix_count; i++) { + const auto& params{in_params[i]}; + + s32 mix_id{i}; + if (behaviour.IsMixInParameterDirtyOnlyUpdateSupported()) { + mix_id = params.mix_id; + } + + auto mix_info{mix_context.GetInfo(mix_id)}; + if (mix_info->in_use != params.in_use) { + mix_info->in_use = params.in_use; + if (!params.in_use) { + mix_info->ClearEffectProcessingOrder(); + } + mix_dirty = true; + } + + if (params.in_use) { + mix_dirty |= mix_info->Update(mix_context.GetEdgeMatrix(), params, effect_context, + splitter_context, behaviour); + } + } + + if (mix_dirty) { + if (behaviour.IsSplitterSupported() && splitter_context.UsingSplitter()) { + if (!mix_context.TSortInfo(splitter_context)) { + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + } else { + mix_context.SortInfo(); + } + } + + if (consumed_input_size != in_header->mix_size) { + LOG_ERROR(Service_Audio, "Consumed an incorrect mixes size, header size={}, consumed={}", + in_header->mix_size, consumed_input_size); + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + input += mix_count * sizeof(MixInfo::InParameter); + + return ResultSuccess; +} + +Result InfoUpdater::UpdateSinks(SinkContext& sink_context, std::span memory_pools, + const u32 memory_pool_count) { + PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count, + behaviour.IsMemoryForceMappingEnabled()); + + std::span in_params{ + reinterpret_cast(input), memory_pool_count}; + std::span out_params{ + reinterpret_cast(output), memory_pool_count}; + + const auto sink_count{sink_context.GetCount()}; + + for (u32 i = 0; i < sink_count; i++) { + const auto& params{in_params[i]}; + auto sink_info{sink_context.GetInfo(i)}; + + if (sink_info->GetType() != params.type) { + sink_info->CleanUp(); + switch (params.type) { + case SinkInfoBase::Type::Invalid: + std::construct_at(reinterpret_cast(sink_info)); + break; + case SinkInfoBase::Type::DeviceSink: + std::construct_at(reinterpret_cast(sink_info)); + break; + case SinkInfoBase::Type::CircularBufferSink: + std::construct_at( + reinterpret_cast(sink_info)); + break; + default: + LOG_ERROR(Service_Audio, "Invalid sink type {}", static_cast(params.type)); + break; + } + } + + BehaviorInfo::ErrorInfo error_info{}; + sink_info->Update(error_info, out_params[i], params, pool_mapper); + + if (error_info.error_code.IsError()) { + behaviour.AppendError(error_info); + } + } + + const auto consumed_input_size{sink_count * + static_cast(sizeof(SinkInfoBase::InParameter))}; + const auto consumed_output_size{sink_count * static_cast(sizeof(SinkInfoBase::OutStatus))}; + if (consumed_input_size != in_header->sinks_size) { + LOG_ERROR(Service_Audio, "Consumed an incorrect sinks size, header size={}, consumed={}", + in_header->sinks_size, consumed_input_size); + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + input += consumed_input_size; + output += consumed_output_size; + out_header->sinks_size = consumed_output_size; + out_header->size += consumed_output_size; + + return ResultSuccess; +} + +Result InfoUpdater::UpdateMemoryPools(std::span memory_pools, + const u32 memory_pool_count) { + PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count, + behaviour.IsMemoryForceMappingEnabled()); + std::span in_params{ + reinterpret_cast(input), memory_pool_count}; + std::span out_params{ + reinterpret_cast(output), memory_pool_count}; + + for (size_t i = 0; i < memory_pool_count; i++) { + auto state{pool_mapper.Update(memory_pools[i], in_params[i], out_params[i])}; + if (state != MemoryPoolInfo::ResultState::Success && + state != MemoryPoolInfo::ResultState::BadParam && + state != MemoryPoolInfo::ResultState::MapFailed && + state != MemoryPoolInfo::ResultState::InUse) { + LOG_WARNING(Service_Audio, "Invalid ResultState from updating memory pools"); + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + } + + const auto consumed_input_size{memory_pool_count * + static_cast(sizeof(MemoryPoolInfo::InParameter))}; + const auto consumed_output_size{memory_pool_count * + static_cast(sizeof(MemoryPoolInfo::OutStatus))}; + if (consumed_input_size != in_header->memory_pool_size) { + LOG_ERROR(Service_Audio, + "Consumed an incorrect memory pool size, header size={}, consumed={}", + in_header->memory_pool_size, consumed_input_size); + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + input += consumed_input_size; + output += consumed_output_size; + out_header->memory_pool_size = consumed_output_size; + out_header->size += consumed_output_size; + return ResultSuccess; +} + +Result InfoUpdater::UpdatePerformanceBuffer(std::span performance_output, + const u64 performance_output_size, + PerformanceManager* performance_manager) { + auto in_params{reinterpret_cast(input)}; + auto out_params{reinterpret_cast(output)}; + + if (performance_manager != nullptr) { + out_params->history_size = + performance_manager->CopyHistories(performance_output.data(), performance_output_size); + performance_manager->SetDetailTarget(in_params->target_node_id); + } else { + out_params->history_size = 0; + } + + const auto consumed_input_size{static_cast(sizeof(PerformanceManager::InParameter))}; + const auto consumed_output_size{static_cast(sizeof(PerformanceManager::OutStatus))}; + if (consumed_input_size != in_header->performance_buffer_size) { + LOG_ERROR(Service_Audio, + "Consumed an incorrect performance size, header size={}, consumed={}", + in_header->performance_buffer_size, consumed_input_size); + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + input += consumed_input_size; + output += consumed_output_size; + out_header->performance_buffer_size = consumed_output_size; + out_header->size += consumed_output_size; + return ResultSuccess; +} + +Result InfoUpdater::UpdateBehaviorInfo(BehaviorInfo& behaviour_) { + const auto in_params{reinterpret_cast(input)}; + + if (!CheckValidRevision(in_params->revision)) { + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + if (in_params->revision != behaviour_.GetUserRevision()) { + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + behaviour_.ClearError(); + behaviour_.UpdateFlags(in_params->flags); + + if (in_header->behaviour_size != sizeof(BehaviorInfo::InParameter)) { + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + input += sizeof(BehaviorInfo::InParameter); + return ResultSuccess; +} + +Result InfoUpdater::UpdateErrorInfo(BehaviorInfo& behaviour_) { + auto out_params{reinterpret_cast(output)}; + behaviour_.CopyErrorInfo(out_params->errors, out_params->error_count); + + const auto consumed_output_size{static_cast(sizeof(BehaviorInfo::OutStatus))}; + + output += consumed_output_size; + out_header->behaviour_size = consumed_output_size; + out_header->size += consumed_output_size; + return ResultSuccess; +} + +Result InfoUpdater::UpdateSplitterInfo(SplitterContext& splitter_context) { + u32 consumed_size{0}; + if (!splitter_context.Update(input, consumed_size)) { + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + input += consumed_size; + + return ResultSuccess; +} + +Result InfoUpdater::UpdateRendererInfo(const u64 elapsed_frames) { + struct RenderInfo { + /* 0x00 */ u64 frames_elapsed; + /* 0x08 */ char unk08[0x8]; + }; + static_assert(sizeof(RenderInfo) == 0x10, "RenderInfo has the wrong size!"); + + auto out_params{reinterpret_cast(output)}; + out_params->frames_elapsed = elapsed_frames; + + const auto consumed_output_size{static_cast(sizeof(RenderInfo))}; + + output += consumed_output_size; + out_header->render_info_size = consumed_output_size; + out_header->size += consumed_output_size; + + return ResultSuccess; +} + +Result InfoUpdater::CheckConsumedSize() { + if (CpuAddr(input) - CpuAddr(input_origin.data()) != expected_input_size) { + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } else if (CpuAddr(output) - CpuAddr(output_origin.data()) != expected_output_size) { + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + return ResultSuccess; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/behavior/info_updater.h b/src/audio_core/renderer/behavior/info_updater.h new file mode 100755 index 000000000..f0b445d9c --- /dev/null +++ b/src/audio_core/renderer/behavior/info_updater.h @@ -0,0 +1,205 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "common/common_types.h" +#include "core/hle/service/audio/errors.h" + +namespace AudioCore::AudioRenderer { +class BehaviorInfo; +class VoiceContext; +class MixContext; +class SinkContext; +class SplitterContext; +class EffectContext; +class MemoryPoolInfo; +class PerformanceManager; + +class InfoUpdater { + struct UpdateDataHeader { + explicit UpdateDataHeader(u32 revision_) : revision{revision_} {} + + /* 0x00 */ u32 revision; + /* 0x04 */ u32 behaviour_size{}; + /* 0x08 */ u32 memory_pool_size{}; + /* 0x0C */ u32 voices_size{}; + /* 0x10 */ u32 voice_resources_size{}; + /* 0x14 */ u32 effects_size{}; + /* 0x18 */ u32 mix_size{}; + /* 0x1C */ u32 sinks_size{}; + /* 0x20 */ u32 performance_buffer_size{}; + /* 0x24 */ char unk24[4]; + /* 0x28 */ u32 render_info_size{}; + /* 0x2C */ char unk2C[0x10]; + /* 0x3C */ u32 size{sizeof(UpdateDataHeader)}; + }; + static_assert(sizeof(UpdateDataHeader) == 0x40, "UpdateDataHeader has the wrong size!"); + +public: + explicit InfoUpdater(std::span input, std::span output, u32 process_handle, + BehaviorInfo& behaviour); + + /** + * Update the voice channel resources. + * + * @param voice_context - Voice context to update. + * @return Result code. + */ + Result UpdateVoiceChannelResources(VoiceContext& voice_context); + + /** + * Update voices. + * + * @param voice_context - Voice context to update. + * @param memory_pools - Memory pools to use for these voices. + * @param memory_pool_count - Number of memory pools. + * @return Result code. + */ + Result UpdateVoices(VoiceContext& voice_context, std::span memory_pools, + u32 memory_pool_count); + + /** + * Update effects. + * + * @param effect_context - Effect context to update. + * @param renderer_active - Whether the AudioRenderer is active. + * @param memory_pools - Memory pools to use for these voices. + * @param memory_pool_count - Number of memory pools. + * @return Result code. + */ + Result UpdateEffects(EffectContext& effect_context, bool renderer_active, + std::span memory_pools, u32 memory_pool_count); + + /** + * Update mixes. + * + * @param mix_context - Mix context to update. + * @param mix_buffer_count - Number of mix buffers. + * @param effect_context - Effect context to update effort order. + * @param splitter_context - Splitter context for the mixes. + * @return Result code. + */ + Result UpdateMixes(MixContext& mix_context, u32 mix_buffer_count, EffectContext& effect_context, + SplitterContext& splitter_context); + + /** + * Update sinks. + * + * @param sink_context - Sink context to update. + * @param memory_pools - Memory pools to use for these voices. + * @param memory_pool_count - Number of memory pools. + * @return Result code. + */ + Result UpdateSinks(SinkContext& sink_context, std::span memory_pools, + u32 memory_pool_count); + + /** + * Update memory pools. + * + * @param memory_pools - Memory pools to use for these voices. + * @param memory_pool_count - Number of memory pools. + * @return Result code. + */ + Result UpdateMemoryPools(std::span memory_pools, u32 memory_pool_count); + + /** + * Update the performance buffer. + * + * @param output - Output buffer for performance metrics. + * @param output_size - Output buffer size. + * @param performance_manager - Performance manager.. + * @return Result code. + */ + Result UpdatePerformanceBuffer(std::span output, u64 output_size, + PerformanceManager* performance_manager); + + /** + * Update behaviour. + * + * @param behaviour - Behaviour to update. + * @return Result code. + */ + Result UpdateBehaviorInfo(BehaviorInfo& behaviour); + + /** + * Update errors. + * + * @param behaviour - Behaviour to update. + * @return Result code. + */ + Result UpdateErrorInfo(BehaviorInfo& behaviour); + + /** + * Update splitter. + * + * @param splitter_context - Splitter context to update. + * @return Result code. + */ + Result UpdateSplitterInfo(SplitterContext& splitter_context); + + /** + * Update renderer info. + * + * @param elapsed_frames - Number of elapsed frames. + * @return Result code. + */ + Result UpdateRendererInfo(u64 elapsed_frames); + + /** + * Check that the input.output sizes match their expected values. + * + * @return Result code. + */ + Result CheckConsumedSize(); + +private: + /** + * Update effects version 1. + * + * @param effect_context - Effect context to update. + * @param renderer_active - Is the AudioRenderer active? + * @param memory_pools - Memory pools to use for these voices. + * @param memory_pool_count - Number of memory pools. + * @return Result code. + */ + Result UpdateEffectsVersion1(EffectContext& effect_context, bool renderer_active, + std::span memory_pools, u32 memory_pool_count); + + /** + * Update effects version 2. + * + * @param effect_context - Effect context to update. + * @param renderer_active - Is the AudioRenderer active? + * @param memory_pools - Memory pools to use for these voices. + * @param memory_pool_count - Number of memory pools. + * @return Result code. + */ + Result UpdateEffectsVersion2(EffectContext& effect_context, bool renderer_active, + std::span memory_pools, u32 memory_pool_count); + + /// Input buffer + u8 const* input; + /// Input buffer start + std::span input_origin; + /// Output buffer start + u8* output; + /// Output buffer start + std::span output_origin; + /// Input header + const UpdateDataHeader* in_header; + /// Output header + UpdateDataHeader* out_header; + /// Expected input size, see CheckConsumedSize + u64 expected_input_size; + /// Expected output size, see CheckConsumedSize + u64 expected_output_size; + /// Unused + u32 process_handle; + /// Behaviour + BehaviorInfo& behaviour; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/command_buffer.cpp b/src/audio_core/renderer/command/command_buffer.cpp new file mode 100755 index 000000000..cd3da472b --- /dev/null +++ b/src/audio_core/renderer/command/command_buffer.cpp @@ -0,0 +1,678 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/command/command_buffer.h" +#include "audio_core/renderer/command/command_list_header.h" +#include "audio_core/renderer/command/command_processing_time_estimator.h" +#include "audio_core/renderer/effect/effect_biquad_filter_info.h" +#include "audio_core/renderer/effect/effect_delay_info.h" +#include "audio_core/renderer/effect/effect_reverb_info.h" +#include "audio_core/renderer/memory/memory_pool_info.h" +#include "audio_core/renderer/mix/mix_info.h" +#include "audio_core/renderer/sink/circular_buffer_sink_info.h" +#include "audio_core/renderer/sink/device_sink_info.h" +#include "audio_core/renderer/sink/sink_info_base.h" +#include "audio_core/renderer/voice/voice_info.h" +#include "audio_core/renderer/voice/voice_state.h" + +namespace AudioCore::AudioRenderer { + +template +T& CommandBuffer::GenerateStart(const s32 node_id) { + if (size + sizeof(T) >= command_list.size_bytes()) { + LOG_ERROR( + Service_Audio, + "Attempting to write commands beyond the end of allocated command buffer memory!"); + UNREACHABLE(); + } + + auto& cmd{*std::construct_at(reinterpret_cast(&command_list[size]))}; + + cmd.magic = CommandMagic; + cmd.enabled = true; + cmd.type = Id; + cmd.size = sizeof(T); + cmd.node_id = node_id; + + return cmd; +} + +template +void CommandBuffer::GenerateEnd(T& cmd) { + cmd.estimated_process_time = time_estimator->Estimate(cmd); + estimated_process_time += cmd.estimated_process_time; + size += sizeof(T); + count++; +} + +void CommandBuffer::GeneratePcmInt16Version1Command(const s32 node_id, + const MemoryPoolInfo& memory_pool_, + VoiceInfo& voice_info, + const VoiceState& voice_state, + const s16 buffer_count, const s8 channel) { + auto& cmd{ + GenerateStart( + node_id)}; + + cmd.src_quality = voice_info.src_quality; + cmd.output_index = buffer_count + channel; + cmd.flags = voice_info.flags & 3; + cmd.sample_rate = voice_info.sample_rate; + cmd.pitch = voice_info.pitch; + cmd.channel_index = channel; + cmd.channel_count = voice_info.channel_count; + + for (u32 i = 0; i < MaxWaveBuffers; i++) { + voice_info.wavebuffers[i].Copy(cmd.wave_buffers[i]); + } + + cmd.voice_state = memory_pool_.Translate(CpuAddr(&voice_state), sizeof(VoiceState)); + + GenerateEnd(cmd); +} + +void CommandBuffer::GeneratePcmInt16Version2Command(const s32 node_id, VoiceInfo& voice_info, + const VoiceState& voice_state, + const s16 buffer_count, const s8 channel) { + auto& cmd{ + GenerateStart( + node_id)}; + + cmd.src_quality = voice_info.src_quality; + cmd.output_index = buffer_count + channel; + cmd.flags = voice_info.flags & 3; + cmd.sample_rate = voice_info.sample_rate; + cmd.pitch = voice_info.pitch; + cmd.channel_index = channel; + cmd.channel_count = voice_info.channel_count; + + for (u32 i = 0; i < MaxWaveBuffers; i++) { + voice_info.wavebuffers[i].Copy(cmd.wave_buffers[i]); + } + + cmd.voice_state = memory_pool->Translate(CpuAddr(&voice_state), sizeof(VoiceState)); + + GenerateEnd(cmd); +} + +void CommandBuffer::GeneratePcmFloatVersion1Command(const s32 node_id, + const MemoryPoolInfo& memory_pool_, + VoiceInfo& voice_info, + const VoiceState& voice_state, + const s16 buffer_count, const s8 channel) { + auto& cmd{ + GenerateStart( + node_id)}; + + cmd.src_quality = voice_info.src_quality; + cmd.output_index = buffer_count + channel; + cmd.flags = voice_info.flags & 3; + cmd.sample_rate = voice_info.sample_rate; + cmd.pitch = voice_info.pitch; + cmd.channel_index = channel; + cmd.channel_count = voice_info.channel_count; + + for (u32 i = 0; i < MaxWaveBuffers; i++) { + voice_info.wavebuffers[i].Copy(cmd.wave_buffers[i]); + } + + cmd.voice_state = memory_pool_.Translate(CpuAddr(&voice_state), sizeof(VoiceState)); + + GenerateEnd(cmd); +} + +void CommandBuffer::GeneratePcmFloatVersion2Command(const s32 node_id, VoiceInfo& voice_info, + const VoiceState& voice_state, + const s16 buffer_count, const s8 channel) { + auto& cmd{ + GenerateStart( + node_id)}; + + cmd.src_quality = voice_info.src_quality; + cmd.output_index = buffer_count + channel; + cmd.flags = voice_info.flags & 3; + cmd.sample_rate = voice_info.sample_rate; + cmd.pitch = voice_info.pitch; + cmd.channel_index = channel; + cmd.channel_count = voice_info.channel_count; + + for (u32 i = 0; i < MaxWaveBuffers; i++) { + voice_info.wavebuffers[i].Copy(cmd.wave_buffers[i]); + } + + cmd.voice_state = memory_pool->Translate(CpuAddr(&voice_state), sizeof(VoiceState)); + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateAdpcmVersion1Command(const s32 node_id, + const MemoryPoolInfo& memory_pool_, + VoiceInfo& voice_info, + const VoiceState& voice_state, + const s16 buffer_count, const s8 channel) { + auto& cmd{ + GenerateStart(node_id)}; + + cmd.src_quality = voice_info.src_quality; + cmd.output_index = buffer_count + channel; + cmd.flags = voice_info.flags & 3; + cmd.sample_rate = voice_info.sample_rate; + cmd.pitch = voice_info.pitch; + + for (u32 i = 0; i < MaxWaveBuffers; i++) { + voice_info.wavebuffers[i].Copy(cmd.wave_buffers[i]); + } + + cmd.voice_state = memory_pool_.Translate(CpuAddr(&voice_state), sizeof(VoiceState)); + cmd.data_address = voice_info.data_address.GetReference(true); + cmd.data_size = voice_info.data_address.GetSize(); + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateAdpcmVersion2Command(const s32 node_id, VoiceInfo& voice_info, + const VoiceState& voice_state, + const s16 buffer_count, const s8 channel) { + auto& cmd{ + GenerateStart(node_id)}; + + cmd.src_quality = voice_info.src_quality; + cmd.output_index = buffer_count + channel; + cmd.flags = voice_info.flags & 3; + cmd.sample_rate = voice_info.sample_rate; + cmd.pitch = voice_info.pitch; + cmd.channel_index = channel; + cmd.channel_count = voice_info.channel_count; + + for (u32 i = 0; i < MaxWaveBuffers; i++) { + voice_info.wavebuffers[i].Copy(cmd.wave_buffers[i]); + } + + cmd.voice_state = memory_pool->Translate(CpuAddr(&voice_state), sizeof(VoiceState)); + cmd.data_address = voice_info.data_address.GetReference(true); + cmd.data_size = voice_info.data_address.GetSize(); + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateVolumeCommand(const s32 node_id, const s16 buffer_offset, + const s16 input_index, const f32 volume, + const u8 precision) { + auto& cmd{GenerateStart(node_id)}; + + cmd.precision = precision; + cmd.input_index = buffer_offset + input_index; + cmd.output_index = buffer_offset + input_index; + cmd.volume = volume; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateVolumeRampCommand(const s32 node_id, VoiceInfo& voice_info, + const s16 buffer_count, const u8 precision) { + auto& cmd{GenerateStart(node_id)}; + + cmd.input_index = buffer_count; + cmd.output_index = buffer_count; + cmd.prev_volume = voice_info.prev_volume; + cmd.volume = voice_info.volume; + cmd.precision = precision; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateBiquadFilterCommand(const s32 node_id, VoiceInfo& voice_info, + const VoiceState& voice_state, + const s16 buffer_count, const s8 channel, + const u32 biquad_index, + const bool use_float_processing) { + auto& cmd{GenerateStart(node_id)}; + + cmd.input = buffer_count + channel; + cmd.output = buffer_count + channel; + + cmd.biquad = voice_info.biquads[biquad_index]; + + cmd.state = memory_pool->Translate(CpuAddr(voice_state.biquad_states[biquad_index].data()), + MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState)); + + cmd.needs_init = !voice_info.biquad_initialized[biquad_index]; + cmd.use_float_processing = use_float_processing; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateBiquadFilterCommand(const s32 node_id, EffectInfoBase& effect_info, + const s16 buffer_offset, const s8 channel, + const bool needs_init, + const bool use_float_processing) { + auto& cmd{GenerateStart(node_id)}; + + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + const auto state{ + reinterpret_cast(effect_info.GetStateBuffer())}; + + cmd.input = buffer_offset + parameter.inputs[channel]; + cmd.output = buffer_offset + parameter.outputs[channel]; + + cmd.biquad.b = parameter.b; + cmd.biquad.a = parameter.a; + + cmd.state = memory_pool->Translate(CpuAddr(state), + MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState)); + + cmd.needs_init = needs_init; + cmd.use_float_processing = use_float_processing; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateMixCommand(const s32 node_id, const s16 input_index, + const s16 output_index, const s16 buffer_offset, + const f32 volume, const u8 precision) { + auto& cmd{GenerateStart(node_id)}; + + cmd.input_index = input_index; + cmd.output_index = output_index; + cmd.volume = volume; + cmd.precision = precision; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateMixRampCommand(const s32 node_id, + [[maybe_unused]] const s16 buffer_count, + const s16 input_index, const s16 output_index, + const f32 volume, const f32 prev_volume, + const CpuAddr prev_samples, const u8 precision) { + if (volume == 0.0f && prev_volume == 0.0f) { + return; + } + + auto& cmd{GenerateStart(node_id)}; + + cmd.input_index = input_index; + cmd.output_index = output_index; + cmd.prev_volume = prev_volume; + cmd.volume = volume; + cmd.previous_sample = prev_samples; + cmd.precision = precision; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateMixRampGroupedCommand(const s32 node_id, const s16 buffer_count, + const s16 input_index, s16 output_index, + std::span volumes, + std::span prev_volumes, + const CpuAddr prev_samples, const u8 precision) { + auto& cmd{GenerateStart(node_id)}; + + cmd.buffer_count = buffer_count; + + for (s32 i = 0; i < buffer_count; i++) { + cmd.inputs[i] = input_index; + cmd.outputs[i] = output_index++; + cmd.prev_volumes[i] = prev_volumes[i]; + cmd.volumes[i] = volumes[i]; + } + + cmd.previous_samples = prev_samples; + cmd.precision = precision; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateDepopPrepareCommand(const s32 node_id, const VoiceState& voice_state, + std::span buffer, const s16 buffer_count, + s16 buffer_offset, const bool was_playing) { + auto& cmd{GenerateStart(node_id)}; + + cmd.enabled = was_playing; + + for (u32 i = 0; i < MaxMixBuffers; i++) { + cmd.inputs[i] = buffer_offset++; + } + + cmd.previous_samples = memory_pool->Translate(CpuAddr(voice_state.previous_samples.data()), + MaxMixBuffers * sizeof(s32)); + cmd.buffer_count = buffer_count; + cmd.depop_buffer = memory_pool->Translate(CpuAddr(buffer.data()), buffer_count * sizeof(s32)); + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateDepopForMixBuffersCommand(const s32 node_id, const MixInfo& mix_info, + std::span depop_buffer) { + auto& cmd{GenerateStart(node_id)}; + + cmd.input = mix_info.buffer_offset; + cmd.count = mix_info.buffer_count; + cmd.decay = mix_info.sample_rate == TargetSampleRate ? 0.96218872f : 0.94369507f; + cmd.depop_buffer = + memory_pool->Translate(CpuAddr(depop_buffer.data()), mix_info.buffer_count * sizeof(s32)); + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateDelayCommand(const s32 node_id, EffectInfoBase& effect_info, + const s16 buffer_offset) { + auto& cmd{GenerateStart(node_id)}; + + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + const auto state{effect_info.GetStateBuffer()}; + + if (IsChannelCountValid(parameter.channel_count)) { + const auto state_buffer{memory_pool->Translate(CpuAddr(state), sizeof(DelayInfo::State))}; + if (state_buffer) { + for (s8 channel = 0; channel < parameter.channel_count; channel++) { + cmd.inputs[channel] = static_cast(buffer_offset + parameter.inputs[channel]); + cmd.outputs[channel] = static_cast(buffer_offset + parameter.outputs[channel]); + } + + cmd.parameter = parameter; + cmd.effect_enabled = effect_info.IsEnabled(); + cmd.state = state_buffer; + cmd.workbuffer = effect_info.GetWorkbuffer(-1); + } + } + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateUpsampleCommand(const s32 node_id, const s16 buffer_offset, + UpsamplerInfo& upsampler_info, const u32 input_count, + std::span inputs, const s16 buffer_count, + const u32 sample_count_, const u32 sample_rate_) { + auto& cmd{GenerateStart(node_id)}; + + cmd.samples_buffer = memory_pool->Translate(upsampler_info.samples_pos, + upsampler_info.sample_count * sizeof(s32)); + cmd.inputs = memory_pool->Translate(CpuAddr(upsampler_info.inputs.data()), MaxChannels); + cmd.buffer_count = buffer_count; + cmd.unk_20 = 0; + cmd.source_sample_count = sample_count_; + cmd.source_sample_rate = sample_rate_; + + upsampler_info.input_count = input_count; + for (u32 i = 0; i < input_count; i++) { + upsampler_info.inputs[i] = buffer_offset + inputs[i]; + } + + cmd.upsampler_info = memory_pool->Translate(CpuAddr(&upsampler_info), sizeof(UpsamplerInfo)); + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateDownMix6chTo2chCommand(const s32 node_id, std::span inputs, + const s16 buffer_offset, + std::span downmix_coeff) { + auto& cmd{GenerateStart(node_id)}; + + for (u32 i = 0; i < MaxChannels; i++) { + cmd.inputs[i] = buffer_offset + inputs[i]; + cmd.outputs[i] = buffer_offset + inputs[i]; + } + + for (u32 i = 0; i < 4; i++) { + cmd.down_mix_coeff[i] = downmix_coeff[i]; + } + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateAuxCommand(const s32 node_id, EffectInfoBase& effect_info, + const s16 input_index, const s16 output_index, + const s16 buffer_offset, const u32 update_count, + const u32 count_max, const u32 write_offset) { + auto& cmd{GenerateStart(node_id)}; + + if (effect_info.GetSendBuffer() != 0 && effect_info.GetReturnBuffer() != 0) { + cmd.input = buffer_offset + input_index; + cmd.output = buffer_offset + output_index; + cmd.send_buffer_info = effect_info.GetSendBufferInfo(); + cmd.send_buffer = effect_info.GetSendBuffer(); + cmd.return_buffer_info = effect_info.GetReturnBufferInfo(); + cmd.return_buffer = effect_info.GetReturnBuffer(); + cmd.count_max = count_max; + cmd.write_offset = write_offset; + cmd.update_count = update_count; + cmd.effect_enabled = effect_info.IsEnabled(); + } + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateDeviceSinkCommand(const s32 node_id, const s16 buffer_offset, + SinkInfoBase& sink_info, const u32 session_id, + std::span samples_buffer) { + auto& cmd{GenerateStart(node_id)}; + const auto& parameter{ + *reinterpret_cast(sink_info.GetParameter())}; + auto state{*reinterpret_cast(sink_info.GetState())}; + + cmd.session_id = session_id; + + if (state.upsampler_info != nullptr) { + const auto size_{state.upsampler_info->sample_count * parameter.input_count}; + const auto size_bytes{size_ * sizeof(s32)}; + const auto addr{memory_pool->Translate(state.upsampler_info->samples_pos, size_bytes)}; + cmd.sample_buffer = {reinterpret_cast(addr), + parameter.input_count * state.upsampler_info->sample_count}; + } else { + cmd.sample_buffer = samples_buffer; + } + + cmd.input_count = parameter.input_count; + for (u32 i = 0; i < parameter.input_count; i++) { + cmd.inputs[i] = buffer_offset + parameter.inputs[i]; + } + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateCircularBufferSinkCommand(const s32 node_id, SinkInfoBase& sink_info, + const s16 buffer_offset) { + auto& cmd{GenerateStart(node_id)}; + const auto& parameter{*reinterpret_cast( + sink_info.GetParameter())}; + auto state{ + *reinterpret_cast(sink_info.GetState())}; + + cmd.input_count = parameter.input_count; + for (u32 i = 0; i < parameter.input_count; i++) { + cmd.inputs[i] = buffer_offset + parameter.inputs[i]; + } + + cmd.address = state.address_info.GetReference(true); + cmd.size = parameter.size; + cmd.pos = state.current_pos; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateReverbCommand(const s32 node_id, EffectInfoBase& effect_info, + const s16 buffer_offset, + const bool long_size_pre_delay_supported) { + auto& cmd{GenerateStart(node_id)}; + + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + const auto state{effect_info.GetStateBuffer()}; + + if (IsChannelCountValid(parameter.channel_count)) { + const auto state_buffer{memory_pool->Translate(CpuAddr(state), sizeof(ReverbInfo::State))}; + if (state_buffer) { + for (s8 channel = 0; channel < parameter.channel_count; channel++) { + cmd.inputs[channel] = buffer_offset + parameter.inputs[channel]; + cmd.outputs[channel] = buffer_offset + parameter.outputs[channel]; + } + + cmd.parameter = parameter; + cmd.effect_enabled = effect_info.IsEnabled(); + cmd.state = state_buffer; + cmd.workbuffer = effect_info.GetWorkbuffer(-1); + cmd.long_size_pre_delay_supported = long_size_pre_delay_supported; + } + } + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateI3dl2ReverbCommand(const s32 node_id, EffectInfoBase& effect_info, + const s16 buffer_offset) { + auto& cmd{GenerateStart(node_id)}; + + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + const auto state{effect_info.GetStateBuffer()}; + + if (IsChannelCountValid(parameter.channel_count)) { + const auto state_buffer{ + memory_pool->Translate(CpuAddr(state), sizeof(I3dl2ReverbInfo::State))}; + if (state_buffer) { + for (s8 channel = 0; channel < parameter.channel_count; channel++) { + cmd.inputs[channel] = buffer_offset + parameter.inputs[channel]; + cmd.outputs[channel] = buffer_offset + parameter.outputs[channel]; + } + + cmd.parameter = parameter; + cmd.effect_enabled = effect_info.IsEnabled(); + cmd.state = state_buffer; + cmd.workbuffer = effect_info.GetWorkbuffer(-1); + } + } + + GenerateEnd(cmd); +} + +void CommandBuffer::GeneratePerformanceCommand(const s32 node_id, const PerformanceState state, + const PerformanceEntryAddresses& entry_addresses) { + auto& cmd{GenerateStart(node_id)}; + + cmd.state = state; + cmd.entry_address = entry_addresses; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateClearMixCommand(const s32 node_id) { + auto& cmd{GenerateStart(node_id)}; + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateCopyMixBufferCommand(const s32 node_id, EffectInfoBase& effect_info, + const s16 buffer_offset, const s8 channel) { + auto& cmd{GenerateStart(node_id)}; + + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + cmd.input_index = buffer_offset + parameter.inputs[channel]; + cmd.output_index = buffer_offset + parameter.outputs[channel]; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateLightLimiterCommand( + const s32 node_id, const s16 buffer_offset, + const LightLimiterInfo::ParameterVersion1& parameter, const LightLimiterInfo::State& state, + const bool enabled, const CpuAddr workbuffer) { + auto& cmd{GenerateStart(node_id)}; + + if (IsChannelCountValid(parameter.channel_count)) { + const auto state_buffer{ + memory_pool->Translate(CpuAddr(&state), sizeof(LightLimiterInfo::State))}; + if (state_buffer) { + for (s8 channel = 0; channel < parameter.channel_count; channel++) { + cmd.inputs[channel] = buffer_offset + parameter.inputs[channel]; + cmd.outputs[channel] = buffer_offset + parameter.outputs[channel]; + } + + std::memcpy(&cmd.parameter, ¶meter, sizeof(LightLimiterInfo::ParameterVersion1)); + cmd.effect_enabled = enabled; + cmd.state = state_buffer; + cmd.workbuffer = workbuffer; + } + } + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateLightLimiterCommand( + const s32 node_id, const s16 buffer_offset, + const LightLimiterInfo::ParameterVersion2& parameter, + const LightLimiterInfo::StatisticsInternal& statistics, const LightLimiterInfo::State& state, + const bool enabled, const CpuAddr workbuffer) { + auto& cmd{GenerateStart(node_id)}; + if (IsChannelCountValid(parameter.channel_count)) { + const auto state_buffer{ + memory_pool->Translate(CpuAddr(&state), sizeof(LightLimiterInfo::State))}; + if (state_buffer) { + for (s8 channel = 0; channel < parameter.channel_count; channel++) { + cmd.inputs[channel] = buffer_offset + parameter.inputs[channel]; + cmd.outputs[channel] = buffer_offset + parameter.outputs[channel]; + } + + cmd.parameter = parameter; + cmd.effect_enabled = enabled; + cmd.state = state_buffer; + if (cmd.parameter.statistics_enabled) { + cmd.result_state = memory_pool->Translate( + CpuAddr(&statistics), sizeof(LightLimiterInfo::StatisticsInternal)); + } else { + cmd.result_state = 0; + } + cmd.workbuffer = workbuffer; + } + } + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateMultitapBiquadFilterCommand(const s32 node_id, VoiceInfo& voice_info, + const VoiceState& voice_state, + const s16 buffer_count, const s8 channel) { + auto& cmd{GenerateStart(node_id)}; + + cmd.input = buffer_count + channel; + cmd.output = buffer_count + channel; + cmd.biquads = voice_info.biquads; + + cmd.states[0] = + memory_pool->Translate(CpuAddr(voice_state.biquad_states[0].data()), + MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState)); + cmd.states[1] = + memory_pool->Translate(CpuAddr(voice_state.biquad_states[1].data()), + MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState)); + + cmd.needs_init[0] = !voice_info.biquad_initialized[0]; + cmd.needs_init[1] = !voice_info.biquad_initialized[1]; + cmd.filter_tap_count = MaxBiquadFilters; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateCaptureCommand(const s32 node_id, EffectInfoBase& effect_info, + const s16 input_index, const s16 output_index, + const s16 buffer_offset, const u32 update_count, + const u32 count_max, const u32 write_offset) { + auto& cmd{GenerateStart(node_id)}; + + if (effect_info.GetSendBuffer()) { + cmd.input = buffer_offset + input_index; + cmd.output = buffer_offset + output_index; + cmd.send_buffer_info = effect_info.GetSendBufferInfo(); + cmd.send_buffer = effect_info.GetSendBuffer(); + cmd.count_max = count_max; + cmd.write_offset = write_offset; + cmd.update_count = update_count; + cmd.effect_enabled = effect_info.IsEnabled(); + } + + GenerateEnd(cmd); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/command_buffer.h b/src/audio_core/renderer/command/command_buffer.h new file mode 100755 index 000000000..74e977c50 --- /dev/null +++ b/src/audio_core/renderer/command/command_buffer.h @@ -0,0 +1,457 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/commands.h" +#include "audio_core/renderer/effect/effect_light_limiter_info.h" +#include "audio_core/renderer/performance/performance_manager.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +struct UpsamplerInfo; +struct VoiceState; +class EffectInfoBase; +class ICommandProcessingTimeEstimator; +class MixInfo; +class MemoryPoolInfo; +class SinkInfoBase; +class VoiceInfo; + +/** + * Utility functions to generate and add commands into the current command list. + */ +class CommandBuffer { +public: + /** + * Generate a PCM s16 version 1 command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param memory_pool - Memory pool for translating buffer addresses to the DSP. + * @param voice_info - The voice info this command is generated from. + * @param voice_state - The voice state the DSP will use for this command. + * @param buffer_count - Number of mix buffers in use, + * data will be read into this index + channel. + * @param channel - Channel index for this command. + */ + void GeneratePcmInt16Version1Command(s32 node_id, const MemoryPoolInfo& memory_pool, + VoiceInfo& voice_info, const VoiceState& voice_state, + s16 buffer_count, s8 channel); + + /** + * Generate a PCM s16 version 2 command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param voice_info - The voice info this command is generated from. + * @param voice_state - The voice state the DSP will use for this command. + * @param buffer_count - Number of mix buffers in use, + * data will be read into this index + channel. + * @param channel - Channel index for this command. + */ + void GeneratePcmInt16Version2Command(s32 node_id, VoiceInfo& voice_info, + const VoiceState& voice_state, s16 buffer_count, + s8 channel); + + /** + * Generate a PCM f32 version 1 command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param memory_pool - Memory pool for translating buffer addresses to the DSP. + * @param voice_info - The voice info this command is generated from. + * @param voice_state - The voice state the DSP will use for this command. + * @param buffer_count - Number of mix buffers in use, + * data will be read into this index + channel. + * @param channel - Channel index for this command. + */ + void GeneratePcmFloatVersion1Command(s32 node_id, const MemoryPoolInfo& memory_pool, + VoiceInfo& voice_info, const VoiceState& voice_state, + s16 buffer_count, s8 channel); + + /** + * Generate a PCM f32 version 2 command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param voice_info - The voice info this command is generated from. + * @param voice_state - The voice state the DSP will use for this command. + * @param buffer_count - Number of mix buffers in use, + * data will be read into this index + channel. + * @param channel - Channel index for this command. + */ + void GeneratePcmFloatVersion2Command(s32 node_id, VoiceInfo& voice_info, + const VoiceState& voice_state, s16 buffer_count, + s8 channel); + + /** + * Generate an ADPCM version 1 command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param memory_pool - Memory pool for translating buffer addresses to the DSP. + * @param voice_info - The voice info this command is generated from. + * @param voice_state - The voice state the DSP will use for this command. + * @param buffer_count - Number of mix buffers in use, + * data will be read into this index + channel. + * @param channel - Channel index for this command. + */ + void GenerateAdpcmVersion1Command(s32 node_id, const MemoryPoolInfo& memory_pool, + VoiceInfo& voice_info, const VoiceState& voice_state, + s16 buffer_count, s8 channel); + + /** + * Generate an ADPCM version 2 command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param voice_info - The voice info this command is generated from. + * @param voice_state - The voice state the DSP will use for this command. + * @param buffer_count - Number of mix buffers in use, + * data will be read into this index + channel. + * @param channel - Channel index for this command. + */ + void GenerateAdpcmVersion2Command(s32 node_id, VoiceInfo& voice_info, + const VoiceState& voice_state, s16 buffer_count, s8 channel); + + /** + * Generate a volume command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param buffer_offset - Base mix buffer index to generate this command at. + * @param input_index - Channel index and mix buffer offset for this command. + * @param volume - Mix volume added to the input samples. + * @param precision - Number of decimal bits for fixed point operations. + */ + void GenerateVolumeCommand(s32 node_id, s16 buffer_offset, s16 input_index, f32 volume, + u8 precision); + + /** + * Generate a volume ramp command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param voice_info - The voice info this command takes its volumes from. + * @param buffer_count - Number of active mix buffers, command will generate at this index. + * @param precision - Number of decimal bits for fixed point operations. + */ + void GenerateVolumeRampCommand(s32 node_id, VoiceInfo& voice_info, s16 buffer_count, + u8 precision); + + /** + * Generate a biquad filter command from a voice, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param voice_info - The voice info this command takes biquad parameters from. + * @param voice_state - Used by the AudioRenderer to track previous samples. + * @param buffer_count - Number of active mix buffers, + * command will generate at this index + channel. + * @param channel - Channel index for this filter to work on. + * @param biquad_index - Which biquad filter to use for this command (0-1). + * @param use_float_processing - Should int or float processing be used? + */ + void GenerateBiquadFilterCommand(s32 node_id, VoiceInfo& voice_info, + const VoiceState& voice_state, s16 buffer_count, s8 channel, + u32 biquad_index, bool use_float_processing); + + /** + * Generate a biquad filter effect command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param effect_info - The effect info this command takes biquad parameters from. + * @param buffer_offset - Mix buffer offset this command will use, + * command will generate at this index + channel. + * @param channel - Channel index for this filter to work on. + * @param needs_init - True if the biquad state needs initialisation. + * @param use_float_processing - Should int or float processing be used? + */ + void GenerateBiquadFilterCommand(s32 node_id, EffectInfoBase& effect_info, s16 buffer_offset, + s8 channel, bool needs_init, bool use_float_processing); + + /** + * Generate a mix command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param input_index - Input mix buffer index for this command. + * Added to the buffer offset. + * @param output_index - Output mix buffer index for this command. + * Added to the buffer offset. + * @param buffer_offset - Mix buffer offset this command will use. + * @param volume - Volume to be applied to the input. + * @param precision - Number of decimal bits for fixed point operations. + */ + void GenerateMixCommand(s32 node_id, s16 input_index, s16 output_index, s16 buffer_offset, + f32 volume, u8 precision); + + /** + * Generate a mix ramp command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param buffer_count - Number of active mix buffers. + * @param input_index - Input mix buffer index for this command. + * Added to buffer_count. + * @param output_index - Output mix buffer index for this command. + * Added to buffer_count. + * @param volume - Current mix volume used for calculating the ramp. + * @param prev_volume - Previous mix volume, used for calculating the ramp, + * also applied to the input. + * @param precision - Number of decimal bits for fixed point operations. + */ + void GenerateMixRampCommand(s32 node_id, s16 buffer_count, s16 input_index, s16 output_index, + f32 volume, f32 prev_volume, CpuAddr prev_samples, u8 precision); + + /** + * Generate a mix ramp grouped command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param buffer_count - Number of active mix buffers. + * @param input_index - Input mix buffer index for this command. + * Added to buffer_count. + * @param output_index - Output mix buffer index for this command. + * Added to buffer_count. + * @param volumes - Current mix volumes used for calculating the ramp. + * @param prev_volumes - Previous mix volumes, used for calculating the ramp, + * also applied to the input. + * @param precision - Number of decimal bits for fixed point operations. + */ + void GenerateMixRampGroupedCommand(s32 node_id, s16 buffer_count, s16 input_index, + s16 output_index, std::span volumes, + std::span prev_volumes, CpuAddr prev_samples, + u8 precision); + + /** + * Generate a depop prepare command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param voice_state - State to track the previous depop samples for each mix buffer. + * @param buffer - State to track the current depop samples for each mix buffer. + * @param buffer_count - Number of active mix buffers. + * @param buffer_offset - Base mix buffer index to generate the channel depops at. + * @param was_playing - Command only needs to work if the voice was previously playing. + */ + void GenerateDepopPrepareCommand(s32 node_id, const VoiceState& voice_state, + std::span buffer, s16 buffer_count, + s16 buffer_offset, bool was_playing); + + /** + * Generate a depop command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param mix_info - Mix info to get the buffer count and base offsets from. + * @param depop_buffer - Buffer of current depop sample values to be added to the input + * channels. + */ + void GenerateDepopForMixBuffersCommand(s32 node_id, const MixInfo& mix_info, + std::span depop_buffer); + + /** + * Generate a delay command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param effect_info - Delay effect info to generate this command from. + * @param buffer_offset - Base mix buffer offset to apply the apply the delay. + */ + void GenerateDelayCommand(s32 node_id, EffectInfoBase& effect_info, s16 buffer_offset); + + /** + * Generate an upsample command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param buffer_offset - Base mix buffer offset to upsample. + * @param upsampler_info - Upsampler info to control the upsampling. + * @param input_count - Number of input channels to upsample. + * @param inputs - Input mix buffer indexes. + * @param buffer_count - Number of active mix buffers. + * @param sample_count - Source sample count of the input. + * @param sample_rate - Source sample rate of the input. + */ + void GenerateUpsampleCommand(s32 node_id, s16 buffer_offset, UpsamplerInfo& upsampler_info, + u32 input_count, std::span inputs, s16 buffer_count, + u32 sample_count, u32 sample_rate); + + /** + * Generate a downmix 6 -> 2 command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param inputs - Input mix buffer indexes. + * @param buffer_offset - Base mix buffer offset of the channels to downmix. + * @param downmix_coeff - Downmixing coefficients. + */ + void GenerateDownMix6chTo2chCommand(s32 node_id, std::span inputs, s16 buffer_offset, + std::span downmix_coeff); + + /** + * Generate an aux buffer command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param effect_info - Aux effect info to generate this command from. + * @param input_index - Input mix buffer index for this command. + * Added to buffer_offset. + * @param output_index - Output mix buffer index for this command. + * Added to buffer_offset. + * @param buffer_offset - Base mix buffer offset to use. + * @param update_count - Number of samples to write back to the game as updated, can be 0. + * @param count_max - Maximum number of samples to read or write. + * @param write_offset - Current read or write offset within the buffer. + */ + void GenerateAuxCommand(s32 node_id, EffectInfoBase& effect_info, s16 input_index, + s16 output_index, s16 buffer_offset, u32 update_count, u32 count_max, + u32 write_offset); + + /** + * Generate a device sink command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param buffer_offset - Base mix buffer offset to use. + * @param sink_info - The sink_info to generate this command from. + * @session_id - System session id this command is generated from. + * @samples_buffer - The buffer to be sent to the sink if upsampling is not used. + */ + void GenerateDeviceSinkCommand(s32 node_id, s16 buffer_offset, SinkInfoBase& sink_info, + u32 session_id, std::span samples_buffer); + + /** + * Generate a circular buffer sink command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param sink_info - The sink_info to generate this command from. + * @param buffer_offset - Base mix buffer offset to use. + */ + void GenerateCircularBufferSinkCommand(s32 node_id, SinkInfoBase& sink_info, s16 buffer_offset); + + /** + * Generate a reverb command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param effect_info - Reverb effect info to generate this command from. + * @param buffer_offset - Base mix buffer offset to use. + * @param long_size_pre_delay_supported - Should a longer pre-delay time be used before reverb + * begins? + */ + void GenerateReverbCommand(s32 node_id, EffectInfoBase& effect_info, s16 buffer_offset, + bool long_size_pre_delay_supported); + + /** + * Generate an I3DL2 reverb command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param effect_info - I3DL2Reverb effect info to generate this command from. + * @param buffer_offset - Base mix buffer offset to use. + */ + void GenerateI3dl2ReverbCommand(s32 node_id, EffectInfoBase& effect_info, s16 buffer_offset); + + /** + * Generate a performance command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param state - State of the performance. + * @param entry_addresses - The addresses to be filled in by the AudioRenderer. + */ + void GeneratePerformanceCommand(s32 node_id, PerformanceState state, + const PerformanceEntryAddresses& entry_addresses); + + /** + * Generate a clear mix command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + */ + void GenerateClearMixCommand(s32 node_id); + + /** + * Generate a copy mix command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param effect_info - BiquadFilter effect info to generate this command from. + * @param buffer_offset - Base mix buffer offset to use. + * @param channel - Index to the effect's parameters input indexes for this command. + */ + void GenerateCopyMixBufferCommand(s32 node_id, EffectInfoBase& effect_info, s16 buffer_offset, + s8 channel); + + /** + * Generate a light limiter version 1 command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param buffer_offset - Base mix buffer offset to use. + * @param parameter - Effect parameter to generate from. + * @param state - State used by the AudioRenderer between commands. + * @param enabled - Is this command enabled? + * @param workbuffer - Game-supplied memory for the state. + */ + void GenerateLightLimiterCommand(s32 node_id, s16 buffer_offset, + const LightLimiterInfo::ParameterVersion1& parameter, + const LightLimiterInfo::State& state, bool enabled, + CpuAddr workbuffer); + + /** + * Generate a light limiter version 2 command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param buffer_offset - Base mix buffer offset to use. + * @param parameter - Effect parameter to generate from. + * @param statistics - Statistics reported by the AudioRenderer on the limiter's state. + * @param state - State used by the AudioRenderer between commands. + * @param enabled - Is this command enabled? + * @param workbuffer - Game-supplied memory for the state. + */ + void GenerateLightLimiterCommand(s32 node_id, s16 buffer_offset, + const LightLimiterInfo::ParameterVersion2& parameter, + const LightLimiterInfo::StatisticsInternal& statistics, + const LightLimiterInfo::State& state, bool enabled, + CpuAddr workbuffer); + + /** + * Generate a multitap biquad filter command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param voice_info - The voice info this command takes biquad parameters from. + * @param voice_state - Used by the AudioRenderer to track previous samples. + * @param buffer_count - Number of active mix buffers, + * command will generate at this index + channel. + * @param channel - Channel index for this filter to work on. + */ + void GenerateMultitapBiquadFilterCommand(s32 node_id, VoiceInfo& voice_info, + const VoiceState& voice_state, s16 buffer_count, + s8 channel); + + /** + * Generate a capture command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param effect_info - Capture effect info to generate this command from. + * @param input_index - Input mix buffer index for this command. + * Added to buffer_offset. + * @param output_index - Output mix buffer index for this command (unused). + * Added to buffer_offset. + * @param buffer_offset - Base mix buffer offset to use. + * @param update_count - Number of samples to write back to the game as updated, can be 0. + * @param count_max - Maximum number of samples to read or write. + * @param write_offset - Current read or write offset within the buffer. + */ + void GenerateCaptureCommand(s32 node_id, EffectInfoBase& effect_info, s16 input_index, + s16 output_index, s16 buffer_offset, u32 update_count, + u32 count_max, u32 write_offset); + + /// Command list buffer generated commands will be added to + std::span command_list{}; + /// Input sample count, unused + u32 sample_count{}; + /// Input sample rate, unused + u32 sample_rate{}; + /// Current size of the command buffer + u64 size{}; + /// Current number of commands added + u32 count{}; + /// Current estimated processing time for all commands + u32 estimated_process_time{}; + /// Used for mapping buffers for the AudioRenderer + MemoryPoolInfo* memory_pool{}; + /// Used for estimating command process times + ICommandProcessingTimeEstimator* time_estimator{}; + /// Used to check which rendering features are currently enabled + BehaviorInfo* behavior{}; + +private: + template + T& GenerateStart(const s32 node_id); + template + void GenerateEnd(T& cmd); +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/command_generator.cpp b/src/audio_core/renderer/command/command_generator.cpp new file mode 100755 index 000000000..bcd4aca5c --- /dev/null +++ b/src/audio_core/renderer/command/command_generator.cpp @@ -0,0 +1,783 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/common/audio_renderer_parameter.h" +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/command/command_buffer.h" +#include "audio_core/renderer/command/command_generator.h" +#include "audio_core/renderer/command/command_list_header.h" +#include "audio_core/renderer/effect/effect_aux_info.h" +#include "audio_core/renderer/effect/effect_biquad_filter_info.h" +#include "audio_core/renderer/effect/effect_buffer_mixer_info.h" +#include "audio_core/renderer/effect/effect_capture_info.h" +#include "audio_core/renderer/effect/effect_context.h" +#include "audio_core/renderer/mix/mix_context.h" +#include "audio_core/renderer/performance/detail_aspect.h" +#include "audio_core/renderer/performance/entry_aspect.h" +#include "audio_core/renderer/sink/device_sink_info.h" +#include "audio_core/renderer/sink/sink_context.h" +#include "audio_core/renderer/splitter/splitter_context.h" +#include "audio_core/renderer/voice/voice_context.h" +#include "common/alignment.h" + +namespace AudioCore::AudioRenderer { + +CommandGenerator::CommandGenerator(CommandBuffer& command_buffer_, + const CommandListHeader& command_list_header_, + const AudioRendererSystemContext& render_context_, + VoiceContext& voice_context_, MixContext& mix_context_, + EffectContext& effect_context_, SinkContext& sink_context_, + SplitterContext& splitter_context_, + PerformanceManager* performance_manager_) + : command_buffer{command_buffer_}, command_header{command_list_header_}, + render_context{render_context_}, voice_context{voice_context_}, mix_context{mix_context_}, + effect_context{effect_context_}, sink_context{sink_context_}, + splitter_context{splitter_context_}, performance_manager{performance_manager_} { + command_buffer.GenerateClearMixCommand(InvalidNodeId); +} + +void CommandGenerator::GenerateDataSourceCommand(VoiceInfo& voice_info, + const VoiceState& voice_state, const s8 channel) { + if (voice_info.mix_id == UnusedMixId) { + if (voice_info.splitter_id != UnusedSplitterId) { + auto destination{splitter_context.GetDesintationData(voice_info.splitter_id, 0)}; + u32 dest_id{0}; + while (destination != nullptr) { + if (destination->IsConfigured()) { + auto mix_id{destination->GetMixId()}; + if (mix_id < mix_context.GetCount()) { + auto mix_info{mix_context.GetInfo(mix_id)}; + command_buffer.GenerateDepopPrepareCommand( + voice_info.node_id, voice_state, render_context.depop_buffer, + mix_info->buffer_count, mix_info->buffer_offset, + voice_info.was_playing); + } + } + dest_id++; + destination = splitter_context.GetDesintationData(voice_info.splitter_id, dest_id); + } + } + } else { + auto mix_info{mix_context.GetInfo(voice_info.mix_id)}; + command_buffer.GenerateDepopPrepareCommand( + voice_info.node_id, voice_state, render_context.depop_buffer, mix_info->buffer_count, + mix_info->buffer_offset, voice_info.was_playing); + } + + if (voice_info.was_playing) { + return; + } + + if (render_context.behavior->IsWaveBufferVer2Supported()) { + switch (voice_info.sample_format) { + case SampleFormat::PcmInt16: + command_buffer.GeneratePcmInt16Version2Command( + voice_info.node_id, voice_info, voice_state, render_context.mix_buffer_count, + channel); + break; + case SampleFormat::PcmFloat: + command_buffer.GeneratePcmFloatVersion2Command( + voice_info.node_id, voice_info, voice_state, render_context.mix_buffer_count, + channel); + break; + case SampleFormat::Adpcm: + command_buffer.GenerateAdpcmVersion2Command(voice_info.node_id, voice_info, voice_state, + render_context.mix_buffer_count, channel); + break; + default: + LOG_ERROR(Service_Audio, "Invalid SampleFormat {}", + static_cast(voice_info.sample_format)); + break; + } + } else { + switch (voice_info.sample_format) { + case SampleFormat::PcmInt16: + command_buffer.GeneratePcmInt16Version1Command( + voice_info.node_id, *command_buffer.memory_pool, voice_info, voice_state, + render_context.mix_buffer_count, channel); + break; + case SampleFormat::PcmFloat: + command_buffer.GeneratePcmFloatVersion1Command( + voice_info.node_id, *command_buffer.memory_pool, voice_info, voice_state, + render_context.mix_buffer_count, channel); + break; + case SampleFormat::Adpcm: + command_buffer.GenerateAdpcmVersion1Command( + voice_info.node_id, *command_buffer.memory_pool, voice_info, voice_state, + render_context.mix_buffer_count, channel); + break; + default: + LOG_ERROR(Service_Audio, "Invalid SampleFormat {}", + static_cast(voice_info.sample_format)); + break; + } + } +} + +void CommandGenerator::GenerateVoiceMixCommand(std::span mix_volumes, + std::span prev_mix_volumes, + const VoiceState& voice_state, s16 output_index, + const s16 buffer_count, const s16 input_index, + const s32 node_id) { + u8 precision{15}; + if (render_context.behavior->IsVolumeMixParameterPrecisionQ23Supported()) { + precision = 23; + } + + if (buffer_count > 8) { + const auto prev_samples{render_context.memory_pool_info->Translate( + CpuAddr(voice_state.previous_samples.data()), buffer_count * sizeof(s32))}; + command_buffer.GenerateMixRampGroupedCommand(node_id, buffer_count, input_index, + output_index, mix_volumes, prev_mix_volumes, + prev_samples, precision); + } else { + for (s16 i = 0; i < buffer_count; i++, output_index++) { + const auto prev_samples{render_context.memory_pool_info->Translate( + CpuAddr(&voice_state.previous_samples[i]), sizeof(s32))}; + + command_buffer.GenerateMixRampCommand(node_id, buffer_count, input_index, output_index, + mix_volumes[i], prev_mix_volumes[i], prev_samples, + precision); + } + } +} + +void CommandGenerator::GenerateBiquadFilterCommandForVoice(VoiceInfo& voice_info, + const VoiceState& voice_state, + const s16 buffer_count, const s8 channel, + const s32 node_id) { + const bool both_biquads_enabled{voice_info.biquads[0].enabled && voice_info.biquads[1].enabled}; + const auto use_float_processing{render_context.behavior->UseBiquadFilterFloatProcessing()}; + + if (both_biquads_enabled && render_context.behavior->UseMultiTapBiquadFilterProcessing() && + use_float_processing) { + command_buffer.GenerateMultitapBiquadFilterCommand(node_id, voice_info, voice_state, + buffer_count, channel); + } else { + for (u32 i = 0; i < MaxBiquadFilters; i++) { + if (voice_info.biquads[i].enabled) { + command_buffer.GenerateBiquadFilterCommand(node_id, voice_info, voice_state, + buffer_count, channel, i, + use_float_processing); + } + } + } +} + +void CommandGenerator::GenerateVoiceCommand(VoiceInfo& voice_info) { + u8 precision{15}; + if (render_context.behavior->IsVolumeMixParameterPrecisionQ23Supported()) { + precision = 23; + } + + for (s8 channel = 0; channel < voice_info.channel_count; channel++) { + const auto resource_id{voice_info.channel_resource_ids[channel]}; + auto& voice_state{voice_context.GetDspSharedState(resource_id)}; + auto& channel_resource{voice_context.GetChannelResource(resource_id)}; + + PerformanceDetailType detail_type{PerformanceDetailType::Invalid}; + switch (voice_info.sample_format) { + case SampleFormat::PcmInt16: + detail_type = PerformanceDetailType::Unk1; + break; + case SampleFormat::PcmFloat: + detail_type = PerformanceDetailType::Unk10; + break; + default: + detail_type = PerformanceDetailType::Unk2; + break; + } + + DetailAspect data_source_detail(*this, PerformanceEntryType::Voice, voice_info.node_id, + detail_type); + GenerateDataSourceCommand(voice_info, voice_state, channel); + + if (data_source_detail.initialized) { + command_buffer.GeneratePerformanceCommand(data_source_detail.node_id, + PerformanceState::Stop, + data_source_detail.performance_entry_address); + } + + if (voice_info.was_playing) { + voice_info.prev_volume = 0.0f; + continue; + } + + if (!voice_info.HasAnyConnection()) { + continue; + } + + DetailAspect biquad_detail_aspect(*this, PerformanceEntryType::Voice, voice_info.node_id, + PerformanceDetailType::Unk4); + GenerateBiquadFilterCommandForVoice( + voice_info, voice_state, render_context.mix_buffer_count, channel, voice_info.node_id); + + if (biquad_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + biquad_detail_aspect.node_id, PerformanceState::Stop, + biquad_detail_aspect.performance_entry_address); + } + + DetailAspect volume_ramp_detail_aspect(*this, PerformanceEntryType::Voice, + voice_info.node_id, PerformanceDetailType::Unk3); + command_buffer.GenerateVolumeRampCommand( + voice_info.node_id, voice_info, render_context.mix_buffer_count + channel, precision); + if (volume_ramp_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + volume_ramp_detail_aspect.node_id, PerformanceState::Stop, + volume_ramp_detail_aspect.performance_entry_address); + } + + voice_info.prev_volume = voice_info.volume; + + if (voice_info.mix_id == UnusedMixId) { + if (voice_info.splitter_id != UnusedSplitterId) { + auto i{channel}; + auto destination{splitter_context.GetDesintationData(voice_info.splitter_id, i)}; + while (destination != nullptr) { + if (destination->IsConfigured()) { + const auto mix_id{destination->GetMixId()}; + if (mix_id < mix_context.GetCount() && + static_cast(mix_id) != UnusedSplitterId) { + auto mix_info{mix_context.GetInfo(mix_id)}; + GenerateVoiceMixCommand( + destination->GetMixVolume(), destination->GetMixVolumePrev(), + voice_state, mix_info->buffer_offset, mix_info->buffer_count, + render_context.mix_buffer_count + channel, voice_info.node_id); + destination->MarkAsNeedToUpdateInternalState(); + } + } + i += voice_info.channel_count; + destination = splitter_context.GetDesintationData(voice_info.splitter_id, i); + } + } + } else { + DetailAspect volume_mix_detail_aspect(*this, PerformanceEntryType::Voice, + voice_info.node_id, PerformanceDetailType::Unk3); + auto mix_info{mix_context.GetInfo(voice_info.mix_id)}; + GenerateVoiceMixCommand(channel_resource.mix_volumes, channel_resource.prev_mix_volumes, + voice_state, mix_info->buffer_offset, mix_info->buffer_count, + render_context.mix_buffer_count + channel, voice_info.node_id); + if (volume_mix_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + volume_mix_detail_aspect.node_id, PerformanceState::Stop, + volume_mix_detail_aspect.performance_entry_address); + } + + channel_resource.prev_mix_volumes = channel_resource.mix_volumes; + } + voice_info.biquad_initialized[0] = voice_info.biquads[0].enabled; + voice_info.biquad_initialized[1] = voice_info.biquads[1].enabled; + } +} + +void CommandGenerator::GenerateVoiceCommands() { + const auto voice_count{voice_context.GetCount()}; + + for (u32 i = 0; i < voice_count; i++) { + auto sorted_info{voice_context.GetSortedInfo(i)}; + + if (sorted_info->ShouldSkip() || !sorted_info->UpdateForCommandGeneration(voice_context)) { + continue; + } + + EntryAspect voice_entry_aspect(*this, PerformanceEntryType::Voice, sorted_info->node_id); + + GenerateVoiceCommand(*sorted_info); + + if (voice_entry_aspect.initialized) { + command_buffer.GeneratePerformanceCommand(voice_entry_aspect.node_id, + PerformanceState::Stop, + voice_entry_aspect.performance_entry_address); + } + } + + splitter_context.UpdateInternalState(); +} + +void CommandGenerator::GenerateBufferMixerCommand(const s16 buffer_offset, + EffectInfoBase& effect_info, const s32 node_id) { + u8 precision{15}; + if (render_context.behavior->IsVolumeMixParameterPrecisionQ23Supported()) { + precision = 23; + } + + if (effect_info.IsEnabled()) { + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + for (u32 i = 0; i < parameter.mix_count; i++) { + if (parameter.volumes[i] != 0.0f) { + command_buffer.GenerateMixCommand(node_id, buffer_offset + parameter.inputs[i], + buffer_offset + parameter.outputs[i], + buffer_offset, parameter.volumes[i], precision); + } + } + } +} + +void CommandGenerator::GenerateDelayCommand(const s16 buffer_offset, EffectInfoBase& effect_info, + const s32 node_id) { + command_buffer.GenerateDelayCommand(node_id, effect_info, buffer_offset); +} + +void CommandGenerator::GenerateReverbCommand(const s16 buffer_offset, EffectInfoBase& effect_info, + const s32 node_id, + const bool long_size_pre_delay_supported) { + command_buffer.GenerateReverbCommand(node_id, effect_info, buffer_offset, + long_size_pre_delay_supported); +} + +void CommandGenerator::GenerateI3dl2ReverbEffectCommand(const s16 buffer_offset, + EffectInfoBase& effect_info, + const s32 node_id) { + command_buffer.GenerateI3dl2ReverbCommand(node_id, effect_info, buffer_offset); +} + +void CommandGenerator::GenerateAuxCommand(const s16 buffer_offset, EffectInfoBase& effect_info, + const s32 node_id) { + + if (effect_info.IsEnabled()) { + effect_info.GetWorkbuffer(0); + effect_info.GetWorkbuffer(1); + } + + if (effect_info.GetSendBuffer() != 0 && effect_info.GetReturnBuffer() != 0) { + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + auto channel_index{parameter.mix_buffer_count - 1}; + u32 write_offset{0}; + for (u32 i = 0; i < parameter.mix_buffer_count; i++, channel_index--) { + auto new_update_count{command_header.sample_count + write_offset}; + const auto update_count{channel_index > 0 ? 0 : new_update_count}; + command_buffer.GenerateAuxCommand(node_id, effect_info, parameter.inputs[i], + parameter.outputs[i], buffer_offset, update_count, + parameter.count_max, write_offset); + write_offset = new_update_count; + } + } +} + +void CommandGenerator::GenerateBiquadFilterEffectCommand(const s16 buffer_offset, + EffectInfoBase& effect_info, + const s32 node_id) { + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + if (effect_info.IsEnabled()) { + bool needs_init{false}; + + switch (parameter.state) { + case EffectInfoBase::ParameterState::Initialized: + needs_init = true; + break; + case EffectInfoBase::ParameterState::Updating: + case EffectInfoBase::ParameterState::Updated: + if (render_context.behavior->IsBiquadFilterEffectStateClaerBugFixed()) { + needs_init = false; + } else { + needs_init = parameter.state == EffectInfoBase::ParameterState::Updating; + } + break; + default: + LOG_ERROR(Service_Audio, "Invalid biquad parameter state {}", + static_cast(parameter.state)); + break; + } + + for (s8 channel = 0; channel < parameter.channel_count; channel++) { + command_buffer.GenerateBiquadFilterCommand( + node_id, effect_info, buffer_offset, channel, needs_init, + render_context.behavior->UseBiquadFilterFloatProcessing()); + } + } else { + for (s8 channel = 0; channel < parameter.channel_count; channel++) { + command_buffer.GenerateCopyMixBufferCommand(node_id, effect_info, buffer_offset, + channel); + } + } +} + +void CommandGenerator::GenerateLightLimiterEffectCommand(const s16 buffer_offset, + EffectInfoBase& effect_info, + const s32 node_id, + const u32 effect_index) { + + const auto& state{*reinterpret_cast(effect_info.GetStateBuffer())}; + + if (render_context.behavior->IsEffectInfoVersion2Supported()) { + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + const auto& result_state{*reinterpret_cast( + &effect_context.GetDspSharedResultState(effect_index))}; + command_buffer.GenerateLightLimiterCommand(node_id, buffer_offset, parameter, result_state, + state, effect_info.IsEnabled(), + effect_info.GetWorkbuffer(-1)); + } else { + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + command_buffer.GenerateLightLimiterCommand(node_id, buffer_offset, parameter, state, + effect_info.IsEnabled(), + effect_info.GetWorkbuffer(-1)); + } +} + +void CommandGenerator::GenerateCaptureCommand(const s16 buffer_offset, EffectInfoBase& effect_info, + const s32 node_id) { + if (effect_info.IsEnabled()) { + effect_info.GetWorkbuffer(0); + } + + if (effect_info.GetSendBuffer()) { + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + auto channel_index{parameter.mix_buffer_count - 1}; + u32 write_offset{0}; + for (u32 i = 0; i < parameter.mix_buffer_count; i++, channel_index--) { + auto new_update_count{command_header.sample_count + write_offset}; + const auto update_count{channel_index > 0 ? 0 : new_update_count}; + command_buffer.GenerateCaptureCommand(node_id, effect_info, parameter.inputs[i], + parameter.outputs[i], buffer_offset, update_count, + parameter.count_max, write_offset); + write_offset = new_update_count; + } + } +} + +void CommandGenerator::GenerateEffectCommand(MixInfo& mix_info) { + const auto effect_count{effect_context.GetCount()}; + for (u32 i = 0; i < effect_count; i++) { + const auto effect_index{mix_info.effect_order_buffer[i]}; + if (effect_index == -1) { + break; + } + + auto& effect_info = effect_context.GetInfo(effect_index); + if (effect_info.ShouldSkip()) { + continue; + } + + const auto entry_type{mix_info.mix_id == FinalMixId ? PerformanceEntryType::FinalMix + : PerformanceEntryType::SubMix}; + + switch (effect_info.GetType()) { + case EffectInfoBase::Type::Mix: { + DetailAspect mix_detail_aspect(*this, entry_type, mix_info.node_id, + PerformanceDetailType::Unk5); + GenerateBufferMixerCommand(mix_info.buffer_offset, effect_info, mix_info.node_id); + if (mix_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + mix_detail_aspect.node_id, PerformanceState::Stop, + mix_detail_aspect.performance_entry_address); + } + } break; + + case EffectInfoBase::Type::Aux: { + DetailAspect aux_detail_aspect(*this, entry_type, mix_info.node_id, + PerformanceDetailType::Unk7); + GenerateAuxCommand(mix_info.buffer_offset, effect_info, mix_info.node_id); + if (aux_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + aux_detail_aspect.node_id, PerformanceState::Stop, + aux_detail_aspect.performance_entry_address); + } + } break; + + case EffectInfoBase::Type::Delay: { + DetailAspect delay_detail_aspect(*this, entry_type, mix_info.node_id, + PerformanceDetailType::Unk6); + GenerateDelayCommand(mix_info.buffer_offset, effect_info, mix_info.node_id); + if (delay_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + delay_detail_aspect.node_id, PerformanceState::Stop, + delay_detail_aspect.performance_entry_address); + } + } break; + + case EffectInfoBase::Type::Reverb: { + DetailAspect reverb_detail_aspect(*this, entry_type, mix_info.node_id, + PerformanceDetailType::Unk8); + GenerateReverbCommand(mix_info.buffer_offset, effect_info, mix_info.node_id, + render_context.behavior->IsLongSizePreDelaySupported()); + if (reverb_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + reverb_detail_aspect.node_id, PerformanceState::Stop, + reverb_detail_aspect.performance_entry_address); + } + } break; + + case EffectInfoBase::Type::I3dl2Reverb: { + DetailAspect i3dl2_detail_aspect(*this, entry_type, mix_info.node_id, + PerformanceDetailType::Unk9); + GenerateI3dl2ReverbEffectCommand(mix_info.buffer_offset, effect_info, mix_info.node_id); + if (i3dl2_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + i3dl2_detail_aspect.node_id, PerformanceState::Stop, + i3dl2_detail_aspect.performance_entry_address); + } + } break; + + case EffectInfoBase::Type::BiquadFilter: { + DetailAspect biquad_detail_aspect(*this, entry_type, mix_info.node_id, + PerformanceDetailType::Unk4); + GenerateBiquadFilterEffectCommand(mix_info.buffer_offset, effect_info, + mix_info.node_id); + if (biquad_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + biquad_detail_aspect.node_id, PerformanceState::Stop, + biquad_detail_aspect.performance_entry_address); + } + } break; + + case EffectInfoBase::Type::LightLimiter: { + DetailAspect light_limiter_detail_aspect(*this, entry_type, mix_info.node_id, + PerformanceDetailType::Unk11); + GenerateLightLimiterEffectCommand(mix_info.buffer_offset, effect_info, mix_info.node_id, + effect_index); + if (light_limiter_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + light_limiter_detail_aspect.node_id, PerformanceState::Stop, + light_limiter_detail_aspect.performance_entry_address); + } + } break; + + case EffectInfoBase::Type::Capture: { + DetailAspect capture_detail_aspect(*this, entry_type, mix_info.node_id, + PerformanceDetailType::Unk12); + GenerateCaptureCommand(mix_info.buffer_offset, effect_info, mix_info.node_id); + if (capture_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + capture_detail_aspect.node_id, PerformanceState::Stop, + capture_detail_aspect.performance_entry_address); + } + } break; + + default: + LOG_ERROR(Service_Audio, "Invalid effect type {}", + static_cast(effect_info.GetType())); + break; + } + + effect_info.UpdateForCommandGeneration(); + } +} + +void CommandGenerator::GenerateMixCommands(MixInfo& mix_info) { + u8 precision{15}; + if (render_context.behavior->IsVolumeMixParameterPrecisionQ23Supported()) { + precision = 23; + } + + if (!mix_info.HasAnyConnection()) { + return; + } + + if (mix_info.dst_mix_id == UnusedMixId) { + if (mix_info.dst_splitter_id != UnusedSplitterId) { + s16 dest_id{0}; + auto destination{ + splitter_context.GetDesintationData(mix_info.dst_splitter_id, dest_id)}; + while (destination != nullptr) { + if (destination->IsConfigured()) { + auto splitter_mix_id{destination->GetMixId()}; + if (splitter_mix_id < mix_context.GetCount()) { + auto splitter_mix_info{mix_context.GetInfo(splitter_mix_id)}; + const s16 input_index{static_cast(mix_info.buffer_offset + + (dest_id % mix_info.buffer_count))}; + for (s16 i = 0; i < splitter_mix_info->buffer_count; i++) { + auto volume{mix_info.volume * destination->GetMixVolume(i)}; + if (volume != 0.0f) { + command_buffer.GenerateMixCommand( + mix_info.node_id, input_index, + splitter_mix_info->buffer_offset + i, mix_info.buffer_offset, + volume, precision); + } + } + } + } + dest_id++; + destination = + splitter_context.GetDesintationData(mix_info.dst_splitter_id, dest_id); + } + } + } else { + auto dest_mix_info{mix_context.GetInfo(mix_info.dst_mix_id)}; + for (s16 i = 0; i < mix_info.buffer_count; i++) { + for (s16 j = 0; j < dest_mix_info->buffer_count; j++) { + auto volume{mix_info.volume * mix_info.mix_volumes[i][j]}; + if (volume != 0.0f) { + command_buffer.GenerateMixCommand(mix_info.node_id, mix_info.buffer_offset + i, + dest_mix_info->buffer_offset + j, + mix_info.buffer_offset, volume, precision); + } + } + } + } +} + +void CommandGenerator::GenerateSubMixCommand(MixInfo& mix_info) { + command_buffer.GenerateDepopForMixBuffersCommand(mix_info.node_id, mix_info, + render_context.depop_buffer); + GenerateEffectCommand(mix_info); + + DetailAspect mix_detail_aspect(*this, PerformanceEntryType::SubMix, mix_info.node_id, + PerformanceDetailType::Unk5); + + GenerateMixCommands(mix_info); + + if (mix_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand(mix_detail_aspect.node_id, PerformanceState::Stop, + mix_detail_aspect.performance_entry_address); + } +} + +void CommandGenerator::GenerateSubMixCommands() { + const auto submix_count{mix_context.GetCount()}; + for (s32 i = 0; i < submix_count; i++) { + auto sorted_info{mix_context.GetSortedInfo(i)}; + if (!sorted_info->in_use || sorted_info->mix_id == FinalMixId) { + continue; + } + + EntryAspect submix_entry_aspect(*this, PerformanceEntryType::SubMix, sorted_info->node_id); + + GenerateSubMixCommand(*sorted_info); + + if (submix_entry_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + submix_entry_aspect.node_id, PerformanceState::Stop, + submix_entry_aspect.performance_entry_address); + } + } +} + +void CommandGenerator::GenerateFinalMixCommand() { + auto& final_mix_info{*mix_context.GetFinalMixInfo()}; + + command_buffer.GenerateDepopForMixBuffersCommand(final_mix_info.node_id, final_mix_info, + render_context.depop_buffer); + GenerateEffectCommand(final_mix_info); + + u8 precision{15}; + if (render_context.behavior->IsVolumeMixParameterPrecisionQ23Supported()) { + precision = 23; + } + + for (s16 i = 0; i < final_mix_info.buffer_count; i++) { + DetailAspect volume_aspect(*this, PerformanceEntryType::FinalMix, final_mix_info.node_id, + PerformanceDetailType::Unk3); + command_buffer.GenerateVolumeCommand(final_mix_info.node_id, final_mix_info.buffer_offset, + i, final_mix_info.volume, precision); + if (volume_aspect.initialized) { + command_buffer.GeneratePerformanceCommand(volume_aspect.node_id, PerformanceState::Stop, + volume_aspect.performance_entry_address); + } + } +} + +void CommandGenerator::GenerateFinalMixCommands() { + auto final_mix_info{mix_context.GetFinalMixInfo()}; + EntryAspect final_mix_entry(*this, PerformanceEntryType::FinalMix, final_mix_info->node_id); + GenerateFinalMixCommand(); + if (final_mix_entry.initialized) { + command_buffer.GeneratePerformanceCommand(final_mix_entry.node_id, PerformanceState::Stop, + final_mix_entry.performance_entry_address); + } +} + +void CommandGenerator::GenerateSinkCommands() { + const auto sink_count{sink_context.GetCount()}; + + for (u32 i = 0; i < sink_count; i++) { + auto sink_info{sink_context.GetInfo(i)}; + if (sink_info->IsUsed() && sink_info->GetType() == SinkInfoBase::Type::DeviceSink) { + auto state{reinterpret_cast(sink_info->GetState())}; + if (command_header.sample_rate != TargetSampleRate && + state->upsampler_info == nullptr) { + auto device_state{sink_info->GetDeviceState()}; + device_state->upsampler_info = render_context.upsampler_manager->Allocate(); + } + + EntryAspect device_sink_entry(*this, PerformanceEntryType::Sink, + sink_info->GetNodeId()); + auto final_mix{mix_context.GetFinalMixInfo()}; + GenerateSinkCommand(final_mix->buffer_offset, *sink_info); + + if (device_sink_entry.initialized) { + command_buffer.GeneratePerformanceCommand( + device_sink_entry.node_id, PerformanceState::Stop, + device_sink_entry.performance_entry_address); + } + } + } + + for (u32 i = 0; i < sink_count; i++) { + auto sink_info{sink_context.GetInfo(i)}; + if (sink_info->IsUsed() && sink_info->GetType() == SinkInfoBase::Type::CircularBufferSink) { + EntryAspect circular_buffer_entry(*this, PerformanceEntryType::Sink, + sink_info->GetNodeId()); + auto final_mix{mix_context.GetFinalMixInfo()}; + GenerateSinkCommand(final_mix->buffer_offset, *sink_info); + + if (circular_buffer_entry.initialized) { + command_buffer.GeneratePerformanceCommand( + circular_buffer_entry.node_id, PerformanceState::Stop, + circular_buffer_entry.performance_entry_address); + } + } + } +} + +void CommandGenerator::GenerateSinkCommand(const s16 buffer_offset, SinkInfoBase& sink_info) { + if (sink_info.ShouldSkip()) { + return; + } + + switch (sink_info.GetType()) { + case SinkInfoBase::Type::DeviceSink: + GenerateDeviceSinkCommand(buffer_offset, sink_info); + break; + + case SinkInfoBase::Type::CircularBufferSink: + command_buffer.GenerateCircularBufferSinkCommand(sink_info.GetNodeId(), sink_info, + buffer_offset); + break; + + default: + LOG_ERROR(Service_Audio, "Invalid sink type {}", static_cast(sink_info.GetType())); + break; + } + + sink_info.UpdateForCommandGeneration(); +} + +void CommandGenerator::GenerateDeviceSinkCommand(const s16 buffer_offset, SinkInfoBase& sink_info) { + auto& parameter{ + *reinterpret_cast(sink_info.GetParameter())}; + auto state{*reinterpret_cast(sink_info.GetState())}; + + if (render_context.channels == 2 && parameter.downmix_enabled) { + command_buffer.GenerateDownMix6chTo2chCommand(InvalidNodeId, parameter.inputs, + buffer_offset, parameter.downmix_coeff); + } else if (render_context.channels == 2 && parameter.input_count == 6) { + constexpr std::array default_coeffs{{1.0f, 0.707f, 0.251f, 0.707f}}; + command_buffer.GenerateDownMix6chTo2chCommand(InvalidNodeId, parameter.inputs, + buffer_offset, default_coeffs); + } + + if (state.upsampler_info != nullptr) { + command_buffer.GenerateUpsampleCommand( + InvalidNodeId, buffer_offset, *state.upsampler_info, parameter.input_count, + parameter.inputs, command_header.buffer_count, command_header.sample_count, + command_header.sample_rate); + } + + command_buffer.GenerateDeviceSinkCommand(InvalidNodeId, buffer_offset, sink_info, + render_context.session_id, + command_header.samples_buffer); +} + +void CommandGenerator::GeneratePerformanceCommand( + s32 node_id, PerformanceState state, const PerformanceEntryAddresses& entry_addresses) { + command_buffer.GeneratePerformanceCommand(node_id, state, entry_addresses); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/command_generator.h b/src/audio_core/renderer/command/command_generator.h new file mode 100755 index 000000000..bbad4cff7 --- /dev/null +++ b/src/audio_core/renderer/command/command_generator.h @@ -0,0 +1,339 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/commands.h" +#include "audio_core/renderer/performance/performance_manager.h" +#include "common/common_types.h" + +namespace AudioCore { +struct AudioRendererSystemContext; + +namespace AudioRenderer { +class CommandBuffer; +struct CommandListHeader; +class VoiceContext; +class MixContext; +class EffectContext; +class SplitterContext; +class SinkContext; +class BehaviorInfo; +class VoiceInfo; +struct VoiceState; +class MixInfo; +class SinkInfoBase; + +/** + * Generates all commands to build up a command list, which are sent to the AudioRender for + * processing. + */ +class CommandGenerator { +public: + explicit CommandGenerator(CommandBuffer& command_buffer, + const CommandListHeader& command_list_header, + const AudioRendererSystemContext& render_context, + VoiceContext& voice_context, MixContext& mix_context, + EffectContext& effect_context, SinkContext& sink_context, + SplitterContext& splitter_context, + PerformanceManager* performance_manager); + + /** + * Calculate the buffer size needed for commands. + * + * @param behavior - Used to check what features are enabled. + * @param params - Input rendering parameters for numbers of voices/mixes/sinks etc. + */ + static u64 CalculateCommandBufferSize(const BehaviorInfo& behavior, + const AudioRendererParameterInternal& params) { + u64 size{0}; + + // Effects + size += params.effects * sizeof(EffectInfoBase); + + // Voices + u64 voice_size{0}; + if (behavior.IsWaveBufferVer2Supported()) { + voice_size = std::max(std::max(sizeof(AdpcmDataSourceVersion2Command), + sizeof(PcmInt16DataSourceVersion2Command)), + sizeof(PcmFloatDataSourceVersion2Command)); + } else { + voice_size = std::max(std::max(sizeof(AdpcmDataSourceVersion1Command), + sizeof(PcmInt16DataSourceVersion1Command)), + sizeof(PcmFloatDataSourceVersion1Command)); + } + voice_size += sizeof(BiquadFilterCommand) * MaxBiquadFilters; + voice_size += sizeof(VolumeRampCommand); + voice_size += sizeof(MixRampGroupedCommand); + + size += params.voices * (params.splitter_infos * sizeof(DepopPrepareCommand) + voice_size); + + // Sub mixes + size += sizeof(DepopForMixBuffersCommand) + + (sizeof(MixCommand) * MaxMixBuffers) * MaxMixBuffers; + + // Final mix + size += sizeof(DepopForMixBuffersCommand) + sizeof(VolumeCommand) * MaxMixBuffers; + + // Splitters + size += params.splitter_destinations * sizeof(MixRampCommand) * MaxMixBuffers; + + // Sinks + size += + params.sinks * std::max(sizeof(DeviceSinkCommand), sizeof(CircularBufferSinkCommand)); + + // Performance + size += (params.effects + params.voices + params.sinks + params.sub_mixes + 1 + + PerformanceManager::MaxDetailEntries) * + sizeof(PerformanceCommand); + return size; + } + + /** + * Get the current command buffer used to generate commands. + * + * @return The command buffer. + */ + CommandBuffer& GetCommandBuffer() { + return command_buffer; + } + + /** + * Get the current performance manager, + * + * @return The performance manager. May be nullptr. + */ + PerformanceManager* GetPerformanceManager() { + return performance_manager; + } + + /** + * Generate a data source command. + * These are the basis for all audio output. + * + * @param voice_info - Generate the command from this voice. + * @param voice_state - State used by the AudioRenderer across calls. + * @param channel - Channel index to generate the command into. + */ + void GenerateDataSourceCommand(VoiceInfo& voice_info, const VoiceState& voice_state, + s8 channel); + + /** + * Generate voice mixing commands. + * These are used to mix buffers together, to mix one input to many outputs, + * and also used as copy commands to move data around and prevent it being accidentally + * overwritten, e.g by another data source command into the same channel. + * + * @param mix_volumes - Current volumes of the mix. + * @param prev_mix_volumes - Previous volumes of the mix. + * @param voice_state - State used by the AudioRenderer across calls. + * @param output_index - Output mix buffer index. + * @param buffer_count - Number of active mix buffers. + * @param input_index - Input mix buffer index. + * @param node_id - Node id of the voice this command is generated for. + */ + void GenerateVoiceMixCommand(std::span mix_volumes, + std::span prev_mix_volumes, + const VoiceState& voice_state, s16 output_index, s16 buffer_count, + s16 input_index, s32 node_id); + + /** + * Generate a biquad filter command for a voice. + * + * @param voice_info - Voice info this command is generated from. + * @param voice_state - State used by the AudioRenderer across calls. + * @param buffer_count - Number of active mix buffers. + * @param channel - Channel index of this command. + * @param node_id - Node id of the voice this command is generated for. + */ + void GenerateBiquadFilterCommandForVoice(VoiceInfo& voice_info, const VoiceState& voice_state, + s16 buffer_count, s8 channel, s32 node_id); + + /** + * Generate commands for a voice. + * Includes a data source, biquad filter, volume and mixing. + * + * @param voice_info - Voice info these commands are generated from. + */ + void GenerateVoiceCommand(VoiceInfo& voice_info); + + /** + * Generate commands for all voices. + */ + void GenerateVoiceCommands(); + + /** + * Generate a mixing command. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info_base - BufferMixer effect info. + * @param node_id - Node id of the mix this command is generated for. + */ + void GenerateBufferMixerCommand(s16 buffer_offset, EffectInfoBase& effect_info_base, + s32 node_id); + + /** + * Generate a delay effect command. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info_base - Delay effect info. + * @param node_id - Node id of the mix this command is generated for. + */ + void GenerateDelayCommand(s16 buffer_offset, EffectInfoBase& effect_info_base, s32 node_id); + + /** + * Generate a reverb effect command. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info_base - Reverb effect info. + * @param node_id - Node id of the mix this command is generated for. + * @param long_size_pre_delay_supported - Use a longer pre-delay time before reverb starts. + */ + void GenerateReverbCommand(s16 buffer_offset, EffectInfoBase& effect_info_base, s32 node_id, + bool long_size_pre_delay_supported); + + /** + * Generate an I3DL2 reverb effect command. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info_base - I3DL2Reverb effect info. + * @param node_id - Node id of the mix this command is generated for. + */ + void GenerateI3dl2ReverbEffectCommand(s16 buffer_offset, EffectInfoBase& effect_info, + s32 node_id); + + /** + * Generate an aux effect command. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info_base - Aux effect info. + * @param node_id - Node id of the mix this command is generated for. + */ + void GenerateAuxCommand(s16 buffer_offset, EffectInfoBase& effect_info, s32 node_id); + + /** + * Generate a biquad filter effect command. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info_base - Aux effect info. + * @param node_id - Node id of the mix this command is generated for. + */ + void GenerateBiquadFilterEffectCommand(s16 buffer_offset, EffectInfoBase& effect_info, + s32 node_id); + + /** + * Generate a light limiter effect command. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info_base - Limiter effect info. + * @param node_id - Node id of the mix this command is generated for. + * @param effect_index - Index for the statistics state. + */ + void GenerateLightLimiterEffectCommand(s16 buffer_offset, EffectInfoBase& effect_info, + s32 node_id, u32 effect_index); + + /** + * Generate a capture effect command. + * Writes a mix buffer back to game memory. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info_base - Capture effect info. + * @param node_id - Node id of the mix this command is generated for. + */ + void GenerateCaptureCommand(s16 buffer_offset, EffectInfoBase& effect_info, s32 node_id); + + /** + * Generate all effect commands for a mix. + * + * @param mix_info - Mix to generate effects from. + */ + void GenerateEffectCommand(MixInfo& mix_info); + + /** + * Generate all mix commands. + * + * @param mix_info - Mix to generate effects from. + */ + void GenerateMixCommands(MixInfo& mix_info); + + /** + * Generate a submix command. + * Generates all effects and all mixing commands. + * + * @param mix_info - Mix to generate effects from. + */ + void GenerateSubMixCommand(MixInfo& mix_info); + + /** + * Generate all submix command. + */ + void GenerateSubMixCommands(); + + /** + * Generate the final mix. + */ + void GenerateFinalMixCommand(); + + /** + * Generate the final mix commands. + */ + void GenerateFinalMixCommands(); + + /** + * Generate all sink commands. + */ + void GenerateSinkCommands(); + + /** + * Generate a sink command. + * Sends samples out to the backend, or a game-supplied circular buffer. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param sink_info - Sink info to generate the commands from. + */ + void GenerateSinkCommand(s16 buffer_offset, SinkInfoBase& sink_info); + + /** + * Generate a device sink command. + * Sends samples out to the backend. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param sink_info - Sink info to generate the commands from. + */ + void GenerateDeviceSinkCommand(s16 buffer_offset, SinkInfoBase& sink_info); + + /** + * Generate a performance command. + * Used to report performance metrics of the AudioRenderer back to the game. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param sink_info - Sink info to generate the commands from. + */ + void GeneratePerformanceCommand(s32 node_id, PerformanceState state, + const PerformanceEntryAddresses& entry_addresses); + +private: + /// Commands will be written by this buffer + CommandBuffer& command_buffer; + /// Header information for the commands generated + const CommandListHeader& command_header; + /// Various things to control generation + const AudioRendererSystemContext& render_context; + /// Used for generating voices + VoiceContext& voice_context; + /// Used for generating mixes + MixContext& mix_context; + /// Used for generating effects + EffectContext& effect_context; + /// Used for generating sinks + SinkContext& sink_context; + /// Used for generating submixes + SplitterContext& splitter_context; + /// Used for generating performance + PerformanceManager* performance_manager; +}; + +} // namespace AudioRenderer +} // namespace AudioCore diff --git a/src/audio_core/renderer/command/command_list_header.h b/src/audio_core/renderer/command/command_list_header.h new file mode 100755 index 000000000..988530b1f --- /dev/null +++ b/src/audio_core/renderer/command/command_list_header.h @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { + +struct CommandListHeader { + u64 buffer_size; + u32 command_count; + std::span samples_buffer; + s16 buffer_count; + u32 sample_count; + u32 sample_rate; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/command_processing_time_estimator.cpp b/src/audio_core/renderer/command/command_processing_time_estimator.cpp new file mode 100755 index 000000000..617f0ad3c --- /dev/null +++ b/src/audio_core/renderer/command/command_processing_time_estimator.cpp @@ -0,0 +1,2524 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/command/command_processing_time_estimator.h" + +namespace AudioCore::AudioRenderer { + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + const PcmInt16DataSourceVersion1Command& command) const { + return static_cast(command.pitch * 0.25f * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + const PcmInt16DataSourceVersion2Command& command) const { + return static_cast(command.pitch * 0.25f * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const PcmFloatDataSourceVersion1Command& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const PcmFloatDataSourceVersion2Command& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + const AdpcmDataSourceVersion1Command& command) const { + return static_cast(command.pitch * 0.25f * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + const AdpcmDataSourceVersion2Command& command) const { + return static_cast(command.pitch * 0.25f * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const VolumeCommand& command) const { + return static_cast((static_cast(sample_count) * 8.8f) * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const VolumeRampCommand& command) const { + return static_cast((static_cast(sample_count) * 9.8f) * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const BiquadFilterCommand& command) const { + return static_cast((static_cast(sample_count) * 58.0f) * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const MixCommand& command) const { + return static_cast((static_cast(sample_count) * 10.0f) * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const MixRampCommand& command) const { + return static_cast((static_cast(sample_count) * 14.4f) * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate(const MixRampGroupedCommand& command) const { + u32 count{0}; + for (u32 i = 0; i < command.buffer_count; i++) { + if (command.volumes[i] != 0.0f || command.prev_volumes[i] != 0.0f) { + count++; + } + } + + return static_cast(((static_cast(sample_count) * 14.4f) * 1.2f) * + static_cast(count)); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const DepopPrepareCommand& command) const { + return 1080; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + const DepopForMixBuffersCommand& command) const { + return static_cast((static_cast(sample_count) * 8.9f) * + static_cast(command.count)); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate(const DelayCommand& command) const { + return static_cast((static_cast(sample_count) * command.parameter.channel_count) * + 202.5f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const UpsampleCommand& command) const { + return 357915; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const DownMix6chTo2chCommand& command) const { + return 16108; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate(const AuxCommand& command) const { + if (command.enabled) { + return 15956; + } + return 3765; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const DeviceSinkCommand& command) const { + return 10042; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const CircularBufferSinkCommand& command) const { + return 55; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate(const ReverbCommand& command) const { + if (command.enabled) { + return static_cast( + (command.parameter.channel_count * static_cast(sample_count) * 750) * 1.2f); + } + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate(const I3dl2ReverbCommand& command) const { + if (command.enabled) { + return static_cast( + (command.parameter.channel_count * static_cast(sample_count) * 530) * 1.2f); + } + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const PerformanceCommand& command) const { + return 1454; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const ClearMixBufferCommand& command) const { + return static_cast( + ((static_cast(sample_count) * 0.83f) * static_cast(buffer_count)) * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const CopyMixBufferCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const LightLimiterVersion1Command& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const LightLimiterVersion2Command& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const MultiTapBiquadFilterCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const CaptureCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + const PcmInt16DataSourceVersion1Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 749.269f + + 6138.94f); + case 240: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 1195.456f + + 7797.047f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + const PcmInt16DataSourceVersion2Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 749.269f + + 6138.94f); + case 240: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 1195.456f + + 7797.047f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + const PcmFloatDataSourceVersion1Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 749.269f + + 6138.94f); + case 240: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 1195.456f + + 7797.047f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + const PcmFloatDataSourceVersion2Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 749.269f + + 6138.94f); + case 240: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 1195.456f + + 7797.047f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + const AdpcmDataSourceVersion1Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 2125.588f + + 9039.47f); + case 240: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 3564.088 + + 6225.471); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + const AdpcmDataSourceVersion2Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 2125.588f + + 9039.47f); + case 240: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 3564.088 + + 6225.471); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const VolumeCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1280.3f); + case 240: + return static_cast(1737.8f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const VolumeRampCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1403.9f); + case 240: + return static_cast(1884.3f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const BiquadFilterCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(4813.2f); + case 240: + return static_cast(6915.4f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const MixCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1342.2f); + case 240: + return static_cast(1833.2f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const MixRampCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1859.0f); + case 240: + return static_cast(2286.1f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate(const MixRampGroupedCommand& command) const { + u32 count{0}; + for (u32 i = 0; i < command.buffer_count; i++) { + if (command.volumes[i] != 0.0f || command.prev_volumes[i] != 0.0f) { + count++; + } + } + + switch (sample_count) { + case 160: + return static_cast((static_cast(sample_count) * 7.245f) * + static_cast(count)); + case 240: + return static_cast((static_cast(sample_count) * 7.245f) * + static_cast(count)); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const DepopPrepareCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(306.62f); + case 240: + return static_cast(293.22f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const DepopForMixBuffersCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(762.96f); + case 240: + return static_cast(726.96f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate(const DelayCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(41635.555f); + case 2: + return static_cast(97861.211f); + case 4: + return static_cast(192515.516f); + case 6: + return static_cast(301755.969f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(578.529f); + case 2: + return static_cast(663.064f); + case 4: + return static_cast(703.983f); + case 6: + return static_cast(760.032f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(8770.345f); + case 2: + return static_cast(25741.18f); + case 4: + return static_cast(47551.168f); + case 6: + return static_cast(81629.219f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(521.283f); + case 2: + return static_cast(585.396f); + case 4: + return static_cast(629.884f); + case 6: + return static_cast(713.57f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const UpsampleCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(292000.0f); + case 240: + return static_cast(0.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const DownMix6chTo2chCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(10009.0f); + case 240: + return static_cast(14577.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate(const AuxCommand& command) const { + // Is this function bugged, returning the wrong time? + // Surely the larger time should be returned when enabled... + // CMP W8, #0 + // MOV W8, #0x60; // 489.163f + // MOV W10, #0x64; // 7177.936f + // CSEL X8, X10, X8, EQ + + switch (sample_count) { + case 160: + if (command.enabled) { + return static_cast(489.163f); + } + return static_cast(7177.936f); + case 240: + if (command.enabled) { + return static_cast(485.562f); + } + return static_cast(9499.822f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate(const DeviceSinkCommand& command) const { + switch (command.input_count) { + case 2: + switch (sample_count) { + case 160: + return static_cast(9261.545f); + case 240: + return static_cast(9336.054f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + case 6: + switch (sample_count) { + case 160: + return static_cast(9336.054f); + case 240: + return static_cast(9566.728f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid input count {}", command.input_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + const CircularBufferSinkCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(static_cast(command.input_count) * 853.629f + 1284.517f); + case 240: + return static_cast(static_cast(command.input_count) * 1726.021f + 1369.683f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate(const ReverbCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(97192.227f); + case 2: + return static_cast(103278.555f); + case 4: + return static_cast(109579.039f); + case 6: + return static_cast(115065.438f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(492.009f); + case 2: + return static_cast(554.463f); + case 4: + return static_cast(595.864f); + case 6: + return static_cast(656.617f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(136463.641f); + case 2: + return static_cast(145749.047f); + case 4: + return static_cast(154796.938f); + case 6: + return static_cast(161968.406f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(495.789f); + case 2: + return static_cast(527.163f); + case 4: + return static_cast(598.752f); + case 6: + return static_cast(666.025f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate(const I3dl2ReverbCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(138836.484f); + case 2: + return static_cast(135428.172f); + case 4: + return static_cast(199181.844f); + case 6: + return static_cast(247345.906f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(718.704f); + case 2: + return static_cast(751.296f); + case 4: + return static_cast(797.464f); + case 6: + return static_cast(867.426f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(199952.734f); + case 2: + return static_cast(195199.5f); + case 4: + return static_cast(290575.875f); + case 6: + return static_cast(363494.531f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(534.24f); + case 2: + return static_cast(570.874f); + case 4: + return static_cast(660.933f); + case 6: + return static_cast(694.596f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const PerformanceCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(489.35f); + case 240: + return static_cast(491.18f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const ClearMixBufferCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(static_cast(buffer_count) * 260.4f + 139.65f); + case 240: + return static_cast(static_cast(buffer_count) * 668.85f + 193.2f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const CopyMixBufferCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(836.32f); + case 240: + return static_cast(1000.9f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const LightLimiterVersion1Command& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const LightLimiterVersion2Command& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const MultiTapBiquadFilterCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const CaptureCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + const PcmInt16DataSourceVersion1Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 427.52f + + 6329.442f); + case 240: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 710.143f + + 7853.286f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + const PcmInt16DataSourceVersion2Command& command) const { + switch (sample_count) { + case 160: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 427.52f + + 6329.442f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 371.876f + + 8049.415f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 423.43f + + 5062.659f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + case 240: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 710.143f + + 7853.286f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 610.487f + + 10138.842f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 676.722f + + 5810.962f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + const PcmFloatDataSourceVersion1Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 1672.026f + + 7681.211f); + case 240: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 2550.414f + + 9663.969f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + const PcmFloatDataSourceVersion2Command& command) const { + switch (sample_count) { + case 160: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1672.026f + + 7681.211f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1672.982f + + 9038.011f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1673.216f + + 6027.577f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + case 240: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2550.414f + + 9663.969f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2522.303f + + 11758.571f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2537.061f + + 7369.309f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + const AdpcmDataSourceVersion1Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 1827.665f + + 7913.808f); + case 240: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 2756.372f + + 9736.702f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + const AdpcmDataSourceVersion2Command& command) const { + switch (sample_count) { + case 160: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1827.665f + + 7913.808f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1829.285f + + 9607.814f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1824.609f + + 6517.476f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + case 240: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2756.372f + + 9736.702f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2731.308f + + 12154.379f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2732.152f + + 7929.442f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const VolumeCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1311.1f); + case 240: + return static_cast(1713.6f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const VolumeRampCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1425.3f); + case 240: + return static_cast(1700.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const BiquadFilterCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(4173.2f); + case 240: + return static_cast(5585.1f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const MixCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1402.8f); + case 240: + return static_cast(1853.2f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const MixRampCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1968.7f); + case 240: + return static_cast(2459.4f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate(const MixRampGroupedCommand& command) const { + u32 count{0}; + for (u32 i = 0; i < command.buffer_count; i++) { + if (command.volumes[i] != 0.0f || command.prev_volumes[i] != 0.0f) { + count++; + } + } + + switch (sample_count) { + case 160: + return static_cast((static_cast(sample_count) * 6.708f) * + static_cast(count)); + case 240: + return static_cast((static_cast(sample_count) * 6.443f) * + static_cast(count)); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const DepopPrepareCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const DepopForMixBuffersCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(739.64f); + case 240: + return static_cast(910.97f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate(const DelayCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(8929.042f); + case 2: + return static_cast(25500.75f); + case 4: + return static_cast(47759.617f); + case 6: + return static_cast(82203.07f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(1295.206f); + case 2: + return static_cast(1213.6f); + case 4: + return static_cast(942.028f); + case 6: + return static_cast(1001.553f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(11941.051f); + case 2: + return static_cast(37197.371f); + case 4: + return static_cast(69749.836f); + case 6: + return static_cast(120042.398f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(997.668f); + case 2: + return static_cast(977.634f); + case 4: + return static_cast(792.309f); + case 6: + return static_cast(875.427f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const UpsampleCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(312990.0f); + case 240: + return static_cast(0.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const DownMix6chTo2chCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(9949.7f); + case 240: + return static_cast(14679.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate(const AuxCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + return static_cast(7182.136f); + } + return static_cast(472.111f); + case 240: + if (command.enabled) { + return static_cast(9435.961f); + } + return static_cast(462.619f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate(const DeviceSinkCommand& command) const { + switch (command.input_count) { + case 2: + switch (sample_count) { + case 160: + return static_cast(8979.956f); + case 240: + return static_cast(9221.907f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + case 6: + switch (sample_count) { + case 160: + return static_cast(9177.903f); + case 240: + return static_cast(9725.897f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid input count {}", command.input_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + const CircularBufferSinkCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(static_cast(command.input_count) * 531.069f + 0.0f); + case 240: + return static_cast(static_cast(command.input_count) * 770.257f + 0.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate(const ReverbCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(81475.055f); + case 2: + return static_cast(84975.0f); + case 4: + return static_cast(91625.148f); + case 6: + return static_cast(95332.266f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(536.298f); + case 2: + return static_cast(588.798f); + case 4: + return static_cast(643.702f); + case 6: + return static_cast(705.999f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(120174.469f); + case 2: + return static_cast(125262.219f); + case 4: + return static_cast(135751.234f); + case 6: + return static_cast(141129.234f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(617.641f); + case 2: + return static_cast(659.536f); + case 4: + return static_cast(711.438f); + case 6: + return static_cast(778.071f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate(const I3dl2ReverbCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(116754.984f); + case 2: + return static_cast(125912.055f); + case 4: + return static_cast(146336.031f); + case 6: + return static_cast(165812.656f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(735.0f); + case 2: + return static_cast(766.615f); + case 4: + return static_cast(834.067f); + case 6: + return static_cast(875.437f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(170292.344f); + case 2: + return static_cast(183875.625f); + case 4: + return static_cast(214696.188f); + case 6: + return static_cast(243846.766f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(508.473f); + case 2: + return static_cast(582.445f); + case 4: + return static_cast(626.419f); + case 6: + return static_cast(682.468f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const PerformanceCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(498.17f); + case 240: + return static_cast(489.42f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const ClearMixBufferCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(static_cast(buffer_count - 1) * 266.645f + 0.0f); + case 240: + return static_cast(static_cast(buffer_count - 1) * 440.681f + 0.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const CopyMixBufferCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(842.59f); + case 240: + return static_cast(986.72f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + const LightLimiterVersion1Command& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(21392.383f); + case 2: + return static_cast(26829.389f); + case 4: + return static_cast(32405.152f); + case 6: + return static_cast(52218.586f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(897.004f); + case 2: + return static_cast(931.549f); + case 4: + return static_cast(975.387f); + case 6: + return static_cast(1016.778f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(30555.504f); + case 2: + return static_cast(39010.785f); + case 4: + return static_cast(48270.18f); + case 6: + return static_cast(76711.875f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(874.429f); + case 2: + return static_cast(921.553f); + case 4: + return static_cast(945.262f); + case 6: + return static_cast(992.26f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + const LightLimiterVersion2Command& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + if (command.parameter.statistics_enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(23308.928f); + case 2: + return static_cast(29954.062f); + case 4: + return static_cast(35807.477f); + case 6: + return static_cast(58339.773f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(21392.383f); + case 2: + return static_cast(26829.389f); + case 4: + return static_cast(32405.152f); + case 6: + return static_cast(52218.586f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(897.004f); + case 2: + return static_cast(931.549f); + case 4: + return static_cast(975.387f); + case 6: + return static_cast(1016.778f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + if (command.parameter.statistics_enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(33526.121f); + case 2: + return static_cast(43549.355f); + case 4: + return static_cast(52190.281f); + case 6: + return static_cast(85526.516f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(30555.504f); + case 2: + return static_cast(39010.785f); + case 4: + return static_cast(48270.18f); + case 6: + return static_cast(76711.875f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(874.429f); + case 2: + return static_cast(921.553f); + case 4: + return static_cast(945.262f); + case 6: + return static_cast(992.26f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const MultiTapBiquadFilterCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const CaptureCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + const PcmInt16DataSourceVersion1Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 427.52f + + 6329.442f); + case 240: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 710.143f + + 7853.286f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + const PcmInt16DataSourceVersion2Command& command) const { + switch (sample_count) { + case 160: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 427.52f + + 6329.442f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 371.876f + + 8049.415f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 423.43f + + 5062.659f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + case 240: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 710.143f + + 7853.286f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 610.487f + + 10138.842f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 676.722f + + 5810.962f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + const PcmFloatDataSourceVersion1Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 1672.026f + + 7681.211f); + case 240: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 2550.414f + + 9663.969f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + const PcmFloatDataSourceVersion2Command& command) const { + switch (sample_count) { + case 160: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1672.026f + + 7681.211f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1672.982f + + 9038.011f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1673.216f + + 6027.577f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + case 240: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2550.414f + + 9663.969f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2522.303f + + 11758.571f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2537.061f + + 7369.309f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + const AdpcmDataSourceVersion1Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 1827.665f + + 7913.808f); + case 240: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 2756.372f + + 9736.702f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + const AdpcmDataSourceVersion2Command& command) const { + switch (sample_count) { + case 160: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1827.665f + + 7913.808f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1829.285f + + 9607.814f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1824.609f + + 6517.476f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + case 240: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2756.372f + + 9736.702f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2731.308f + + 12154.379f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2732.152f + + 7929.442f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const VolumeCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1311.1f); + case 240: + return static_cast(1713.6f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const VolumeRampCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1425.3f); + case 240: + return static_cast(1700.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const BiquadFilterCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(4173.2f); + case 240: + return static_cast(5585.1f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const MixCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1402.8f); + case 240: + return static_cast(1853.2f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const MixRampCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1968.7f); + case 240: + return static_cast(2459.4f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate(const MixRampGroupedCommand& command) const { + u32 count{0}; + for (u32 i = 0; i < command.buffer_count; i++) { + if (command.volumes[i] != 0.0f || command.prev_volumes[i] != 0.0f) { + count++; + } + } + + switch (sample_count) { + case 160: + return static_cast((static_cast(sample_count) * 6.708f) * + static_cast(count)); + case 240: + return static_cast((static_cast(sample_count) * 6.443f) * + static_cast(count)); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const DepopPrepareCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const DepopForMixBuffersCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(739.64f); + case 240: + return static_cast(910.97f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate(const DelayCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(8929.042f); + case 2: + return static_cast(25500.75f); + case 4: + return static_cast(47759.617f); + case 6: + return static_cast(82203.07f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(1295.206f); + case 2: + return static_cast(1213.6f); + case 4: + return static_cast(942.028f); + case 6: + return static_cast(1001.553f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(11941.051f); + case 2: + return static_cast(37197.371f); + case 4: + return static_cast(69749.836f); + case 6: + return static_cast(120042.398f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(997.668f); + case 2: + return static_cast(977.634f); + case 4: + return static_cast(792.309f); + case 6: + return static_cast(875.427f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const UpsampleCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(312990.0f); + case 240: + return static_cast(0.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const DownMix6chTo2chCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(9949.7f); + case 240: + return static_cast(14679.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate(const AuxCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + return static_cast(7182.136f); + } + return static_cast(472.111f); + case 240: + if (command.enabled) { + return static_cast(9435.961f); + } + return static_cast(462.619f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate(const DeviceSinkCommand& command) const { + switch (command.input_count) { + case 2: + switch (sample_count) { + case 160: + return static_cast(8979.956f); + case 240: + return static_cast(9221.907f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + case 6: + switch (sample_count) { + case 160: + return static_cast(9177.903f); + case 240: + return static_cast(9725.897f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid input count {}", command.input_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + const CircularBufferSinkCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(static_cast(command.input_count) * 531.069f + 0.0f); + case 240: + return static_cast(static_cast(command.input_count) * 770.257f + 0.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate(const ReverbCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(81475.055f); + case 2: + return static_cast(84975.0f); + case 4: + return static_cast(91625.148f); + case 6: + return static_cast(95332.266f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(536.298f); + case 2: + return static_cast(588.798f); + case 4: + return static_cast(643.702f); + case 6: + return static_cast(705.999f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(120174.469f); + case 2: + return static_cast(125262.219f); + case 4: + return static_cast(135751.234f); + case 6: + return static_cast(141129.234f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(617.641f); + case 2: + return static_cast(659.536f); + case 4: + return static_cast(711.438f); + case 6: + return static_cast(778.071f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate(const I3dl2ReverbCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(116754.984f); + case 2: + return static_cast(125912.055f); + case 4: + return static_cast(146336.031f); + case 6: + return static_cast(165812.656f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(735.0f); + case 2: + return static_cast(766.615f); + case 4: + return static_cast(834.067f); + case 6: + return static_cast(875.437f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(170292.344f); + case 2: + return static_cast(183875.625f); + case 4: + return static_cast(214696.188f); + case 6: + return static_cast(243846.766f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(508.473f); + case 2: + return static_cast(582.445f); + case 4: + return static_cast(626.419f); + case 6: + return static_cast(682.468f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const PerformanceCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(498.17f); + case 240: + return static_cast(489.42f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const ClearMixBufferCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(static_cast(buffer_count - 1) * 266.645f + 0.0f); + case 240: + return static_cast(static_cast(buffer_count - 1) * 440.681f + 0.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const CopyMixBufferCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(842.59f); + case 240: + return static_cast(986.72f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + const LightLimiterVersion1Command& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(21392.383f); + case 2: + return static_cast(26829.389f); + case 4: + return static_cast(32405.152f); + case 6: + return static_cast(52218.586f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(897.004f); + case 2: + return static_cast(931.549f); + case 4: + return static_cast(975.387f); + case 6: + return static_cast(1016.778f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(30555.504f); + case 2: + return static_cast(39010.785f); + case 4: + return static_cast(48270.18f); + case 6: + return static_cast(76711.875f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(874.429f); + case 2: + return static_cast(921.553f); + case 4: + return static_cast(945.262f); + case 6: + return static_cast(992.26f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + const LightLimiterVersion2Command& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + if (command.parameter.statistics_enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(23308.928f); + case 2: + return static_cast(29954.062f); + case 4: + return static_cast(35807.477f); + case 6: + return static_cast(58339.773f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(21392.383f); + case 2: + return static_cast(26829.389f); + case 4: + return static_cast(32405.152f); + case 6: + return static_cast(52218.586f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(897.004f); + case 2: + return static_cast(931.549f); + case 4: + return static_cast(975.387f); + case 6: + return static_cast(1016.778f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + if (command.parameter.statistics_enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(33526.121f); + case 2: + return static_cast(43549.355f); + case 4: + return static_cast(52190.281f); + case 6: + return static_cast(85526.516f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(30555.504f); + case 2: + return static_cast(39010.785f); + case 4: + return static_cast(48270.18f); + case 6: + return static_cast(76711.875f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(874.429f); + case 2: + return static_cast(921.553f); + case 4: + return static_cast(945.262f); + case 6: + return static_cast(992.26f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const MultiTapBiquadFilterCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(7424.5f); + case 240: + return static_cast(9730.4f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate(const CaptureCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + return static_cast(426.982f); + } + return static_cast(4261.005f); + case 240: + if (command.enabled) { + return static_cast(435.204f); + } + return static_cast(5858.265f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/command_processing_time_estimator.h b/src/audio_core/renderer/command/command_processing_time_estimator.h new file mode 100755 index 000000000..dd7b29f32 --- /dev/null +++ b/src/audio_core/renderer/command/command_processing_time_estimator.h @@ -0,0 +1,208 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/renderer/command/commands.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Estimate the processing time required for all commands. + */ +class ICommandProcessingTimeEstimator { +public: + virtual ~ICommandProcessingTimeEstimator() = default; + + virtual u32 Estimate(const PcmInt16DataSourceVersion1Command& command) const = 0; + virtual u32 Estimate(const PcmInt16DataSourceVersion2Command& command) const = 0; + virtual u32 Estimate(const PcmFloatDataSourceVersion1Command& command) const = 0; + virtual u32 Estimate(const PcmFloatDataSourceVersion2Command& command) const = 0; + virtual u32 Estimate(const AdpcmDataSourceVersion1Command& command) const = 0; + virtual u32 Estimate(const AdpcmDataSourceVersion2Command& command) const = 0; + virtual u32 Estimate(const VolumeCommand& command) const = 0; + virtual u32 Estimate(const VolumeRampCommand& command) const = 0; + virtual u32 Estimate(const BiquadFilterCommand& command) const = 0; + virtual u32 Estimate(const MixCommand& command) const = 0; + virtual u32 Estimate(const MixRampCommand& command) const = 0; + virtual u32 Estimate(const MixRampGroupedCommand& command) const = 0; + virtual u32 Estimate(const DepopPrepareCommand& command) const = 0; + virtual u32 Estimate(const DepopForMixBuffersCommand& command) const = 0; + virtual u32 Estimate(const DelayCommand& command) const = 0; + virtual u32 Estimate(const UpsampleCommand& command) const = 0; + virtual u32 Estimate(const DownMix6chTo2chCommand& command) const = 0; + virtual u32 Estimate(const AuxCommand& command) const = 0; + virtual u32 Estimate(const DeviceSinkCommand& command) const = 0; + virtual u32 Estimate(const CircularBufferSinkCommand& command) const = 0; + virtual u32 Estimate(const ReverbCommand& command) const = 0; + virtual u32 Estimate(const I3dl2ReverbCommand& command) const = 0; + virtual u32 Estimate(const PerformanceCommand& command) const = 0; + virtual u32 Estimate(const ClearMixBufferCommand& command) const = 0; + virtual u32 Estimate(const CopyMixBufferCommand& command) const = 0; + virtual u32 Estimate(const LightLimiterVersion1Command& command) const = 0; + virtual u32 Estimate(const LightLimiterVersion2Command& command) const = 0; + virtual u32 Estimate(const MultiTapBiquadFilterCommand& command) const = 0; + virtual u32 Estimate(const CaptureCommand& command) const = 0; +}; + +class CommandProcessingTimeEstimatorVersion1 final : public ICommandProcessingTimeEstimator { +public: + CommandProcessingTimeEstimatorVersion1(u32 sample_count_, u32 buffer_count_) + : sample_count{sample_count_}, buffer_count{buffer_count_} {} + + u32 Estimate(const PcmInt16DataSourceVersion1Command& command) const override; + u32 Estimate(const PcmInt16DataSourceVersion2Command& command) const override; + u32 Estimate(const PcmFloatDataSourceVersion1Command& command) const override; + u32 Estimate(const PcmFloatDataSourceVersion2Command& command) const override; + u32 Estimate(const AdpcmDataSourceVersion1Command& command) const override; + u32 Estimate(const AdpcmDataSourceVersion2Command& command) const override; + u32 Estimate(const VolumeCommand& command) const override; + u32 Estimate(const VolumeRampCommand& command) const override; + u32 Estimate(const BiquadFilterCommand& command) const override; + u32 Estimate(const MixCommand& command) const override; + u32 Estimate(const MixRampCommand& command) const override; + u32 Estimate(const MixRampGroupedCommand& command) const override; + u32 Estimate(const DepopPrepareCommand& command) const override; + u32 Estimate(const DepopForMixBuffersCommand& command) const override; + u32 Estimate(const DelayCommand& command) const override; + u32 Estimate(const UpsampleCommand& command) const override; + u32 Estimate(const DownMix6chTo2chCommand& command) const override; + u32 Estimate(const AuxCommand& command) const override; + u32 Estimate(const DeviceSinkCommand& command) const override; + u32 Estimate(const CircularBufferSinkCommand& command) const override; + u32 Estimate(const ReverbCommand& command) const override; + u32 Estimate(const I3dl2ReverbCommand& command) const override; + u32 Estimate(const PerformanceCommand& command) const override; + u32 Estimate(const ClearMixBufferCommand& command) const override; + u32 Estimate(const CopyMixBufferCommand& command) const override; + u32 Estimate(const LightLimiterVersion1Command& command) const override; + u32 Estimate(const LightLimiterVersion2Command& command) const override; + u32 Estimate(const MultiTapBiquadFilterCommand& command) const override; + u32 Estimate(const CaptureCommand& command) const override; + +private: + u32 sample_count{}; + u32 buffer_count{}; +}; + +class CommandProcessingTimeEstimatorVersion2 final : public ICommandProcessingTimeEstimator { +public: + CommandProcessingTimeEstimatorVersion2(u32 sample_count_, u32 buffer_count_) + : sample_count{sample_count_}, buffer_count{buffer_count_} {} + + u32 Estimate(const PcmInt16DataSourceVersion1Command& command) const override; + u32 Estimate(const PcmInt16DataSourceVersion2Command& command) const override; + u32 Estimate(const PcmFloatDataSourceVersion1Command& command) const override; + u32 Estimate(const PcmFloatDataSourceVersion2Command& command) const override; + u32 Estimate(const AdpcmDataSourceVersion1Command& command) const override; + u32 Estimate(const AdpcmDataSourceVersion2Command& command) const override; + u32 Estimate(const VolumeCommand& command) const override; + u32 Estimate(const VolumeRampCommand& command) const override; + u32 Estimate(const BiquadFilterCommand& command) const override; + u32 Estimate(const MixCommand& command) const override; + u32 Estimate(const MixRampCommand& command) const override; + u32 Estimate(const MixRampGroupedCommand& command) const override; + u32 Estimate(const DepopPrepareCommand& command) const override; + u32 Estimate(const DepopForMixBuffersCommand& command) const override; + u32 Estimate(const DelayCommand& command) const override; + u32 Estimate(const UpsampleCommand& command) const override; + u32 Estimate(const DownMix6chTo2chCommand& command) const override; + u32 Estimate(const AuxCommand& command) const override; + u32 Estimate(const DeviceSinkCommand& command) const override; + u32 Estimate(const CircularBufferSinkCommand& command) const override; + u32 Estimate(const ReverbCommand& command) const override; + u32 Estimate(const I3dl2ReverbCommand& command) const override; + u32 Estimate(const PerformanceCommand& command) const override; + u32 Estimate(const ClearMixBufferCommand& command) const override; + u32 Estimate(const CopyMixBufferCommand& command) const override; + u32 Estimate(const LightLimiterVersion1Command& command) const override; + u32 Estimate(const LightLimiterVersion2Command& command) const override; + u32 Estimate(const MultiTapBiquadFilterCommand& command) const override; + u32 Estimate(const CaptureCommand& command) const override; + +private: + u32 sample_count{}; + u32 buffer_count{}; +}; + +class CommandProcessingTimeEstimatorVersion3 final : public ICommandProcessingTimeEstimator { +public: + CommandProcessingTimeEstimatorVersion3(u32 sample_count_, u32 buffer_count_) + : sample_count{sample_count_}, buffer_count{buffer_count_} {} + + u32 Estimate(const PcmInt16DataSourceVersion1Command& command) const override; + u32 Estimate(const PcmInt16DataSourceVersion2Command& command) const override; + u32 Estimate(const PcmFloatDataSourceVersion1Command& command) const override; + u32 Estimate(const PcmFloatDataSourceVersion2Command& command) const override; + u32 Estimate(const AdpcmDataSourceVersion1Command& command) const override; + u32 Estimate(const AdpcmDataSourceVersion2Command& command) const override; + u32 Estimate(const VolumeCommand& command) const override; + u32 Estimate(const VolumeRampCommand& command) const override; + u32 Estimate(const BiquadFilterCommand& command) const override; + u32 Estimate(const MixCommand& command) const override; + u32 Estimate(const MixRampCommand& command) const override; + u32 Estimate(const MixRampGroupedCommand& command) const override; + u32 Estimate(const DepopPrepareCommand& command) const override; + u32 Estimate(const DepopForMixBuffersCommand& command) const override; + u32 Estimate(const DelayCommand& command) const override; + u32 Estimate(const UpsampleCommand& command) const override; + u32 Estimate(const DownMix6chTo2chCommand& command) const override; + u32 Estimate(const AuxCommand& command) const override; + u32 Estimate(const DeviceSinkCommand& command) const override; + u32 Estimate(const CircularBufferSinkCommand& command) const override; + u32 Estimate(const ReverbCommand& command) const override; + u32 Estimate(const I3dl2ReverbCommand& command) const override; + u32 Estimate(const PerformanceCommand& command) const override; + u32 Estimate(const ClearMixBufferCommand& command) const override; + u32 Estimate(const CopyMixBufferCommand& command) const override; + u32 Estimate(const LightLimiterVersion1Command& command) const override; + u32 Estimate(const LightLimiterVersion2Command& command) const override; + u32 Estimate(const MultiTapBiquadFilterCommand& command) const override; + u32 Estimate(const CaptureCommand& command) const override; + +private: + u32 sample_count{}; + u32 buffer_count{}; +}; + +class CommandProcessingTimeEstimatorVersion4 final : public ICommandProcessingTimeEstimator { +public: + CommandProcessingTimeEstimatorVersion4(u32 sample_count_, u32 buffer_count_) + : sample_count{sample_count_}, buffer_count{buffer_count_} {} + + u32 Estimate(const PcmInt16DataSourceVersion1Command& command) const override; + u32 Estimate(const PcmInt16DataSourceVersion2Command& command) const override; + u32 Estimate(const PcmFloatDataSourceVersion1Command& command) const override; + u32 Estimate(const PcmFloatDataSourceVersion2Command& command) const override; + u32 Estimate(const AdpcmDataSourceVersion1Command& command) const override; + u32 Estimate(const AdpcmDataSourceVersion2Command& command) const override; + u32 Estimate(const VolumeCommand& command) const override; + u32 Estimate(const VolumeRampCommand& command) const override; + u32 Estimate(const BiquadFilterCommand& command) const override; + u32 Estimate(const MixCommand& command) const override; + u32 Estimate(const MixRampCommand& command) const override; + u32 Estimate(const MixRampGroupedCommand& command) const override; + u32 Estimate(const DepopPrepareCommand& command) const override; + u32 Estimate(const DepopForMixBuffersCommand& command) const override; + u32 Estimate(const DelayCommand& command) const override; + u32 Estimate(const UpsampleCommand& command) const override; + u32 Estimate(const DownMix6chTo2chCommand& command) const override; + u32 Estimate(const AuxCommand& command) const override; + u32 Estimate(const DeviceSinkCommand& command) const override; + u32 Estimate(const CircularBufferSinkCommand& command) const override; + u32 Estimate(const ReverbCommand& command) const override; + u32 Estimate(const I3dl2ReverbCommand& command) const override; + u32 Estimate(const PerformanceCommand& command) const override; + u32 Estimate(const ClearMixBufferCommand& command) const override; + u32 Estimate(const CopyMixBufferCommand& command) const override; + u32 Estimate(const LightLimiterVersion1Command& command) const override; + u32 Estimate(const LightLimiterVersion2Command& command) const override; + u32 Estimate(const MultiTapBiquadFilterCommand& command) const override; + u32 Estimate(const CaptureCommand& command) const override; + +private: + u32 sample_count{}; + u32 buffer_count{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/commands.h b/src/audio_core/renderer/command/commands.h new file mode 100755 index 000000000..45124a548 --- /dev/null +++ b/src/audio_core/renderer/command/commands.h @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/renderer/command/data_source/adpcm.h" +#include "audio_core/renderer/command/data_source/pcm_float.h" +#include "audio_core/renderer/command/data_source/pcm_int16.h" +#include "audio_core/renderer/command/effect/aux_.h" +#include "audio_core/renderer/command/effect/biquad_filter.h" +#include "audio_core/renderer/command/effect/capture.h" +#include "audio_core/renderer/command/effect/delay.h" +#include "audio_core/renderer/command/effect/i3dl2_reverb.h" +#include "audio_core/renderer/command/effect/light_limiter.h" +#include "audio_core/renderer/command/effect/multi_tap_biquad_filter.h" +#include "audio_core/renderer/command/effect/reverb.h" +#include "audio_core/renderer/command/icommand.h" +#include "audio_core/renderer/command/mix/clear_mix.h" +#include "audio_core/renderer/command/mix/copy_mix.h" +#include "audio_core/renderer/command/mix/depop_for_mix_buffers.h" +#include "audio_core/renderer/command/mix/depop_prepare.h" +#include "audio_core/renderer/command/mix/mix.h" +#include "audio_core/renderer/command/mix/mix_ramp.h" +#include "audio_core/renderer/command/mix/mix_ramp_grouped.h" +#include "audio_core/renderer/command/mix/volume.h" +#include "audio_core/renderer/command/mix/volume_ramp.h" +#include "audio_core/renderer/command/performance/performance.h" +#include "audio_core/renderer/command/resample/downmix_6ch_to_2ch.h" +#include "audio_core/renderer/command/resample/upsample.h" +#include "audio_core/renderer/command/sink/circular_buffer.h" +#include "audio_core/renderer/command/sink/device.h" diff --git a/src/audio_core/renderer/command/data_source/adpcm.cpp b/src/audio_core/renderer/command/data_source/adpcm.cpp new file mode 100755 index 000000000..e66ed2990 --- /dev/null +++ b/src/audio_core/renderer/command/data_source/adpcm.cpp @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/data_source/adpcm.h" +#include "audio_core/renderer/command/data_source/decode.h" + +namespace AudioCore::AudioRenderer { + +void AdpcmDataSourceVersion1Command::Dump(const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("AdpcmDataSourceVersion1Command\n\toutput_index {:02X} source sample " + "rate {} target sample rate {} src quality {}\n", + output_index, sample_rate, processor.target_sample_rate, src_quality); +} + +void AdpcmDataSourceVersion1Command::Process(const ADSP::CommandListProcessor& processor) { + auto out_buffer{processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count)}; + + DecodeFromWaveBuffersArgs args{ + .sample_format{SampleFormat::Adpcm}, + .output{out_buffer}, + .voice_state{reinterpret_cast(voice_state)}, + .wave_buffers{wave_buffers}, + .channel{0}, + .channel_count{1}, + .src_quality{src_quality}, + .pitch{pitch}, + .source_sample_rate{sample_rate}, + .target_sample_rate{processor.target_sample_rate}, + .sample_count{processor.sample_count}, + .data_address{data_address}, + .data_size{data_size}, + .IsVoicePlayedSampleCountResetAtLoopPointSupported{(flags & 1) != 0}, + .IsVoicePitchAndSrcSkippedSupported{(flags & 2) != 0}, + }; + + DecodeFromWaveBuffers(*processor.memory, args); +} + +bool AdpcmDataSourceVersion1Command::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +void AdpcmDataSourceVersion2Command::Dump(const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("AdpcmDataSourceVersion2Command\n\toutput_index {:02X} source sample " + "rate {} target sample rate {} src quality {}\n", + output_index, sample_rate, processor.target_sample_rate, src_quality); +} + +void AdpcmDataSourceVersion2Command::Process(const ADSP::CommandListProcessor& processor) { + auto out_buffer{processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count)}; + + DecodeFromWaveBuffersArgs args{ + .sample_format{SampleFormat::Adpcm}, + .output{out_buffer}, + .voice_state{reinterpret_cast(voice_state)}, + .wave_buffers{wave_buffers}, + .channel{0}, + .channel_count{1}, + .src_quality{src_quality}, + .pitch{pitch}, + .source_sample_rate{sample_rate}, + .target_sample_rate{processor.target_sample_rate}, + .sample_count{processor.sample_count}, + .data_address{data_address}, + .data_size{data_size}, + .IsVoicePlayedSampleCountResetAtLoopPointSupported{(flags & 1) != 0}, + .IsVoicePitchAndSrcSkippedSupported{(flags & 2) != 0}, + }; + + DecodeFromWaveBuffers(*processor.memory, args); +} + +bool AdpcmDataSourceVersion2Command::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/data_source/adpcm.h b/src/audio_core/renderer/command/data_source/adpcm.h new file mode 100755 index 000000000..a9cf9cee4 --- /dev/null +++ b/src/audio_core/renderer/command/data_source/adpcm.h @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/common/common.h" +#include "audio_core/common/wave_buffer.h" +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command to decode ADPCM-encoded version 1 wavebuffers + * into the output_index mix buffer. + */ +struct AdpcmDataSourceVersion1Command : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Quality used for sample rate conversion + SrcQuality src_quality; + /// Mix buffer index for decoded samples + s16 output_index; + /// Flags to control decoding (see AudioCore::AudioRenderer::VoiceInfo::Flags) + u16 flags; + /// Wavebuffer sample rate + u32 sample_rate; + /// Pitch used for sample rate conversion + f32 pitch; + /// Wavebuffers containing the wavebuffer address, context address, looping information etc + std::array wave_buffers; + /// Voice state, updated each call and written back to game + CpuAddr voice_state; + /// Coefficients data address + CpuAddr data_address; + /// Coefficients data size + u64 data_size; +}; + +/** + * AudioRenderer command to decode ADPCM-encoded version 2 wavebuffers + * into the output_index mix buffer. + */ +struct AdpcmDataSourceVersion2Command : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Quality used for sample rate conversion + SrcQuality src_quality; + /// Mix buffer index for decoded samples + s16 output_index; + /// Flags to control decoding (see AudioCore::AudioRenderer::VoiceInfo::Flags) + u16 flags; + /// Wavebuffer sample rate + u32 sample_rate; + /// Pitch used for sample rate conversion + f32 pitch; + /// Target channel to read within the wavebuffer + s8 channel_index; + /// Number of channels within the wavebuffer + s8 channel_count; + /// Wavebuffers containing the wavebuffer address, context address, looping information etc + std::array wave_buffers; + /// Voice state, updated each call and written back to game + CpuAddr voice_state; + /// Coefficients data address + CpuAddr data_address; + /// Coefficients data size + u64 data_size; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/data_source/decode.cpp b/src/audio_core/renderer/command/data_source/decode.cpp new file mode 100755 index 000000000..0759a67ef --- /dev/null +++ b/src/audio_core/renderer/command/data_source/decode.cpp @@ -0,0 +1,428 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "audio_core/renderer/command/data_source/decode.h" +#include "audio_core/renderer/command/resample/resample.h" +#include "common/fixed_point.h" +#include "common/logging/log.h" +#include "core/memory.h" + +namespace AudioCore::AudioRenderer { + +constexpr u32 TempBufferSize = 0x3F00; +constexpr std::array PitchBySrcQuality = {4, 8, 4}; + +/** + * Decode PCM data. Only s16 or f32 is supported. + * + * @tparam T - Type to decode. Only s16 and f32 are supported. + * @param memory - Core memory for reading samples. + * @param out_buffer - Output mix buffer to receive the samples. + * @param req - Information for how to decode. + * @return Number of samples decoded. + */ +template +static u32 DecodePcm(Core::Memory::Memory& memory, std::span out_buffer, + const DecodeArg& req) { + constexpr s32 min{std::numeric_limits::min()}; + constexpr s32 max{std::numeric_limits::max()}; + + if (req.buffer == 0 || req.buffer_size == 0) { + return 0; + } + + if (req.start_offset >= req.end_offset) { + return 0; + } + + auto samples_to_decode{ + std::min(req.samples_to_read, req.end_offset - req.start_offset - req.offset)}; + u32 channel_count{static_cast(req.channel_count)}; + + switch (req.channel_count) { + default: { + const VAddr source{req.buffer + + (((req.start_offset + req.offset) * channel_count) * sizeof(T))}; + const u64 size{channel_count * samples_to_decode}; + const u64 size_bytes{size * sizeof(T)}; + + std::vector samples(size); + memory.ReadBlockUnsafe(source, samples.data(), size_bytes); + + if constexpr (std::is_floating_point_v) { + for (u32 i = 0; i < samples_to_decode; i++) { + auto sample{static_cast(samples[i * channel_count + req.target_channel] * + std::numeric_limits::max())}; + out_buffer[i] = static_cast(std::clamp(sample, min, max)); + } + } else { + for (u32 i = 0; i < samples_to_decode; i++) { + out_buffer[i] = samples[i * channel_count + req.target_channel]; + } + } + } break; + + case 1: + if (req.target_channel != 0) { + LOG_ERROR(Service_Audio, "Invalid target channel, expected 0, got {}", + req.target_channel); + return 0; + } + + const VAddr source{req.buffer + ((req.start_offset + req.offset) * sizeof(T))}; + std::vector samples(samples_to_decode); + memory.ReadBlockUnsafe(source, samples.data(), samples_to_decode * sizeof(T)); + + if constexpr (std::is_floating_point_v) { + for (u32 i = 0; i < samples_to_decode; i++) { + auto sample{static_cast(samples[i * channel_count + req.target_channel] * + std::numeric_limits::max())}; + out_buffer[i] = static_cast(std::clamp(sample, min, max)); + } + } else { + std::memcpy(out_buffer.data(), samples.data(), samples_to_decode * sizeof(s16)); + } + break; + } + + return samples_to_decode; +} + +/** + * Decode ADPCM data. + * + * @param memory - Core memory for reading samples. + * @param out_buffer - Output mix buffer to receive the samples. + * @param req - Information for how to decode. + * @return Number of samples decoded. + */ +static u32 DecodeAdpcm(Core::Memory::Memory& memory, std::span out_buffer, + const DecodeArg& req) { + constexpr u32 SamplesPerFrame{14}; + constexpr u32 NibblesPerFrame{16}; + + if (req.buffer == 0 || req.buffer_size == 0) { + LOG_ERROR(Service_Audio, "Buffer is 0!"); + return 0; + } + + if (req.start_offset >= req.end_offset) { + LOG_ERROR(Service_Audio, "Start offset greater than end offset!"); + return 0; + } + + auto end{(req.end_offset % SamplesPerFrame) + + NibblesPerFrame * (req.end_offset / SamplesPerFrame)}; + if (req.end_offset % SamplesPerFrame) { + end += 3; + } else { + end += 1; + } + + if (end / 2 > req.buffer_size) { + LOG_ERROR(Service_Audio, "End greater than buffer size!"); + return 0; + } + + auto samples_to_process{ + std::min(req.end_offset - req.start_offset - req.offset, req.samples_to_read)}; + + auto samples_to_read{samples_to_process}; + auto start_pos{req.start_offset + req.offset}; + auto samples_remaining_in_frame{start_pos % SamplesPerFrame}; + auto position_in_frame{(start_pos / SamplesPerFrame) * NibblesPerFrame + + samples_remaining_in_frame}; + + if (samples_remaining_in_frame) { + position_in_frame += 2; + } + + const auto size{std::max((samples_to_process / 8U) * SamplesPerFrame, 8U)}; + std::vector wavebuffer(size, 0); + memory.ReadBlockUnsafe(req.buffer + position_in_frame / 2, wavebuffer.data(), + wavebuffer.size()); + + auto context{req.adpcm_context}; + auto header{context->header}; + u8 coeff_index{static_cast((header >> 4U) & 0xFU)}; + u8 scale{static_cast(header & 0xFU)}; + s32 coeff0{req.coefficients[coeff_index * 2 + 0]}; + s32 coeff1{req.coefficients[coeff_index * 2 + 1]}; + + auto yn0{context->yn0}; + auto yn1{context->yn1}; + + static constexpr std::array Steps{ + 0, 1, 2, 3, 4, 5, 6, 7, -8, -7, -6, -5, -4, -3, -2, -1, + }; + + const auto decode_sample = [&](const s32 code) -> s16 { + const auto xn = code * (1 << scale); + const auto prediction = coeff0 * yn0 + coeff1 * yn1; + const auto sample = ((xn << 11) + 0x400 + prediction) >> 11; + const auto saturated = std::clamp(sample, -0x8000, 0x7FFF); + yn1 = yn0; + yn0 = static_cast(saturated); + return yn0; + }; + + u32 read_index{0}; + u32 write_index{0}; + + while (samples_to_read > 0) { + // Are we at a new frame? + if ((position_in_frame % NibblesPerFrame) == 0) { + header = wavebuffer[read_index++]; + coeff_index = (header >> 4) & 0xF; + scale = header & 0xF; + coeff0 = req.coefficients[coeff_index * 2 + 0]; + coeff1 = req.coefficients[coeff_index * 2 + 1]; + position_in_frame += 2; + + // Can we consume all of this frame's samples? + if (samples_to_read >= SamplesPerFrame) { + // Can grab all samples until the next header + for (u32 i = 0; i < SamplesPerFrame / 2; i++) { + auto code0{Steps[(wavebuffer[read_index] >> 4) & 0xF]}; + auto code1{Steps[wavebuffer[read_index] & 0xF]}; + read_index++; + + out_buffer[write_index++] = decode_sample(code0); + out_buffer[write_index++] = decode_sample(code1); + } + + position_in_frame += SamplesPerFrame; + samples_to_read -= SamplesPerFrame; + continue; + } + } + + // Decode a single sample + auto code{wavebuffer[read_index]}; + if (position_in_frame & 1) { + code &= 0xF; + read_index++; + } else { + code >>= 4; + } + + out_buffer[write_index++] = decode_sample(Steps[code]); + + position_in_frame++; + samples_to_read--; + } + + context->header = header; + context->yn0 = yn0; + context->yn1 = yn1; + + return samples_to_process; +} + +/** + * Decode implementation. + * Decode wavebuffers according to the given args. + * + * @param memory - Core memory to read data from. + * @param args - The wavebuffer data, and information for how to decode it. + */ +void DecodeFromWaveBuffers(Core::Memory::Memory& memory, const DecodeFromWaveBuffersArgs& args) { + auto& voice_state{*args.voice_state}; + auto remaining_sample_count{args.sample_count}; + auto fraction{voice_state.fraction}; + + const auto sample_rate_ratio{ + (Common::FixedPoint<49, 15>(args.source_sample_rate) / args.target_sample_rate) * + args.pitch}; + const auto size_required{fraction + remaining_sample_count * sample_rate_ratio}; + + if (size_required.to_int_floor() < 0) { + return; + } + + auto pitch{PitchBySrcQuality[static_cast(args.src_quality)]}; + if (static_cast(pitch + size_required.to_int_floor()) > TempBufferSize) { + return; + } + + auto max_remaining_sample_count{ + ((Common::FixedPoint<17, 15>(TempBufferSize) - fraction) / sample_rate_ratio) + .to_uint_floor()}; + max_remaining_sample_count = std::min(max_remaining_sample_count, remaining_sample_count); + + auto wavebuffers_consumed{voice_state.wave_buffers_consumed}; + auto wavebuffer_index{voice_state.wave_buffer_index}; + auto played_sample_count{voice_state.played_sample_count}; + + bool is_buffer_starved{false}; + u32 offset{voice_state.offset}; + + auto output_buffer{args.output}; + std::vector temp_buffer(TempBufferSize, 0); + + while (remaining_sample_count > 0) { + const auto samples_to_write{std::min(remaining_sample_count, max_remaining_sample_count)}; + const auto samples_to_read{ + (fraction + samples_to_write * sample_rate_ratio).to_uint_floor()}; + + u32 temp_buffer_pos{0}; + + if (!args.IsVoicePitchAndSrcSkippedSupported) { + for (u32 i = 0; i < pitch; i++) { + temp_buffer[i] = voice_state.sample_history[i]; + } + temp_buffer_pos = pitch; + } + + u32 samples_read{0}; + while (samples_read < samples_to_read) { + if (wavebuffer_index >= MaxWaveBuffers) { + LOG_ERROR(Service_Audio, "Invalid wavebuffer index! {}", wavebuffer_index); + wavebuffer_index = 0; + voice_state.wave_buffer_valid.fill(false); + wavebuffers_consumed = MaxWaveBuffers; + } + + if (!voice_state.wave_buffer_valid[wavebuffer_index]) { + is_buffer_starved = true; + break; + } + + auto& wavebuffer{args.wave_buffers[wavebuffer_index]}; + + if (offset == 0 && args.sample_format == SampleFormat::Adpcm && + wavebuffer.context != 0) { + memory.ReadBlockUnsafe(wavebuffer.context, &voice_state.adpcm_context, + wavebuffer.context_size); + } + + auto start_offset{wavebuffer.start_offset}; + auto end_offset{wavebuffer.end_offset}; + + if (wavebuffer.loop && voice_state.loop_count > 0 && + wavebuffer.loop_start_offset != 0 && wavebuffer.loop_end_offset != 0 && + wavebuffer.loop_start_offset <= wavebuffer.loop_end_offset) { + start_offset = wavebuffer.loop_start_offset; + end_offset = wavebuffer.loop_end_offset; + } + + DecodeArg decode_arg{.buffer{wavebuffer.buffer}, + .buffer_size{wavebuffer.buffer_size}, + .start_offset{start_offset}, + .end_offset{end_offset}, + .channel_count{args.channel_count}, + .coefficients{}, + .adpcm_context{nullptr}, + .target_channel{args.channel}, + .offset{offset}, + .samples_to_read{samples_to_read - samples_read}}; + + s32 samples_decoded{0}; + + switch (args.sample_format) { + case SampleFormat::PcmInt16: + samples_decoded = DecodePcm( + memory, {&temp_buffer[temp_buffer_pos], args.sample_count}, decode_arg); + break; + + case SampleFormat::PcmFloat: + samples_decoded = DecodePcm( + memory, {&temp_buffer[temp_buffer_pos], args.sample_count}, decode_arg); + break; + + case SampleFormat::Adpcm: { + decode_arg.adpcm_context = &voice_state.adpcm_context; + memory.ReadBlockUnsafe(args.data_address, &decode_arg.coefficients, args.data_size); + samples_decoded = DecodeAdpcm( + memory, {&temp_buffer[temp_buffer_pos], args.sample_count}, decode_arg); + } break; + + default: + LOG_ERROR(Service_Audio, "Invalid sample format to decode {}", + static_cast(args.sample_format)); + samples_decoded = 0; + break; + } + + played_sample_count += samples_decoded; + samples_read += samples_decoded; + temp_buffer_pos += samples_decoded; + offset += samples_decoded; + + if (samples_decoded == 0 || offset >= end_offset - start_offset) { + offset = 0; + if (!wavebuffer.loop) { + voice_state.wave_buffer_valid[wavebuffer_index] = false; + voice_state.loop_count = 0; + + if (wavebuffer.stream_ended) { + played_sample_count = 0; + } + + wavebuffer_index = (wavebuffer_index + 1) % MaxWaveBuffers; + wavebuffers_consumed++; + } else { + voice_state.loop_count++; + if (wavebuffer.loop_count > 0 && + (voice_state.loop_count > wavebuffer.loop_count || samples_decoded == 0)) { + voice_state.wave_buffer_valid[wavebuffer_index] = false; + voice_state.loop_count = 0; + + if (wavebuffer.stream_ended) { + played_sample_count = 0; + } + + wavebuffer_index = (wavebuffer_index + 1) % MaxWaveBuffers; + wavebuffers_consumed++; + } + + if (samples_decoded == 0) { + is_buffer_starved = true; + break; + } + + if (args.IsVoicePlayedSampleCountResetAtLoopPointSupported) { + played_sample_count = 0; + } + } + } + } + + if (args.IsVoicePitchAndSrcSkippedSupported) { + if (samples_read > output_buffer.size()) { + LOG_ERROR(Service_Audio, "Attempting to write past the end of output buffer!"); + } + for (u32 i = 0; i < samples_read; i++) { + output_buffer[i] = temp_buffer[i]; + } + } else { + std::memset(&temp_buffer[temp_buffer_pos], 0, + (samples_to_read - samples_read) * sizeof(s16)); + + Resample(output_buffer, temp_buffer, sample_rate_ratio, fraction, samples_to_write, + args.src_quality); + + std::memcpy(voice_state.sample_history.data(), &temp_buffer[samples_to_read], + pitch * sizeof(s16)); + } + + remaining_sample_count -= samples_to_write; + if (remaining_sample_count != 0 && is_buffer_starved) { + LOG_ERROR(Service_Audio, "Samples remaining but buffer is starving??"); + break; + } + + output_buffer = output_buffer.subspan(samples_to_write); + } + + voice_state.wave_buffers_consumed = wavebuffers_consumed; + voice_state.played_sample_count = played_sample_count; + voice_state.wave_buffer_index = wavebuffer_index; + voice_state.offset = offset; + voice_state.fraction = fraction; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/data_source/decode.h b/src/audio_core/renderer/command/data_source/decode.h new file mode 100755 index 000000000..4d63d6fa8 --- /dev/null +++ b/src/audio_core/renderer/command/data_source/decode.h @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/common/common.h" +#include "audio_core/common/wave_buffer.h" +#include "audio_core/renderer/voice/voice_state.h" +#include "common/common_types.h" + +namespace Core::Memory { +class Memory; +} + +namespace AudioCore::AudioRenderer { + +struct DecodeFromWaveBuffersArgs { + SampleFormat sample_format; + std::span output; + VoiceState* voice_state; + std::span wave_buffers; + s8 channel; + s8 channel_count; + SrcQuality src_quality; + f32 pitch; + u32 source_sample_rate; + u32 target_sample_rate; + u32 sample_count; + CpuAddr data_address; + u64 data_size; + bool IsVoicePlayedSampleCountResetAtLoopPointSupported; + bool IsVoicePitchAndSrcSkippedSupported; +}; + +struct DecodeArg { + CpuAddr buffer; + u64 buffer_size; + u32 start_offset; + u32 end_offset; + s8 channel_count; + std::array coefficients; + VoiceState::AdpcmContext* adpcm_context; + s8 target_channel; + u32 offset; + u32 samples_to_read; +}; + +/** + * Decode wavebuffers according to the given args. + * + * @param memory - Core memory to read data from. + * @param args - The wavebuffer data, and information for how to decode it. + */ +void DecodeFromWaveBuffers(Core::Memory::Memory& memory, const DecodeFromWaveBuffersArgs& args); + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/data_source/pcm_float.cpp b/src/audio_core/renderer/command/data_source/pcm_float.cpp new file mode 100755 index 000000000..be77fab69 --- /dev/null +++ b/src/audio_core/renderer/command/data_source/pcm_float.cpp @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/data_source/decode.h" +#include "audio_core/renderer/command/data_source/pcm_float.h" + +namespace AudioCore::AudioRenderer { + +void PcmFloatDataSourceVersion1Command::Dump(const ADSP::CommandListProcessor& processor, + std::string& string) { + string += + fmt::format("PcmFloatDataSourceVersion1Command\n\toutput_index {:02X} channel {} " + "channel count {} source sample rate {} target sample rate {} src quality {}\n", + output_index, channel_index, channel_count, sample_rate, + processor.target_sample_rate, src_quality); +} + +void PcmFloatDataSourceVersion1Command::Process(const ADSP::CommandListProcessor& processor) { + auto out_buffer = processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count); + + DecodeFromWaveBuffersArgs args{ + .sample_format{SampleFormat::PcmFloat}, + .output{out_buffer}, + .voice_state{reinterpret_cast(voice_state)}, + .wave_buffers{wave_buffers}, + .channel{channel_index}, + .channel_count{channel_count}, + .src_quality{src_quality}, + .pitch{pitch}, + .source_sample_rate{sample_rate}, + .target_sample_rate{processor.target_sample_rate}, + .sample_count{processor.sample_count}, + .data_address{0}, + .data_size{0}, + .IsVoicePlayedSampleCountResetAtLoopPointSupported{(flags & 1) != 0}, + .IsVoicePitchAndSrcSkippedSupported{(flags & 2) != 0}, + }; + + DecodeFromWaveBuffers(*processor.memory, args); +} + +bool PcmFloatDataSourceVersion1Command::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +void PcmFloatDataSourceVersion2Command::Dump(const ADSP::CommandListProcessor& processor, + std::string& string) { + string += + fmt::format("PcmFloatDataSourceVersion2Command\n\toutput_index {:02X} channel {} " + "channel count {} source sample rate {} target sample rate {} src quality {}\n", + output_index, channel_index, channel_count, sample_rate, + processor.target_sample_rate, src_quality); +} + +void PcmFloatDataSourceVersion2Command::Process(const ADSP::CommandListProcessor& processor) { + auto out_buffer = processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count); + + DecodeFromWaveBuffersArgs args{ + .sample_format{SampleFormat::PcmFloat}, + .output{out_buffer}, + .voice_state{reinterpret_cast(voice_state)}, + .wave_buffers{wave_buffers}, + .channel{channel_index}, + .channel_count{channel_count}, + .src_quality{src_quality}, + .pitch{pitch}, + .source_sample_rate{sample_rate}, + .target_sample_rate{processor.target_sample_rate}, + .sample_count{processor.sample_count}, + .data_address{0}, + .data_size{0}, + .IsVoicePlayedSampleCountResetAtLoopPointSupported{(flags & 1) != 0}, + .IsVoicePitchAndSrcSkippedSupported{(flags & 2) != 0}, + }; + + DecodeFromWaveBuffers(*processor.memory, args); +} + +bool PcmFloatDataSourceVersion2Command::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/data_source/pcm_float.h b/src/audio_core/renderer/command/data_source/pcm_float.h new file mode 100755 index 000000000..e4af77c20 --- /dev/null +++ b/src/audio_core/renderer/command/data_source/pcm_float.h @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/wave_buffer.h" +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command to decode PCM float-encoded version 1 wavebuffers + * into the output_index mix buffer. + */ +struct PcmFloatDataSourceVersion1Command : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Quality used for sample rate conversion + SrcQuality src_quality; + /// Mix buffer index for decoded samples + s16 output_index; + /// Flags to control decoding (see AudioCore::AudioRenderer::VoiceInfo::Flags) + u16 flags; + /// Wavebuffer sample rate + u32 sample_rate; + /// Pitch used for sample rate conversion + f32 pitch; + /// Target channel to read within the wavebuffer + s8 channel_index; + /// Number of channels within the wavebuffer + s8 channel_count; + /// Wavebuffers containing the wavebuffer address, context address, looping information etc + std::array wave_buffers; + /// Voice state, updated each call and written back to game + CpuAddr voice_state; +}; + +/** + * AudioRenderer command to decode PCM float-encoded version 2 wavebuffers + * into the output_index mix buffer. + */ +struct PcmFloatDataSourceVersion2Command : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Quality used for sample rate conversion + SrcQuality src_quality; + /// Mix buffer index for decoded samples + s16 output_index; + /// Flags to control decoding (see AudioCore::AudioRenderer::VoiceInfo::Flags) + u16 flags; + /// Wavebuffer sample rate + u32 sample_rate; + /// Pitch used for sample rate conversion + f32 pitch; + /// Target channel to read within the wavebuffer + s8 channel_index; + /// Number of channels within the wavebuffer + s8 channel_count; + /// Wavebuffers containing the wavebuffer address, context address, looping information etc + std::array wave_buffers; + /// Voice state, updated each call and written back to game + CpuAddr voice_state; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/data_source/pcm_int16.cpp b/src/audio_core/renderer/command/data_source/pcm_int16.cpp new file mode 100755 index 000000000..7a27463e4 --- /dev/null +++ b/src/audio_core/renderer/command/data_source/pcm_int16.cpp @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/data_source/decode.h" +#include "audio_core/renderer/command/data_source/pcm_int16.h" + +namespace AudioCore::AudioRenderer { + +void PcmInt16DataSourceVersion1Command::Dump(const ADSP::CommandListProcessor& processor, + std::string& string) { + string += + fmt::format("PcmInt16DataSourceVersion1Command\n\toutput_index {:02X} channel {} " + "channel count {} source sample rate {} target sample rate {} src quality {}\n", + output_index, channel_index, channel_count, sample_rate, + processor.target_sample_rate, src_quality); +} + +void PcmInt16DataSourceVersion1Command::Process(const ADSP::CommandListProcessor& processor) { + auto out_buffer = processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count); + + DecodeFromWaveBuffersArgs args{ + .sample_format{SampleFormat::PcmInt16}, + .output{out_buffer}, + .voice_state{reinterpret_cast(voice_state)}, + .wave_buffers{wave_buffers}, + .channel{channel_index}, + .channel_count{channel_count}, + .src_quality{src_quality}, + .pitch{pitch}, + .source_sample_rate{sample_rate}, + .target_sample_rate{processor.target_sample_rate}, + .sample_count{processor.sample_count}, + .data_address{0}, + .data_size{0}, + .IsVoicePlayedSampleCountResetAtLoopPointSupported{(flags & 1) != 0}, + .IsVoicePitchAndSrcSkippedSupported{(flags & 2) != 0}, + }; + + DecodeFromWaveBuffers(*processor.memory, args); +} + +bool PcmInt16DataSourceVersion1Command::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +void PcmInt16DataSourceVersion2Command::Dump(const ADSP::CommandListProcessor& processor, + std::string& string) { + string += + fmt::format("PcmInt16DataSourceVersion2Command\n\toutput_index {:02X} channel {} " + "channel count {} source sample rate {} target sample rate {} src quality {}\n", + output_index, channel_index, channel_count, sample_rate, + processor.target_sample_rate, src_quality); +} + +void PcmInt16DataSourceVersion2Command::Process(const ADSP::CommandListProcessor& processor) { + auto out_buffer = processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count); + DecodeFromWaveBuffersArgs args{ + .sample_format{SampleFormat::PcmInt16}, + .output{out_buffer}, + .voice_state{reinterpret_cast(voice_state)}, + .wave_buffers{wave_buffers}, + .channel{channel_index}, + .channel_count{channel_count}, + .src_quality{src_quality}, + .pitch{pitch}, + .source_sample_rate{sample_rate}, + .target_sample_rate{processor.target_sample_rate}, + .sample_count{processor.sample_count}, + .data_address{0}, + .data_size{0}, + .IsVoicePlayedSampleCountResetAtLoopPointSupported{(flags & 1) != 0}, + .IsVoicePitchAndSrcSkippedSupported{(flags & 2) != 0}, + }; + + DecodeFromWaveBuffers(*processor.memory, args); +} + +bool PcmInt16DataSourceVersion2Command::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/data_source/pcm_int16.h b/src/audio_core/renderer/command/data_source/pcm_int16.h new file mode 100755 index 000000000..5de1ad60d --- /dev/null +++ b/src/audio_core/renderer/command/data_source/pcm_int16.h @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/wave_buffer.h" +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command to decode PCM s16-encoded version 1 wavebuffers + * into the output_index mix buffer. + */ +struct PcmInt16DataSourceVersion1Command : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Quality used for sample rate conversion + SrcQuality src_quality; + /// Mix buffer index for decoded samples + s16 output_index; + /// Flags to control decoding (see AudioCore::AudioRenderer::VoiceInfo::Flags) + u16 flags; + /// Wavebuffer sample rate + u32 sample_rate; + /// Pitch used for sample rate conversion + f32 pitch; + /// Target channel to read within the wavebuffer + s8 channel_index; + /// Number of channels within the wavebuffer + s8 channel_count; + /// Wavebuffers containing the wavebuffer address, context address, looping information etc + std::array wave_buffers; + /// Voice state, updated each call and written back to game + CpuAddr voice_state; +}; + +/** + * AudioRenderer command to decode PCM s16-encoded version 2 wavebuffers + * into the output_index mix buffer. + */ +struct PcmInt16DataSourceVersion2Command : ICommand { + /** + * Print this command's information to a string. + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Quality used for sample rate conversion + SrcQuality src_quality; + /// Mix buffer index for decoded samples + s16 output_index; + /// Flags to control decoding (see AudioCore::AudioRenderer::VoiceInfo::Flags) + u16 flags; + /// Wavebuffer sample rate + u32 sample_rate; + /// Pitch used for sample rate conversion + f32 pitch; + /// Target channel to read within the wavebuffer + s8 channel_index; + /// Number of channels within the wavebuffer + s8 channel_count; + /// Wavebuffers containing the wavebuffer address, context address, looping information etc + std::array wave_buffers; + /// Voice state, updated each call and written back to game + CpuAddr voice_state; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/aux_.cpp b/src/audio_core/renderer/command/effect/aux_.cpp new file mode 100755 index 000000000..dae70ac0f --- /dev/null +++ b/src/audio_core/renderer/command/effect/aux_.cpp @@ -0,0 +1,207 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/effect/aux_.h" +#include "audio_core/renderer/effect/effect_aux_info.h" +#include "core/memory.h" + +namespace AudioCore::AudioRenderer { +/** + * Reset an AuxBuffer. + * + * @param memory - Core memory for writing. + * @param aux_info - Memory address pointing to the AuxInfo to reset. + */ +static void ResetAuxBufferDsp(Core::Memory::Memory& memory, const CpuAddr aux_info) { + if (aux_info == 0) { + LOG_ERROR(Service_Audio, "Aux info is 0!"); + return; + } + + auto info{reinterpret_cast(memory.GetPointer(aux_info))}; + info->read_offset = 0; + info->write_offset = 0; + info->total_sample_count = 0; +} + +/** + * Write the given input mix buffer to the memory at send_buffer, and update send_info_ if + * update_count is set, to notify the game that an update happened. + * + * @param memory - Core memory for writing. + * @param send_info_ - Meta information for where to write the mix buffer. + * @param sample_count - Unused. + * @param send_buffer - Memory address to write the mix buffer to. + * @param count_max - Maximum number of samples in the receiving buffer. + * @param input - Input mix buffer to write. + * @param write_count_ - Number of samples to write. + * @param write_offset - Current offset to begin writing the receiving buffer at. + * @param update_count - If non-zero, send_info_ will be updated. + * @return Number of samples written. + */ +static u32 WriteAuxBufferDsp(Core::Memory::Memory& memory, const CpuAddr send_info_, + [[maybe_unused]] u32 sample_count, const CpuAddr send_buffer, + const u32 count_max, std::span input, + const u32 write_count_, const u32 write_offset, + const u32 update_count) { + if (write_count_ > count_max) { + LOG_ERROR(Service_Audio, + "write_count must be smaller than count_max! write_count {}, count_max {}", + write_count_, count_max); + return 0; + } + + if (input.empty()) { + LOG_ERROR(Service_Audio, "input buffer is empty!"); + return 0; + } + + if (send_buffer == 0) { + LOG_ERROR(Service_Audio, "send_buffer is 0!"); + return 0; + } + + if (count_max == 0) { + return 0; + } + + AuxInfo::AuxInfoDsp send_info{}; + memory.ReadBlockUnsafe(send_info_, &send_info, sizeof(AuxInfo::AuxInfoDsp)); + + u32 target_write_offset{send_info.write_offset + write_offset}; + if (target_write_offset > count_max || write_count_ == 0) { + return 0; + } + + u32 write_count{write_count_}; + u32 write_pos{0}; + while (write_count > 0) { + u32 to_write{std::min(count_max - target_write_offset, write_count)}; + + if (to_write > 0) { + memory.WriteBlockUnsafe(send_buffer + target_write_offset * sizeof(s32), + &input[write_pos], to_write * sizeof(s32)); + } + + target_write_offset = (target_write_offset + to_write) % count_max; + write_count -= to_write; + write_pos += to_write; + } + + if (update_count) { + send_info.write_offset = (send_info.write_offset + update_count) % count_max; + } + + memory.WriteBlockUnsafe(send_info_, &send_info, sizeof(AuxInfo::AuxInfoDsp)); + + return write_count_; +} + +/** + * Read the given memory at return_buffer into the output mix buffer, and update return_info_ if + * update_count is set, to notify the game that an update happened. + * + * @param memory - Core memory for writing. + * @param return_info_ - Meta information for where to read the mix buffer. + * @param return_buffer - Memory address to read the samples from. + * @param count_max - Maximum number of samples in the receiving buffer. + * @param output - Output mix buffer which will receive the samples. + * @param count_ - Number of samples to read. + * @param read_offset - Current offset to begin reading the return_buffer at. + * @param update_count - If non-zero, send_info_ will be updated. + * @return Number of samples read. + */ +static u32 ReadAuxBufferDsp(Core::Memory::Memory& memory, const CpuAddr return_info_, + const CpuAddr return_buffer, const u32 count_max, std::span output, + const u32 count_, const u32 read_offset, const u32 update_count) { + if (count_max == 0) { + return 0; + } + + if (count_ > count_max) { + LOG_ERROR(Service_Audio, "count must be smaller than count_max! count {}, count_max {}", + count_, count_max); + return 0; + } + + if (output.empty()) { + LOG_ERROR(Service_Audio, "output buffer is empty!"); + return 0; + } + + if (return_buffer == 0) { + LOG_ERROR(Service_Audio, "return_buffer is 0!"); + return 0; + } + + AuxInfo::AuxInfoDsp return_info{}; + memory.ReadBlockUnsafe(return_info_, &return_info, sizeof(AuxInfo::AuxInfoDsp)); + + u32 target_read_offset{return_info.read_offset + read_offset}; + if (target_read_offset > count_max) { + return 0; + } + + u32 read_count{count_}; + u32 read_pos{0}; + while (read_count > 0) { + u32 to_read{std::min(count_max - target_read_offset, read_count)}; + + if (to_read > 0) { + memory.ReadBlockUnsafe(return_buffer + target_read_offset * sizeof(s32), + &output[read_pos], to_read * sizeof(s32)); + } + + target_read_offset = (target_read_offset + to_read) % count_max; + read_count -= to_read; + read_pos += to_read; + } + + if (update_count) { + return_info.read_offset = (return_info.read_offset + update_count) % count_max; + } + + memory.WriteBlockUnsafe(return_info_, &return_info, sizeof(AuxInfo::AuxInfoDsp)); + + return count_; +} + +void AuxCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("AuxCommand\n\tenabled {} input {:02X} output {:02X}\n", effect_enabled, + input, output); +} + +void AuxCommand::Process(const ADSP::CommandListProcessor& processor) { + auto input_buffer{ + processor.mix_buffers.subspan(input * processor.sample_count, processor.sample_count)}; + auto output_buffer{ + processor.mix_buffers.subspan(output * processor.sample_count, processor.sample_count)}; + + if (effect_enabled) { + WriteAuxBufferDsp(*processor.memory, send_buffer_info, processor.sample_count, send_buffer, + count_max, input_buffer, processor.sample_count, write_offset, + update_count); + + auto read{ReadAuxBufferDsp(*processor.memory, return_buffer_info, return_buffer, count_max, + output_buffer, processor.sample_count, write_offset, + update_count)}; + + if (read != processor.sample_count) { + std::memset(&output_buffer[read], 0, processor.sample_count - read); + } + } else { + ResetAuxBufferDsp(*processor.memory, send_buffer_info); + ResetAuxBufferDsp(*processor.memory, return_buffer_info); + if (input != output) { + std::memcpy(output_buffer.data(), input_buffer.data(), output_buffer.size_bytes()); + } + } +} + +bool AuxCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/aux_.h b/src/audio_core/renderer/command/effect/aux_.h new file mode 100755 index 000000000..825c93732 --- /dev/null +++ b/src/audio_core/renderer/command/effect/aux_.h @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command to read and write an auxiliary buffer, writing the input mix buffer to game + * memory, and reading into the output buffer from game memory. + */ +struct AuxCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer index + s16 input; + /// Output mix buffer index + s16 output; + /// Meta info for writing + CpuAddr send_buffer_info; + /// Meta info for reading + CpuAddr return_buffer_info; + /// Game memory write buffer + CpuAddr send_buffer; + /// Game memory read buffer + CpuAddr return_buffer; + /// Max samples to read/write + u32 count_max; + /// Current read/write offset + u32 write_offset; + /// Number of samples to update per call + u32 update_count; + /// is this effect enabled? + bool effect_enabled; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/biquad_filter.cpp b/src/audio_core/renderer/command/effect/biquad_filter.cpp new file mode 100755 index 000000000..1baae74fd --- /dev/null +++ b/src/audio_core/renderer/command/effect/biquad_filter.cpp @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/effect/biquad_filter.h" +#include "audio_core/renderer/voice/voice_state.h" + +namespace AudioCore::AudioRenderer { +/** + * Biquad filter float implementation. + * + * @param output - Output container for filtered samples. + * @param input - Input container for samples to be filtered. + * @param b - Feedforward coefficients. + * @param a - Feedback coefficients. + * @param state - State to track previous samples between calls. + * @param sample_count - Number of samples to process. + */ +void ApplyBiquadFilterFloat(std::span output, std::span input, + std::array& b_, std::array& a_, + VoiceState::BiquadFilterState& state, const u32 sample_count) { + constexpr s64 min{std::numeric_limits::min()}; + constexpr s64 max{std::numeric_limits::max()}; + std::array b{Common::FixedPoint<50, 14>::from_base(b_[0]).to_double(), + Common::FixedPoint<50, 14>::from_base(b_[1]).to_double(), + Common::FixedPoint<50, 14>::from_base(b_[2]).to_double()}; + std::array a{Common::FixedPoint<50, 14>::from_base(a_[0]).to_double(), + Common::FixedPoint<50, 14>::from_base(a_[1]).to_double()}; + std::array s{state.s0.to_double(), state.s1.to_double(), state.s2.to_double(), + state.s3.to_double()}; + + for (u32 i = 0; i < sample_count; i++) { + f64 in_sample{static_cast(input[i])}; + auto sample{in_sample * b[0] + s[0] * b[1] + s[1] * b[2] + s[2] * a[0] + s[3] * a[1]}; + + output[i] = static_cast(std::clamp(static_cast(sample), min, max)); + + s[1] = s[0]; + s[0] = in_sample; + s[3] = s[2]; + s[2] = sample; + } + + state.s0 = s[0]; + state.s1 = s[1]; + state.s2 = s[2]; + state.s3 = s[3]; +} + +/** + * Biquad filter s32 implementation. + * + * @param output - Output container for filtered samples. + * @param input - Input container for samples to be filtered. + * @param b - Feedforward coefficients. + * @param a - Feedback coefficients. + * @param state - State to track previous samples between calls. + * @param sample_count - Number of samples to process. + */ +static void ApplyBiquadFilterInt(std::span output, std::span input, + std::array& b_, std::array& a_, + VoiceState::BiquadFilterState& state, const u32 sample_count) { + constexpr s64 min{std::numeric_limits::min()}; + constexpr s64 max{std::numeric_limits::max()}; + std::array, 3> b{ + Common::FixedPoint<50, 14>::from_base(b_[0]), + Common::FixedPoint<50, 14>::from_base(b_[1]), + Common::FixedPoint<50, 14>::from_base(b_[2]), + }; + std::array, 3> a{ + Common::FixedPoint<50, 14>::from_base(a_[0]), + Common::FixedPoint<50, 14>::from_base(a_[1]), + }; + + for (u32 i = 0; i < sample_count; i++) { + s64 in_sample{input[i]}; + auto sample{in_sample * b[0] + state.s0}; + const auto out_sample{std::clamp(sample.to_long(), min, max)}; + + output[i] = static_cast(out_sample); + + state.s0 = state.s1 + b[1] * in_sample + a[0] * out_sample; + state.s1 = 0 + b[2] * in_sample + a[1] * out_sample; + } +} + +void BiquadFilterCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format( + "BiquadFilterCommand\n\tinput {:02X} output {:02X} needs_init {} use_float_processing {}\n", + input, output, needs_init, use_float_processing); +} + +void BiquadFilterCommand::Process(const ADSP::CommandListProcessor& processor) { + auto state_{reinterpret_cast(state)}; + if (needs_init) { + std::memset(state_, 0, sizeof(VoiceState::BiquadFilterState)); + } + + auto input_buffer{ + processor.mix_buffers.subspan(input * processor.sample_count, processor.sample_count)}; + auto output_buffer{ + processor.mix_buffers.subspan(output * processor.sample_count, processor.sample_count)}; + + if (use_float_processing) { + ApplyBiquadFilterFloat(output_buffer, input_buffer, biquad.b, biquad.a, *state_, + processor.sample_count); + } else { + ApplyBiquadFilterInt(output_buffer, input_buffer, biquad.b, biquad.a, *state_, + processor.sample_count); + } +} + +bool BiquadFilterCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/biquad_filter.h b/src/audio_core/renderer/command/effect/biquad_filter.h new file mode 100755 index 000000000..4c9c42d29 --- /dev/null +++ b/src/audio_core/renderer/command/effect/biquad_filter.h @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "audio_core/renderer/voice/voice_info.h" +#include "audio_core/renderer/voice/voice_state.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for applying a biquad filter to the input mix buffer, saving the results to + * the output mix buffer. + */ +struct BiquadFilterCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer index + s16 input; + /// Output mix buffer index + s16 output; + /// Input parameters for biquad + VoiceInfo::BiquadFilterParameter biquad; + /// Biquad state, updated each call + CpuAddr state; + /// If true, reset the state + bool needs_init; + /// If true, use float processing rather than int + bool use_float_processing; +}; + +/** + * Biquad filter float implementation. + * + * @param output - Output container for filtered samples. + * @param input - Input container for samples to be filtered. + * @param b - Feedforward coefficients. + * @param a - Feedback coefficients. + * @param state - State to track previous samples. + * @param sample_count - Number of samples to process. + */ +void ApplyBiquadFilterFloat(std::span output, std::span input, + std::array& b, std::array& a, + VoiceState::BiquadFilterState& state, const u32 sample_count); + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/capture.cpp b/src/audio_core/renderer/command/effect/capture.cpp new file mode 100755 index 000000000..112ddf60b --- /dev/null +++ b/src/audio_core/renderer/command/effect/capture.cpp @@ -0,0 +1,142 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/effect/capture.h" +#include "audio_core/renderer/effect/effect_aux_info.h" +#include "core/memory.h" + +namespace AudioCore::AudioRenderer { +/** + * Reset an AuxBuffer. + * + * @param memory - Core memory for writing. + * @param aux_info - Memory address pointing to the AuxInfo to reset. + */ +static void ResetAuxBufferDsp(Core::Memory::Memory& memory, const CpuAddr aux_info) { + if (aux_info == 0) { + LOG_ERROR(Service_Audio, "Aux info is 0!"); + return; + } + + memory.Write32(VAddr(aux_info + offsetof(AuxInfo::AuxInfoDsp, read_offset)), 0); + memory.Write32(VAddr(aux_info + offsetof(AuxInfo::AuxInfoDsp, write_offset)), 0); + memory.Write32(VAddr(aux_info + offsetof(AuxInfo::AuxInfoDsp, total_sample_count)), 0); +} + +/** + * Write the given input mix buffer to the memory at send_buffer, and update send_info_ if + * update_count is set, to notify the game that an update happened. + * + * @param memory - Core memory for writing. + * @param send_info_ - Header information for where to write the mix buffer. + * @param send_buffer - Memory address to write the mix buffer to. + * @param count_max - Maximum number of samples in the receiving buffer. + * @param input - Input mix buffer to write. + * @param write_count_ - Number of samples to write. + * @param write_offset - Current offset to begin writing the receiving buffer at. + * @param update_count - If non-zero, send_info_ will be updated. + * @return Number of samples written. + */ +static u32 WriteAuxBufferDsp(Core::Memory::Memory& memory, const CpuAddr send_info_, + const CpuAddr send_buffer, u32 count_max, std::span input, + const u32 write_count_, const u32 write_offset, + const u32 update_count) { + if (write_count_ > count_max) { + LOG_ERROR(Service_Audio, + "write_count must be smaller than count_max! write_count {}, count_max {}", + write_count_, count_max); + return 0; + } + + if (send_info_ == 0) { + LOG_ERROR(Service_Audio, "send_info is 0!"); + return 0; + } + + if (input.empty()) { + LOG_ERROR(Service_Audio, "input buffer is empty!"); + return 0; + } + + if (send_buffer == 0) { + LOG_ERROR(Service_Audio, "send_buffer is 0!"); + return 0; + } + + if (count_max == 0) { + return 0; + } + + AuxInfo::AuxBufferInfo send_info{}; + memory.ReadBlockUnsafe(send_info_, &send_info, sizeof(AuxInfo::AuxBufferInfo)); + + u32 target_write_offset{send_info.dsp_info.write_offset + write_offset}; + if (target_write_offset > count_max || write_count_ == 0) { + return 0; + } + + u32 write_count{write_count_}; + u32 write_pos{0}; + while (write_count > 0) { + u32 to_write{std::min(count_max - target_write_offset, write_count)}; + + if (to_write > 0) { + memory.WriteBlockUnsafe(send_buffer + target_write_offset * sizeof(s32), + &input[write_pos], to_write * sizeof(s32)); + } + + target_write_offset = (target_write_offset + to_write) % count_max; + write_count -= to_write; + write_pos += to_write; + } + + if (update_count) { + const auto count_diff{send_info.dsp_info.total_sample_count - + send_info.cpu_info.total_sample_count}; + if (count_diff >= count_max) { + auto dsp_lost_count{send_info.dsp_info.lost_sample_count + update_count}; + if (dsp_lost_count - send_info.cpu_info.lost_sample_count < + send_info.dsp_info.lost_sample_count - send_info.cpu_info.lost_sample_count) { + dsp_lost_count = send_info.cpu_info.lost_sample_count - 1; + } + send_info.dsp_info.lost_sample_count = dsp_lost_count; + } + + send_info.dsp_info.write_offset = + (send_info.dsp_info.write_offset + update_count + count_max) % count_max; + + auto new_sample_count{send_info.dsp_info.total_sample_count + update_count}; + if (new_sample_count - send_info.cpu_info.total_sample_count < count_diff) { + new_sample_count = send_info.cpu_info.total_sample_count - 1; + } + send_info.dsp_info.total_sample_count = new_sample_count; + } + + memory.WriteBlockUnsafe(send_info_, &send_info, sizeof(AuxInfo::AuxBufferInfo)); + + return write_count_; +} + +void CaptureCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("CaptureCommand\n\tenabled {} input {:02X} output {:02X}", effect_enabled, + input, output); +} + +void CaptureCommand::Process(const ADSP::CommandListProcessor& processor) { + if (effect_enabled) { + auto input_buffer{ + processor.mix_buffers.subspan(input * processor.sample_count, processor.sample_count)}; + WriteAuxBufferDsp(*processor.memory, send_buffer_info, send_buffer, count_max, input_buffer, + processor.sample_count, write_offset, update_count); + } else { + ResetAuxBufferDsp(*processor.memory, send_buffer_info); + } +} + +bool CaptureCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/capture.h b/src/audio_core/renderer/command/effect/capture.h new file mode 100755 index 000000000..8670acb24 --- /dev/null +++ b/src/audio_core/renderer/command/effect/capture.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for capturing a mix buffer. That is, writing it back to a given game memory + * address. + */ +struct CaptureCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer index + s16 input; + /// Output mix buffer index + s16 output; + /// Meta info for writing + CpuAddr send_buffer_info; + /// Game memory write buffer + CpuAddr send_buffer; + /// Max samples to read/write + u32 count_max; + /// Current read/write offset + u32 write_offset; + /// Number of samples to update per call + u32 update_count; + /// is this effect enabled? + bool effect_enabled; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/delay.cpp b/src/audio_core/renderer/command/effect/delay.cpp new file mode 100755 index 000000000..5329b11ed --- /dev/null +++ b/src/audio_core/renderer/command/effect/delay.cpp @@ -0,0 +1,244 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/effect/delay.h" + +namespace AudioCore::AudioRenderer { +/** + * Update the DelayInfo state according to the given parameters. + * + * @param params - Input parameters to update the state. + * @param state - State to be updated. + */ +static void SetDelayEffectParameter(const DelayInfo::ParameterVersion1& params, + DelayInfo::State& state) { + auto channel_spread{params.channel_spread}; + state.feedback_gain = params.feedback_gain * 0.97998046875f; + state.delay_feedback_gain = state.feedback_gain * (1.0f - channel_spread); + if (params.channel_count == 4 || params.channel_count == 6) { + channel_spread >>= 1; + } + state.delay_feedback_cross_gain = channel_spread * state.feedback_gain; + state.lowpass_feedback_gain = params.lowpass_amount * 0.949951171875f; + state.lowpass_gain = 1.0f - state.lowpass_feedback_gain; +} + +/** + * Initialize a new DelayInfo state according to the given parameters. + * + * @param params - Input parameters to update the state. + * @param state - State to be updated. + * @param workbuffer - Game-supplied memory for the state. (Unused) + */ +static void InitializeDelayEffect(const DelayInfo::ParameterVersion1& params, + DelayInfo::State& state, + [[maybe_unused]] const CpuAddr workbuffer) { + state = {}; + + for (u32 channel = 0; channel < params.channel_count; channel++) { + Common::FixedPoint<32, 32> sample_count_max{0.064f}; + sample_count_max *= params.sample_rate.to_int_floor() * params.delay_time_max; + + Common::FixedPoint<18, 14> delay_time{params.delay_time}; + delay_time *= params.sample_rate / 1000; + Common::FixedPoint<32, 32> sample_count{delay_time}; + + if (sample_count > sample_count_max) { + sample_count = sample_count_max; + } + + state.delay_lines[channel].sample_count_max = sample_count_max.to_int_floor(); + state.delay_lines[channel].sample_count = sample_count.to_int_floor(); + state.delay_lines[channel].buffer.resize(state.delay_lines[channel].sample_count, 0); + if (state.delay_lines[channel].buffer.size() == 0) { + state.delay_lines[channel].buffer.push_back(0); + } + state.delay_lines[channel].buffer_pos = 0; + state.delay_lines[channel].decay_rate = 1.0f; + } + + SetDelayEffectParameter(params, state); +} + +/** + * Delay effect impl, according to the parameters and current state, on the input mix buffers, + * saving the results to the output mix buffers. + * + * @param params - Input parameters to use. + * @param state - State to use, must be initialized (see InitializeDelayEffect). + * @param inputs - Input mix buffers to performan the delay on. + * @param outputs - Output mix buffers to receive the delayed samples. + * @param sample_count - Number of samples to process. + */ +template +static void ApplyDelay(const DelayInfo::ParameterVersion1& params, DelayInfo::State& state, + std::vector>& inputs, + std::vector>& outputs, const u32 sample_count) { + for (u32 i = 0; i < sample_count; i++) { + std::array, Channels> input_samples{}; + for (u32 channel = 0; channel < Channels; channel++) { + input_samples[channel] = inputs[channel][i] * 64; + } + + std::array, Channels> delay_samples{}; + for (u32 channel = 0; channel < Channels; channel++) { + delay_samples[channel] = state.delay_lines[channel].Read(); + } + + std::array, Channels>, Channels> matrix{}; + if constexpr (Channels == 1) { + matrix = {{ + {state.feedback_gain}, + }}; + } else if constexpr (Channels == 2) { + matrix = {{ + {state.delay_feedback_gain, state.delay_feedback_cross_gain}, + {state.delay_feedback_cross_gain, state.delay_feedback_gain}, + }}; + } else if constexpr (Channels == 4) { + matrix = {{ + {state.delay_feedback_gain, state.delay_feedback_cross_gain, + state.delay_feedback_cross_gain, 0.0f}, + {state.delay_feedback_cross_gain, state.delay_feedback_gain, 0.0f, + state.delay_feedback_cross_gain}, + {state.delay_feedback_cross_gain, 0.0f, state.delay_feedback_gain, + state.delay_feedback_cross_gain}, + {0.0f, state.delay_feedback_cross_gain, state.delay_feedback_cross_gain, + state.delay_feedback_gain}, + }}; + } else if constexpr (Channels == 6) { + matrix = {{ + {state.delay_feedback_gain, 0.0f, 0.0f, 0.0f, state.delay_feedback_cross_gain, + state.delay_feedback_cross_gain}, + {0.0f, state.delay_feedback_gain, 0.0f, state.delay_feedback_cross_gain, + state.delay_feedback_cross_gain, 0.0f}, + {state.delay_feedback_cross_gain, 0.0f, state.delay_feedback_gain, + state.delay_feedback_cross_gain, 0.0f, 0.0f}, + {0.0f, state.delay_feedback_cross_gain, state.delay_feedback_cross_gain, + state.delay_feedback_gain, 0.0f, 0.0f}, + {state.delay_feedback_cross_gain, state.delay_feedback_cross_gain, 0.0f, 0.0f, + state.delay_feedback_gain, 0.0f}, + {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, params.feedback_gain}, + }}; + } + + std::array, Channels> gained_samples{}; + for (u32 channel = 0; channel < Channels; channel++) { + Common::FixedPoint<50, 14> delay{}; + for (u32 j = 0; j < Channels; j++) { + delay += delay_samples[j] * matrix[j][channel]; + } + gained_samples[channel] = input_samples[channel] * params.in_gain + delay; + } + + for (u32 channel = 0; channel < Channels; channel++) { + state.lowpass_z[channel] = gained_samples[channel] * state.lowpass_gain + + state.lowpass_z[channel] * state.lowpass_feedback_gain; + state.delay_lines[channel].Write(state.lowpass_z[channel]); + } + + for (u32 channel = 0; channel < Channels; channel++) { + outputs[channel][i] = (input_samples[channel] * params.dry_gain + + delay_samples[channel] * params.wet_gain) + .to_int_floor() / + 64; + } + } +} + +/** + * Apply a delay effect if enabled, according to the parameters and current state, on the input mix + * buffers, saving the results to the output mix buffers. + * + * @param params - Input parameters to use. + * @param state - State to use, must be initialized (see InitializeDelayEffect). + * @param enabled - If enabled, delay will be applied, otherwise input is copied to output. + * @param inputs - Input mix buffers to performan the delay on. + * @param outputs - Output mix buffers to receive the delayed samples. + * @param sample_count - Number of samples to process. + */ +static void ApplyDelayEffect(const DelayInfo::ParameterVersion1& params, DelayInfo::State& state, + const bool enabled, std::vector>& inputs, + std::vector>& outputs, const u32 sample_count) { + + if (!IsChannelCountValid(params.channel_count)) { + LOG_ERROR(Service_Audio, "Invalid delay channels {}", params.channel_count); + return; + } + + if (enabled) { + switch (params.channel_count) { + case 1: + ApplyDelay<1>(params, state, inputs, outputs, sample_count); + break; + case 2: + ApplyDelay<2>(params, state, inputs, outputs, sample_count); + break; + case 4: + ApplyDelay<4>(params, state, inputs, outputs, sample_count); + break; + case 6: + ApplyDelay<6>(params, state, inputs, outputs, sample_count); + break; + default: + for (u32 channel = 0; channel < params.channel_count; channel++) { + if (inputs[channel].data() != outputs[channel].data()) { + std::memcpy(outputs[channel].data(), inputs[channel].data(), + sample_count * sizeof(s32)); + } + } + break; + } + } else { + for (u32 channel = 0; channel < params.channel_count; channel++) { + if (inputs[channel].data() != outputs[channel].data()) { + std::memcpy(outputs[channel].data(), inputs[channel].data(), + sample_count * sizeof(s32)); + } + } + } +} + +void DelayCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("DelayCommand\n\tenabled {} \n\tinputs: ", effect_enabled); + for (u32 i = 0; i < MaxChannels; i++) { + string += fmt::format("{:02X}, ", inputs[i]); + } + string += "\n\toutputs: "; + for (u32 i = 0; i < MaxChannels; i++) { + string += fmt::format("{:02X}, ", outputs[i]); + } + string += "\n"; +} + +void DelayCommand::Process(const ADSP::CommandListProcessor& processor) { + std::vector> input_buffers(parameter.channel_count); + std::vector> output_buffers(parameter.channel_count); + + for (s16 i = 0; i < parameter.channel_count; i++) { + input_buffers[i] = processor.mix_buffers.subspan(inputs[i] * processor.sample_count, + processor.sample_count); + output_buffers[i] = processor.mix_buffers.subspan(outputs[i] * processor.sample_count, + processor.sample_count); + } + + auto state_{reinterpret_cast(state)}; + + if (effect_enabled) { + if (parameter.state == DelayInfo::ParameterState::Updating) { + SetDelayEffectParameter(parameter, *state_); + } else if (parameter.state == DelayInfo::ParameterState::Initialized) { + InitializeDelayEffect(parameter, *state_, workbuffer); + } + } + ApplyDelayEffect(parameter, *state_, effect_enabled, input_buffers, output_buffers, + processor.sample_count); +} + +bool DelayCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/delay.h b/src/audio_core/renderer/command/effect/delay.h new file mode 100755 index 000000000..b9c8f3e07 --- /dev/null +++ b/src/audio_core/renderer/command/effect/delay.h @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/command/icommand.h" +#include "audio_core/renderer/effect/effect_delay_info.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for a delay effect. Delays inputs mix buffers according to the parameters + * and state, outputs receives the delayed samples. + */ +struct DelayCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer offsets for each channel + std::array inputs; + /// Output mix buffer offsets for each channel + std::array outputs; + /// Input parameters + DelayInfo::ParameterVersion1 parameter; + /// State, updated each call + CpuAddr state; + /// Game-supplied workbuffer (Unused) + CpuAddr workbuffer; + /// Is this effect enabled? + bool effect_enabled; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/i3dl2_reverb.cpp b/src/audio_core/renderer/command/effect/i3dl2_reverb.cpp new file mode 100755 index 000000000..4237e849c --- /dev/null +++ b/src/audio_core/renderer/command/effect/i3dl2_reverb.cpp @@ -0,0 +1,428 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/effect/i3dl2_reverb.h" + +namespace AudioCore::AudioRenderer { + +constexpr std::array MinDelayLineTimes{ + 5.0f, + 6.0f, + 13.0f, + 14.0f, +}; +constexpr std::array MaxDelayLineTimes{ + 45.7042007446f, + 82.7817001343f, + 149.938293457f, + 271.575805664f, +}; +constexpr std::array Decay0MaxDelayLineTimes{17.0f, 13.0f, + 9.0f, 7.0f}; +constexpr std::array Decay1MaxDelayLineTimes{19.0f, 11.0f, + 10.0f, 6.0f}; +constexpr std::array EarlyTapTimes{ + 0.0171360000968f, + 0.0591540001333f, + 0.161733001471f, + 0.390186011791f, + 0.425262004137f, + 0.455410987139f, + 0.689737021923f, + 0.74590998888f, + 0.833844006062f, + 0.859502017498f, + 0.0f, + 0.0750240013003f, + 0.168788000941f, + 0.299901008606f, + 0.337442994118f, + 0.371903002262f, + 0.599011003971f, + 0.716741025448f, + 0.817858994007f, + 0.85166400671f, +}; + +constexpr std::array EarlyGains{ + 0.67096f, 0.61027f, 1.0f, 0.3568f, 0.68361f, 0.65978f, 0.51939f, + 0.24712f, 0.45945f, 0.45021f, 0.64196f, 0.54879f, 0.92925f, 0.3827f, + 0.72867f, 0.69794f, 0.5464f, 0.24563f, 0.45214f, 0.44042f}; + +/** + * Update the I3dl2ReverbInfo state according to the given parameters. + * + * @param params - Input parameters to update the state. + * @param state - State to be updated. + * @param reset - If enabled, the state buffers will be reset. Only set this on initialize. + */ +static void UpdateI3dl2ReverbEffectParameter(const I3dl2ReverbInfo::ParameterVersion1& params, + I3dl2ReverbInfo::State& state, const bool reset) { + const auto pow_10 = [](f32 val) -> f32 { + return (val >= 0.0f) ? 1.0f : (val <= -5.3f) ? 0.0f : std::pow(10.0f, val); + }; + const auto sin = [](f32 degrees) -> f32 { + return std::sin(degrees * std::numbers::pi_v / 180.0f); + }; + const auto cos = [](f32 degrees) -> f32 { + return std::cos(degrees * std::numbers::pi_v / 180.0f); + }; + + Common::FixedPoint<50, 14> delay{static_cast(params.sample_rate) / 1000.0f}; + + Common::FixedPoint<50, 14> early_gain{ + std::min(params.room_gain + params.reflection_gain, 5000.0f) / 2000.0f}; + state.early_gain = pow_10(early_gain.to_float()); + Common::FixedPoint<50, 14> late_gain{std::min(params.room_gain + params.reverb_gain, 5000.0f) / + 2000.0f}; + state.late_gain = pow_10(late_gain.to_float()); + + Common::FixedPoint<50, 14> hf_gain{pow_10(params.room_HF_gain / 2000.0f)}; + if (hf_gain >= 1.0f) { + state.lowpass_1 = 0.0f; + state.lowpass_2 = 1.0f; + } else { + const auto reference_hf{(params.reference_HF * 256.0f) / + static_cast(params.sample_rate)}; + const Common::FixedPoint<50, 14> a{1.0f - hf_gain.to_float()}; + const Common::FixedPoint<50, 14> b{2.0f + (-cos(reference_hf) * (hf_gain * 2.0f))}; + const Common::FixedPoint<50, 14> c{ + std::sqrt(std::pow(b.to_float(), 2.0f) + (std::pow(a.to_float(), 2.0f) * -4.0f))}; + + state.lowpass_1 = std::min(((b - c) / (a * 2.0f)).to_float(), 0.99723f); + state.lowpass_2 = 1.0f - state.lowpass_1; + } + + state.early_to_late_taps = + (((params.reflection_delay + params.late_reverb_delay_time) * 1000.0f) * delay).to_int(); + state.last_reverb_echo = params.late_reverb_diffusion * 0.6f * 0.01f; + + for (u32 i = 0; i < I3dl2ReverbInfo::MaxDelayLines; i++) { + auto curr_delay{ + ((MinDelayLineTimes[i] + (params.late_reverb_density / 100.0f) * + (MaxDelayLineTimes[i] - MinDelayLineTimes[i])) * + delay) + .to_int()}; + state.fdn_delay_lines[i].SetDelay(curr_delay); + + const auto a{ + (static_cast(state.fdn_delay_lines[i].delay + state.decay_delay_lines0[i].delay + + state.decay_delay_lines1[i].delay) * + -60.0f) / + (params.late_reverb_decay_time * static_cast(params.sample_rate))}; + const auto b{a / params.late_reverb_HF_decay_ratio}; + const auto c{ + cos(((params.reference_HF * 0.5f) * 128.0f) / static_cast(params.sample_rate)) / + sin(((params.reference_HF * 0.5f) * 128.0f) / static_cast(params.sample_rate))}; + const auto d{pow_10((b - a) / 40.0f)}; + const auto e{pow_10((b + a) / 40.0f) * 0.7071f}; + + state.lowpass_coeff[i][0] = ((c * d + 1.0f) * e) / (c + d); + state.lowpass_coeff[i][1] = ((1.0f - (c * d)) * e) / (c + d); + state.lowpass_coeff[i][2] = (c - d) / (c + d); + + state.decay_delay_lines0[i].wet_gain = state.last_reverb_echo; + state.decay_delay_lines1[i].wet_gain = state.last_reverb_echo * -0.9f; + } + + if (reset) { + state.shelf_filter.fill(0.0f); + state.lowpass_0 = 0.0f; + for (u32 i = 0; i < I3dl2ReverbInfo::MaxDelayLines; i++) { + std::ranges::fill(state.fdn_delay_lines[i].buffer, 0); + std::ranges::fill(state.decay_delay_lines0[i].buffer, 0); + std::ranges::fill(state.decay_delay_lines1[i].buffer, 0); + } + std::ranges::fill(state.center_delay_line.buffer, 0); + std::ranges::fill(state.early_delay_line.buffer, 0); + } + + const auto reflection_time{(params.late_reverb_delay_time * 0.9998f + 0.02f) * 1000.0f}; + const auto reflection_delay{params.reflection_delay * 1000.0f}; + for (u32 i = 0; i < I3dl2ReverbInfo::MaxDelayTaps; i++) { + auto length{((reflection_delay + reflection_time * EarlyTapTimes[i]) * delay).to_int()}; + if (length >= state.early_delay_line.max_delay) { + length = state.early_delay_line.max_delay; + } + state.early_tap_steps[i] = length; + } +} + +/** + * Initialize a new I3dl2ReverbInfo state according to the given parameters. + * + * @param params - Input parameters to update the state. + * @param state - State to be updated. + * @param workbuffer - Game-supplied memory for the state. (Unused) + */ +static void InitializeI3dl2ReverbEffect(const I3dl2ReverbInfo::ParameterVersion1& params, + I3dl2ReverbInfo::State& state, const CpuAddr workbuffer) { + state = {}; + Common::FixedPoint<50, 14> delay{static_cast(params.sample_rate) / 1000}; + + for (u32 i = 0; i < I3dl2ReverbInfo::MaxDelayLines; i++) { + auto fdn_delay_time{(MaxDelayLineTimes[i] * delay).to_uint_floor()}; + state.fdn_delay_lines[i].Initialize(fdn_delay_time); + + auto decay0_delay_time{(Decay0MaxDelayLineTimes[i] * delay).to_uint_floor()}; + state.decay_delay_lines0[i].Initialize(decay0_delay_time); + + auto decay1_delay_time{(Decay1MaxDelayLineTimes[i] * delay).to_uint_floor()}; + state.decay_delay_lines1[i].Initialize(decay1_delay_time); + } + + const auto center_delay_time{(5 * delay).to_uint_floor()}; + state.center_delay_line.Initialize(center_delay_time); + + const auto early_delay_time{(400 * delay).to_uint_floor()}; + state.early_delay_line.Initialize(early_delay_time); + + UpdateI3dl2ReverbEffectParameter(params, state, true); +} + +/** + * Pass-through the effect, copying input to output directly, with no reverb applied. + * + * @param inputs - Array of input mix buffers to copy. + * @param outputs - Array of output mix buffers to receive copy. + * @param channel_count - Number of channels in inputs and outputs. + * @param sample_count - Number of samples within each channel (unused). + */ +static void ApplyI3dl2ReverbEffectBypass(std::span> inputs, + std::span> outputs, const u32 channel_count, + [[maybe_unused]] const u32 sample_count) { + for (u32 i = 0; i < channel_count; i++) { + if (inputs[i].data() != outputs[i].data()) { + std::memcpy(outputs[i].data(), inputs[i].data(), outputs[i].size_bytes()); + } + } +} + +/** + * Tick the delay lines, reading and returning their current output, and writing a new decaying + * sample (mix). + * + * @param decay0 - The first decay line. + * @param decay1 - The second decay line. + * @param fdn - Feedback delay network. + * @param mix - The new calculated sample to be written and decayed. + * @return The next delayed and decayed sample. + */ +static Common::FixedPoint<50, 14> Axfx2AllPassTick(I3dl2ReverbInfo::I3dl2DelayLine& decay0, + I3dl2ReverbInfo::I3dl2DelayLine& decay1, + I3dl2ReverbInfo::I3dl2DelayLine& fdn, + const Common::FixedPoint<50, 14> mix) { + auto val{decay0.Read()}; + auto mixed{mix - (val * decay0.wet_gain)}; + auto out{decay0.Tick(mixed) + (mixed * decay0.wet_gain)}; + + val = decay1.Read(); + mixed = out - (val * decay1.wet_gain); + out = decay1.Tick(mixed) + (mixed * decay1.wet_gain); + + fdn.Tick(out); + return out; +} + +/** + * Impl. Apply a I3DL2 reverb according to the current state, on the input mix buffers, + * saving the results to the output mix buffers. + * + * @tparam Channels - Number of channels to process. 1-6. + Inputs/outputs should have this many buffers. + * @param state - State to use, must be initialized (see InitializeI3dl2ReverbEffect). + * @param inputs - Input mix buffers to perform the reverb on. + * @param outputs - Output mix buffers to receive the reverbed samples. + * @param sample_count - Number of samples to process. + */ +template +static void ApplyI3dl2ReverbEffect(I3dl2ReverbInfo::State& state, + std::span> inputs, + std::span> outputs, const u32 sample_count) { + constexpr std::array OutTapIndexes1Ch{ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + constexpr std::array OutTapIndexes2Ch{ + 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, + }; + constexpr std::array OutTapIndexes4Ch{ + 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 3, 3, 3, + }; + constexpr std::array OutTapIndexes6Ch{ + 4, 0, 0, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 2, 2, 2, + }; + + std::span tap_indexes{}; + if constexpr (Channels == 1) { + tap_indexes = OutTapIndexes1Ch; + } else if constexpr (Channels == 2) { + tap_indexes = OutTapIndexes2Ch; + } else if constexpr (Channels == 4) { + tap_indexes = OutTapIndexes4Ch; + } else if constexpr (Channels == 6) { + tap_indexes = OutTapIndexes6Ch; + } + + for (u32 i = 0; i < sample_count; i++) { + Common::FixedPoint<50, 14> early_to_late_tap{ + state.early_delay_line.TapOut(state.early_to_late_taps)}; + std::array, Channels> output_samples{}; + + for (u32 early_tap = 0; early_tap < I3dl2ReverbInfo::MaxDelayTaps; early_tap++) { + output_samples[tap_indexes[early_tap]] += + state.early_delay_line.TapOut(state.early_tap_steps[early_tap]) * + EarlyGains[early_tap]; + if constexpr (Channels == 6) { + output_samples[5] += + state.early_delay_line.TapOut(state.early_tap_steps[early_tap]) * + EarlyGains[early_tap]; + } + } + + Common::FixedPoint<50, 14> current_sample{}; + for (u32 channel = 0; channel < Channels; channel++) { + current_sample += inputs[channel][i]; + } + + state.lowpass_0 = + (current_sample * state.lowpass_2 + state.lowpass_0 * state.lowpass_1).to_float(); + state.early_delay_line.Tick(state.lowpass_0); + + for (u32 channel = 0; channel < Channels; channel++) { + output_samples[channel] *= state.early_gain; + } + + std::array, I3dl2ReverbInfo::MaxDelayLines> filtered_samples{}; + for (u32 delay_line = 0; delay_line < I3dl2ReverbInfo::MaxDelayLines; delay_line++) { + filtered_samples[delay_line] = + state.fdn_delay_lines[delay_line].Read() * state.lowpass_coeff[delay_line][0] + + state.shelf_filter[delay_line]; + state.shelf_filter[delay_line] = + (filtered_samples[delay_line] * state.lowpass_coeff[delay_line][2] + + state.fdn_delay_lines[delay_line].Read() * state.lowpass_coeff[delay_line][1]) + .to_float(); + } + + const std::array, I3dl2ReverbInfo::MaxDelayLines> mix_matrix{ + filtered_samples[1] + filtered_samples[2] + early_to_late_tap * state.late_gain, + -filtered_samples[0] - filtered_samples[3] + early_to_late_tap * state.late_gain, + filtered_samples[0] - filtered_samples[3] + early_to_late_tap * state.late_gain, + filtered_samples[1] - filtered_samples[2] + early_to_late_tap * state.late_gain, + }; + + std::array, I3dl2ReverbInfo::MaxDelayLines> allpass_samples{}; + for (u32 delay_line = 0; delay_line < I3dl2ReverbInfo::MaxDelayLines; delay_line++) { + allpass_samples[delay_line] = Axfx2AllPassTick( + state.decay_delay_lines0[delay_line], state.decay_delay_lines1[delay_line], + state.fdn_delay_lines[delay_line], mix_matrix[delay_line]); + } + + const auto out_channels{std::min(Channels, size_t(4))}; + for (u32 channel = 0; channel < out_channels; channel++) { + auto out_sample{output_samples[channel] + allpass_samples[channel] + + state.dry_gain * static_cast(inputs[channel][i])}; + outputs[channel][i] = + static_cast(std::clamp(out_sample.to_float(), -8388600.0f, 8388600.0f)); + } + + if constexpr (Channels == 6) { + auto center{ + state.center_delay_line.Tick((allpass_samples[2] - allpass_samples[3]) * 0.5f)}; + auto out_sample{static_cast(inputs[4][i]) * state.dry_gain + + output_samples[4] * state.early_gain + center}; + outputs[4][i] = + static_cast(std::clamp(out_sample.to_float(), -8388600.0f, 8388600.0f)); + + out_sample = static_cast(inputs[5][i]) * state.dry_gain + + output_samples[5] * state.early_gain + allpass_samples[3]; + outputs[5][i] = + static_cast(std::clamp(out_sample.to_float(), -8388600.0f, 8388600.0f)); + } + } +} + +/** + * Apply a I3DL2 reverb if enabled, according to the current state, on the input mix buffers, + * saving the results to the output mix buffers. + * + * @param params - Input parameters to use. + * @param state - State to use, must be initialized (see InitializeI3dl2ReverbEffect). + * @param enabled - If enabled, delay will be applied, otherwise input is copied to output. + * @param inputs - Input mix buffers to performan the delay on. + * @param outputs - Output mix buffers to receive the delayed samples. + * @param sample_count - Number of samples to process. + */ +static void ApplyI3dl2ReverbEffect(const I3dl2ReverbInfo::ParameterVersion1& params, + I3dl2ReverbInfo::State& state, const bool enabled, + std::span> inputs, + std::span> outputs, const u32 sample_count) { + if (enabled) { + switch (params.channel_count) { + case 0: + return; + case 1: + ApplyI3dl2ReverbEffect<1>(state, inputs, outputs, sample_count); + break; + case 2: + ApplyI3dl2ReverbEffect<2>(state, inputs, outputs, sample_count); + break; + case 4: + ApplyI3dl2ReverbEffect<4>(state, inputs, outputs, sample_count); + break; + case 6: + ApplyI3dl2ReverbEffect<6>(state, inputs, outputs, sample_count); + break; + default: + ApplyI3dl2ReverbEffectBypass(inputs, outputs, params.channel_count, sample_count); + break; + } + } else { + ApplyI3dl2ReverbEffectBypass(inputs, outputs, params.channel_count, sample_count); + } +} + +void I3dl2ReverbCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("I3dl2ReverbCommand\n\tenabled {} \n\tinputs: ", effect_enabled); + for (u32 i = 0; i < parameter.channel_count; i++) { + string += fmt::format("{:02X}, ", inputs[i]); + } + string += "\n\toutputs: "; + for (u32 i = 0; i < parameter.channel_count; i++) { + string += fmt::format("{:02X}, ", outputs[i]); + } + string += "\n"; +} + +void I3dl2ReverbCommand::Process(const ADSP::CommandListProcessor& processor) { + std::vector> input_buffers(parameter.channel_count); + std::vector> output_buffers(parameter.channel_count); + + for (u32 i = 0; i < parameter.channel_count; i++) { + input_buffers[i] = processor.mix_buffers.subspan(inputs[i] * processor.sample_count, + processor.sample_count); + output_buffers[i] = processor.mix_buffers.subspan(outputs[i] * processor.sample_count, + processor.sample_count); + } + + auto state_{reinterpret_cast(state)}; + + if (effect_enabled) { + if (parameter.state == I3dl2ReverbInfo::ParameterState::Updating) { + UpdateI3dl2ReverbEffectParameter(parameter, *state_, false); + } else if (parameter.state == I3dl2ReverbInfo::ParameterState::Initialized) { + InitializeI3dl2ReverbEffect(parameter, *state_, workbuffer); + } + } + ApplyI3dl2ReverbEffect(parameter, *state_, effect_enabled, input_buffers, output_buffers, + processor.sample_count); +} + +bool I3dl2ReverbCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/i3dl2_reverb.h b/src/audio_core/renderer/command/effect/i3dl2_reverb.h new file mode 100755 index 000000000..2836f9b8b --- /dev/null +++ b/src/audio_core/renderer/command/effect/i3dl2_reverb.h @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/command/icommand.h" +#include "audio_core/renderer/effect/effect_i3dl2_info.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for a I3DL2Reverb effect. Apply a reverb to inputs mix buffer according to + * the I3DL2 spec, outputs receives the results. + */ +struct I3dl2ReverbCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer offsets for each channel + std::array inputs; + /// Output mix buffer offsets for each channel + std::array outputs; + /// Input parameters + I3dl2ReverbInfo::ParameterVersion1 parameter; + /// State, updated each call + CpuAddr state; + /// Game-supplied workbuffer (Unused) + CpuAddr workbuffer; + /// Is this effect enabled? + bool effect_enabled; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/light_limiter.cpp b/src/audio_core/renderer/command/effect/light_limiter.cpp new file mode 100755 index 000000000..676dbec59 --- /dev/null +++ b/src/audio_core/renderer/command/effect/light_limiter.cpp @@ -0,0 +1,202 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/effect/light_limiter.h" + +namespace AudioCore::AudioRenderer { +/** + * Update the LightLimiterInfo state according to the given parameters. + * A no-op. + * + * @param params - Input parameters to update the state. + * @param state - State to be updated. + */ +static void UpdateLightLimiterEffectParameter(const LightLimiterInfo::ParameterVersion2& params, + LightLimiterInfo::State& state) {} + +/** + * Initialize a new LightLimiterInfo state according to the given parameters. + * + * @param params - Input parameters to update the state. + * @param state - State to be updated. + * @param workbuffer - Game-supplied memory for the state. (Unused) + */ +static void InitializeLightLimiterEffect(const LightLimiterInfo::ParameterVersion2& params, + LightLimiterInfo::State& state, const CpuAddr workbuffer) { + state = {}; + state.samples_average.fill(0.0f); + state.compression_gain.fill(1.0f); + state.look_ahead_sample_offsets.fill(0); + for (u32 i = 0; i < params.channel_count; i++) { + state.look_ahead_sample_buffers[i].resize(params.look_ahead_samples_max, 0.0f); + } +} + +/** + * Apply a light limiter effect if enabled, according to the current state, on the input mix + * buffers, saving the results to the output mix buffers. + * + * @param params - Input parameters to use. + * @param state - State to use, must be initialized (see InitializeLightLimiterEffect). + * @param enabled - If enabled, limiter will be applied, otherwise input is copied to output. + * @param inputs - Input mix buffers to perform the limiter on. + * @param outputs - Output mix buffers to receive the limited samples. + * @param sample_count - Number of samples to process. + * @params statistics - Optional output statistics, only used with version 2. + */ +static void ApplyLightLimiterEffect(const LightLimiterInfo::ParameterVersion2& params, + LightLimiterInfo::State& state, const bool enabled, + std::vector>& inputs, + std::vector>& outputs, const u32 sample_count, + LightLimiterInfo::StatisticsInternal* statistics) { + constexpr s64 min{std::numeric_limits::min()}; + constexpr s64 max{std::numeric_limits::max()}; + + if (enabled) { + if (statistics && params.statistics_reset_required) { + for (u32 i = 0; i < params.channel_count; i++) { + statistics->channel_compression_gain_min[i] = 1.0f; + statistics->channel_max_sample[i] = 0; + } + } + + for (u32 sample_index = 0; sample_index < sample_count; sample_index++) { + for (u32 channel = 0; channel < params.channel_count; channel++) { + auto sample{Common::FixedPoint<49, 15>(inputs[channel][sample_index]) * + params.input_gain}; + auto abs_sample{sample}; + if (sample < 0.0f) { + abs_sample = -sample; + } + auto coeff{abs_sample > state.samples_average[channel] ? params.attack_coeff + : params.release_coeff}; + state.samples_average[channel] += + ((abs_sample - state.samples_average[channel]) * coeff).to_float(); + + auto above_threshold{state.samples_average[channel] > params.threshold}; + auto attenuation{above_threshold ? params.threshold / state.samples_average[channel] + : 1.0f}; + coeff = attenuation < state.compression_gain[channel] ? params.attack_coeff + : params.release_coeff; + state.compression_gain[channel] += + (attenuation - state.compression_gain[channel]) * coeff; + + auto lookahead_sample{ + state.look_ahead_sample_buffers[channel] + [state.look_ahead_sample_offsets[channel]]}; + state.look_ahead_sample_buffers[channel][state.look_ahead_sample_offsets[channel]] = + sample; + state.look_ahead_sample_offsets[channel] = + (state.look_ahead_sample_offsets[channel] + 1) % params.look_ahead_samples_min; + + outputs[channel][sample_index] = static_cast(std::clamp( + (lookahead_sample * state.compression_gain[channel] * params.output_gain) + .to_long(), + min, max)); + + if (statistics) { + statistics->channel_max_sample[channel] = + std::max(statistics->channel_max_sample[channel], abs_sample.to_float()); + statistics->channel_compression_gain_min[channel] = + std::min(statistics->channel_compression_gain_min[channel], + state.compression_gain[channel].to_float()); + } + } + } + } else { + for (u32 i = 0; i < params.channel_count; i++) { + if (params.inputs[i] != params.outputs[i]) { + std::memcpy(outputs[i].data(), inputs[i].data(), outputs[i].size_bytes()); + } + } + } +} + +void LightLimiterVersion1Command::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("LightLimiterVersion1Command\n\tinputs: "); + for (u32 i = 0; i < MaxChannels; i++) { + string += fmt::format("{:02X}, ", inputs[i]); + } + string += "\n\toutputs: "; + for (u32 i = 0; i < MaxChannels; i++) { + string += fmt::format("{:02X}, ", outputs[i]); + } + string += "\n"; +} + +void LightLimiterVersion1Command::Process(const ADSP::CommandListProcessor& processor) { + std::vector> input_buffers(parameter.channel_count); + std::vector> output_buffers(parameter.channel_count); + + for (u32 i = 0; i < parameter.channel_count; i++) { + input_buffers[i] = processor.mix_buffers.subspan(inputs[i] * processor.sample_count, + processor.sample_count); + output_buffers[i] = processor.mix_buffers.subspan(outputs[i] * processor.sample_count, + processor.sample_count); + } + + auto state_{reinterpret_cast(state)}; + + if (effect_enabled) { + if (parameter.state == LightLimiterInfo::ParameterState::Updating) { + UpdateLightLimiterEffectParameter(parameter, *state_); + } else if (parameter.state == LightLimiterInfo::ParameterState::Initialized) { + InitializeLightLimiterEffect(parameter, *state_, workbuffer); + } + } + + LightLimiterInfo::StatisticsInternal* statistics{nullptr}; + ApplyLightLimiterEffect(parameter, *state_, effect_enabled, input_buffers, output_buffers, + processor.sample_count, statistics); +} + +bool LightLimiterVersion1Command::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +void LightLimiterVersion2Command::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("LightLimiterVersion2Command\n\tinputs: \n"); + for (u32 i = 0; i < MaxChannels; i++) { + string += fmt::format("{:02X}, ", inputs[i]); + } + string += "\n\toutputs: "; + for (u32 i = 0; i < MaxChannels; i++) { + string += fmt::format("{:02X}, ", outputs[i]); + } + string += "\n"; +} + +void LightLimiterVersion2Command::Process(const ADSP::CommandListProcessor& processor) { + auto state_{reinterpret_cast(state)}; + + std::vector> input_buffers(parameter.channel_count); + std::vector> output_buffers(parameter.channel_count); + + for (u32 i = 0; i < parameter.channel_count; i++) { + input_buffers[i] = processor.mix_buffers.subspan(inputs[i] * processor.sample_count, + processor.sample_count); + output_buffers[i] = processor.mix_buffers.subspan(outputs[i] * processor.sample_count, + processor.sample_count); + } + + if (effect_enabled) { + if (parameter.state == LightLimiterInfo::ParameterState::Updating) { + UpdateLightLimiterEffectParameter(parameter, *state_); + } else if (parameter.state == LightLimiterInfo::ParameterState::Initialized) { + InitializeLightLimiterEffect(parameter, *state_, workbuffer); + } + } + + auto statistics{reinterpret_cast(result_state)}; + ApplyLightLimiterEffect(parameter, *state_, effect_enabled, input_buffers, output_buffers, + processor.sample_count, statistics); +} + +bool LightLimiterVersion2Command::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/light_limiter.h b/src/audio_core/renderer/command/effect/light_limiter.h new file mode 100755 index 000000000..48c3849ac --- /dev/null +++ b/src/audio_core/renderer/command/effect/light_limiter.h @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/command/icommand.h" +#include "audio_core/renderer/effect/effect_light_limiter_info.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for limiting volume between a high and low threshold. + * Version 1. + */ +struct LightLimiterVersion1Command : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer offsets for each channel + std::array inputs; + /// Output mix buffer offsets for each channel + std::array outputs; + /// Input parameters + LightLimiterInfo::ParameterVersion2 parameter; + /// State, updated each call + CpuAddr state; + /// Game-supplied workbuffer (Unused) + CpuAddr workbuffer; + /// Is this effect enabled? + bool effect_enabled; +}; + +/** + * AudioRenderer command for limiting volume between a high and low threshold. + * Version 2 with output statistics. + */ +struct LightLimiterVersion2Command : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer offsets for each channel + std::array inputs; + /// Output mix buffer offsets for each channel + std::array outputs; + /// Input parameters + LightLimiterInfo::ParameterVersion2 parameter; + /// State, updated each call + CpuAddr state; + /// Game-supplied workbuffer (Unused) + CpuAddr workbuffer; + /// Optional statistics, sent back to the sysmodule + CpuAddr result_state; + /// Is this effect enabled? + bool effect_enabled; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/multi_tap_biquad_filter.cpp b/src/audio_core/renderer/command/effect/multi_tap_biquad_filter.cpp new file mode 100755 index 000000000..b3c3ba4ba --- /dev/null +++ b/src/audio_core/renderer/command/effect/multi_tap_biquad_filter.cpp @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/effect/biquad_filter.h" +#include "audio_core/renderer/command/effect/multi_tap_biquad_filter.h" + +namespace AudioCore::AudioRenderer { + +void MultiTapBiquadFilterCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format( + "MultiTapBiquadFilterCommand\n\tinput {:02X}\n\toutput {:02X}\n\tneeds_init ({}, {})\n", + input, output, needs_init[0], needs_init[1]); +} + +void MultiTapBiquadFilterCommand::Process(const ADSP::CommandListProcessor& processor) { + if (filter_tap_count > MaxBiquadFilters) { + LOG_ERROR(Service_Audio, "Too many filter taps! {}", filter_tap_count); + filter_tap_count = MaxBiquadFilters; + } + + auto input_buffer{ + processor.mix_buffers.subspan(input * processor.sample_count, processor.sample_count)}; + auto output_buffer{ + processor.mix_buffers.subspan(output * processor.sample_count, processor.sample_count)}; + + // TODO: Fix this, currently just applies the filter to the input twice, + // and doesn't chain the biquads together at all. + for (u32 i = 0; i < filter_tap_count; i++) { + auto state{reinterpret_cast(states[i])}; + if (needs_init[i]) { + std::memset(state, 0, sizeof(VoiceState::BiquadFilterState)); + } + + ApplyBiquadFilterFloat(output_buffer, input_buffer, biquads[i].b, biquads[i].a, *state, + processor.sample_count); + } +} + +bool MultiTapBiquadFilterCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/multi_tap_biquad_filter.h b/src/audio_core/renderer/command/effect/multi_tap_biquad_filter.h new file mode 100755 index 000000000..99c2c0830 --- /dev/null +++ b/src/audio_core/renderer/command/effect/multi_tap_biquad_filter.h @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/command/icommand.h" +#include "audio_core/renderer/voice/voice_info.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for applying multiple biquads at once. + */ +struct MultiTapBiquadFilterCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer index + s16 input; + /// Output mix buffer index + s16 output; + /// Biquad parameters + std::array biquads; + /// Biquad states, updated each call + std::array states; + /// If each biquad needs initialisation + std::array needs_init; + /// Number of active biquads + u8 filter_tap_count; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/reverb.cpp b/src/audio_core/renderer/command/effect/reverb.cpp new file mode 100755 index 000000000..b9b4f43b3 --- /dev/null +++ b/src/audio_core/renderer/command/effect/reverb.cpp @@ -0,0 +1,433 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/effect/reverb.h" + +namespace AudioCore::AudioRenderer { + +constexpr std::array FdnMaxDelayLineTimes = { + 53.9532470703125f, + 79.19256591796875f, + 116.23876953125f, + 170.61529541015625f, +}; + +constexpr std::array DecayMaxDelayLineTimes = { + 7.0f, + 9.0f, + 13.0f, + 17.0f, +}; + +constexpr std::array, ReverbInfo::NumEarlyModes> + EarlyDelayTimes = { + {{0.000000f, 3.500000f, 2.799988f, 3.899963f, 2.699951f, 13.399963f, 7.899963f, 8.399963f, + 9.899963f, 12.000000f, 12.500000f}, + {0.000000f, 11.799988f, 5.500000f, 11.199951f, 10.399963f, 38.099976f, 22.199951f, + 29.599976f, 21.199951f, 24.799988f, 40.000000f}, + {0.000000f, 41.500000f, 20.500000f, 41.299988f, 0.000000f, 29.500000f, 33.799988f, + 45.199951f, 46.799988f, 0.000000f, 50.000000f}, + {33.099976f, 43.299988f, 22.799988f, 37.899963f, 14.899963f, 35.299988f, 17.899963f, + 34.199951f, 0.000000f, 43.299988f, 50.000000f}, + {0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, + 0.000000f, 0.000000f, 0.000000f}}, +}; + +constexpr std::array, ReverbInfo::NumEarlyModes> + EarlyDelayGains = {{ + {0.699951f, 0.679993f, 0.699951f, 0.679993f, 0.699951f, 0.679993f, 0.699951f, 0.679993f, + 0.679993f, 0.679993f}, + {0.699951f, 0.679993f, 0.699951f, 0.679993f, 0.699951f, 0.679993f, 0.679993f, 0.679993f, + 0.679993f, 0.679993f}, + {0.500000f, 0.699951f, 0.699951f, 0.679993f, 0.500000f, 0.679993f, 0.679993f, 0.699951f, + 0.679993f, 0.000000f}, + {0.929993f, 0.919983f, 0.869995f, 0.859985f, 0.939941f, 0.809998f, 0.799988f, 0.769958f, + 0.759949f, 0.649963f}, + {0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, + 0.000000f, 0.000000f}, + }}; + +constexpr std::array, ReverbInfo::NumLateModes> + FdnDelayTimes = {{ + {53.953247f, 79.192566f, 116.238770f, 130.615295f}, + {53.953247f, 79.192566f, 116.238770f, 170.615295f}, + {5.000000f, 10.000000f, 5.000000f, 10.000000f}, + {47.029968f, 71.000000f, 103.000000f, 170.000000f}, + {53.953247f, 79.192566f, 116.238770f, 170.615295f}, + }}; + +constexpr std::array, ReverbInfo::NumLateModes> + DecayDelayTimes = {{ + {7.000000f, 9.000000f, 13.000000f, 17.000000f}, + {7.000000f, 9.000000f, 13.000000f, 17.000000f}, + {1.000000f, 1.000000f, 1.000000f, 1.000000f}, + {7.000000f, 7.000000f, 13.000000f, 9.000000f}, + {7.000000f, 9.000000f, 13.000000f, 17.000000f}, + }}; + +/** + * Update the ReverbInfo state according to the given parameters. + * + * @param params - Input parameters to update the state. + * @param state - State to be updated. + */ +static void UpdateReverbEffectParameter(const ReverbInfo::ParameterVersion2& params, + ReverbInfo::State& state) { + const auto pow_10 = [](f32 val) -> f32 { + return (val >= 0.0f) ? 1.0f : (val <= -5.3f) ? 0.0f : std::pow(10.0f, val); + }; + const auto cos = [](f32 degrees) -> f32 { + return std::cos(degrees * std::numbers::pi_v / 180.0f); + }; + + static bool unk_initialized{false}; + static Common::FixedPoint<50, 14> unk_value{}; + + const auto sample_rate{Common::FixedPoint<50, 14>::from_base(params.sample_rate)}; + const auto pre_delay_time{Common::FixedPoint<50, 14>::from_base(params.pre_delay)}; + + for (u32 i = 0; i < ReverbInfo::MaxDelayTaps; i++) { + auto early_delay{ + ((pre_delay_time + EarlyDelayTimes[params.early_mode][i]) * sample_rate).to_int()}; + early_delay = std::min(early_delay, state.pre_delay_line.sample_count_max); + state.early_delay_times[i] = early_delay + 1; + state.early_gains[i] = Common::FixedPoint<50, 14>::from_base(params.early_gain) * + EarlyDelayGains[params.early_mode][i]; + } + + if (params.channel_count == 2) { + state.early_gains[4] * 0.5f; + state.early_gains[5] * 0.5f; + } + + auto pre_time{ + ((pre_delay_time + EarlyDelayTimes[params.early_mode][10]) * sample_rate).to_int()}; + state.pre_delay_time = std::min(pre_time, state.pre_delay_line.sample_count_max); + + if (!unk_initialized) { + unk_value = cos((1280.0f / sample_rate).to_float()); + unk_initialized = true; + } + + for (u32 i = 0; i < ReverbInfo::MaxDelayLines; i++) { + const auto fdn_delay{(FdnDelayTimes[params.late_mode][i] * sample_rate).to_int()}; + state.fdn_delay_lines[i].sample_count = + std::min(fdn_delay, state.fdn_delay_lines[i].sample_count_max); + state.fdn_delay_lines[i].buffer_end = + &state.fdn_delay_lines[i].buffer[state.fdn_delay_lines[i].sample_count - 1]; + + const auto decay_delay{(DecayDelayTimes[params.late_mode][i] * sample_rate).to_int()}; + state.decay_delay_lines[i].sample_count = + std::min(decay_delay, state.decay_delay_lines[i].sample_count_max); + state.decay_delay_lines[i].buffer_end = + &state.decay_delay_lines[i].buffer[state.decay_delay_lines[i].sample_count - 1]; + + state.decay_delay_lines[i].decay = + 0.5999755859375f * (1.0f - Common::FixedPoint<50, 14>::from_base(params.colouration)); + + auto a{(Common::FixedPoint<50, 14>(state.fdn_delay_lines[i].sample_count_max) + + state.decay_delay_lines[i].sample_count_max) * + -3}; + auto b{a / (Common::FixedPoint<50, 14>::from_base(params.decay_time) * sample_rate)}; + Common::FixedPoint<50, 14> c{0.0f}; + Common::FixedPoint<50, 14> d{0.0f}; + auto hf_decay_ratio{Common::FixedPoint<50, 14>::from_base(params.high_freq_decay_ratio)}; + + if (hf_decay_ratio > 0.99493408203125f) { + c = 0.0f; + d = 1.0f; + } else { + const auto e{ + pow_10(((((1.0f / hf_decay_ratio) - 1.0f) * 2) / 100 * (b / 10)).to_float())}; + const auto f{1.0f - e}; + const auto g{2.0f - (unk_value * e * 2)}; + const auto h{std::sqrt(std::pow(g.to_float(), 2.0f) - (std::pow(f, 2.0f) * 4))}; + + c = (g - h) / (f * 2.0f); + d = 1.0f - c; + } + + state.hf_decay_prev_gain[i] = c; + state.hf_decay_gain[i] = pow_10((b / 1000).to_float()) * d * 0.70709228515625f; + state.prev_feedback_output[i] = 0; + } +} + +/** + * Initialize a new ReverbInfo state according to the given parameters. + * + * @param params - Input parameters to update the state. + * @param state - State to be updated. + * @param workbuffer - Game-supplied memory for the state. (Unused) + * @param long_size_pre_delay_supported - Use a longer pre-delay time before reverb begins. + */ +static void InitializeReverbEffect(const ReverbInfo::ParameterVersion2& params, + ReverbInfo::State& state, const CpuAddr workbuffer, + const bool long_size_pre_delay_supported) { + state = {}; + + auto delay{Common::FixedPoint<50, 14>::from_base(params.sample_rate)}; + + for (u32 i = 0; i < ReverbInfo::MaxDelayLines; i++) { + auto fdn_delay_time{(FdnMaxDelayLineTimes[i] * delay).to_uint_floor()}; + state.fdn_delay_lines[i].Initialize(fdn_delay_time, 1.0f); + + auto decay_delay_time{(DecayMaxDelayLineTimes[i] * delay).to_uint_floor()}; + state.decay_delay_lines[i].Initialize(decay_delay_time, 0.0f); + } + + const auto pre_delay{long_size_pre_delay_supported ? 350.0f : 150.0f}; + const auto pre_delay_line{(pre_delay * delay).to_uint_floor()}; + state.pre_delay_line.Initialize(pre_delay_line, 1.0f); + + const auto center_delay_time{(5 * delay).to_uint_floor()}; + state.center_delay_line.Initialize(center_delay_time, 1.0f); + + UpdateReverbEffectParameter(params, state); + + for (u32 i = 0; i < ReverbInfo::MaxDelayLines; i++) { + std::ranges::fill(state.fdn_delay_lines[i].buffer, 0); + std::ranges::fill(state.decay_delay_lines[i].buffer, 0); + } + std::ranges::fill(state.center_delay_line.buffer, 0); + std::ranges::fill(state.pre_delay_line.buffer, 0); +} + +/** + * Pass-through the effect, copying input to output directly, with no reverb applied. + * + * @param inputs - Array of input mix buffers to copy. + * @param outputs - Array of output mix buffers to receive copy. + * @param channel_count - Number of channels in inputs and outputs. + * @param sample_count - Number of samples within each channel. + */ +static void ApplyReverbEffectBypass(std::span> inputs, + std::span> outputs, const u32 channel_count, + const u32 sample_count) { + for (u32 i = 0; i < channel_count; i++) { + if (inputs[i].data() != outputs[i].data()) { + std::memcpy(outputs[i].data(), inputs[i].data(), outputs[i].size_bytes()); + } + } +} + +/** + * Tick the delay lines, reading and returning their current output, and writing a new decaying + * sample (mix). + * + * @param decay - The decay line. + * @param fdn - Feedback delay network. + * @param mix - The new calculated sample to be written and decayed. + * @return The next delayed and decayed sample. + */ +static Common::FixedPoint<50, 14> Axfx2AllPassTick(ReverbInfo::ReverbDelayLine& decay, + ReverbInfo::ReverbDelayLine& fdn, + const Common::FixedPoint<50, 14> mix) { + const auto val{decay.Read()}; + const auto mixed{mix - (val * decay.decay)}; + const auto out{decay.Tick(mixed) + (mixed * decay.decay)}; + + fdn.Tick(out); + return out; +} + +/** + * Impl. Apply a Reverb according to the current state, on the input mix buffers, + * saving the results to the output mix buffers. + * + * @tparam Channels - Number of channels to process. 1-6. + Inputs/outputs should have this many buffers. + * @param params - Input parameters to update the state. + * @param state - State to use, must be initialized (see InitializeReverbEffect). + * @param inputs - Input mix buffers to perform the reverb on. + * @param outputs - Output mix buffers to receive the reverbed samples. + * @param sample_count - Number of samples to process. + */ +template +static void ApplyReverbEffect(const ReverbInfo::ParameterVersion2& params, ReverbInfo::State& state, + std::vector>& inputs, + std::vector>& outputs, const u32 sample_count) { + constexpr std::array OutTapIndexes1Ch{ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + constexpr std::array OutTapIndexes2Ch{ + 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, + }; + constexpr std::array OutTapIndexes4Ch{ + 0, 0, 1, 1, 0, 1, 2, 2, 3, 3, + }; + constexpr std::array OutTapIndexes6Ch{ + 0, 0, 1, 1, 4, 4, 2, 2, 3, 3, + }; + + std::span tap_indexes{}; + if constexpr (Channels == 1) { + tap_indexes = OutTapIndexes1Ch; + } else if constexpr (Channels == 2) { + tap_indexes = OutTapIndexes2Ch; + } else if constexpr (Channels == 4) { + tap_indexes = OutTapIndexes4Ch; + } else if constexpr (Channels == 6) { + tap_indexes = OutTapIndexes6Ch; + } + + for (u32 sample_index = 0; sample_index < sample_count; sample_index++) { + std::array, Channels> output_samples{}; + + for (u32 early_tap = 0; early_tap < ReverbInfo::MaxDelayTaps; early_tap++) { + const auto sample{state.pre_delay_line.TapOut(state.early_delay_times[early_tap]) * + state.early_gains[early_tap]}; + output_samples[tap_indexes[early_tap]] += sample; + if constexpr (Channels == 6) { + output_samples[5] += sample; + } + } + + if constexpr (Channels == 6) { + output_samples[5] *= 0.2f; + } + + Common::FixedPoint<50, 14> input_sample{}; + for (u32 channel = 0; channel < Channels; channel++) { + input_sample += inputs[channel][sample_index]; + } + + input_sample *= 64; + input_sample *= Common::FixedPoint<50, 14>::from_base(params.base_gain); + state.pre_delay_line.Write(input_sample); + + for (u32 i = 0; i < ReverbInfo::MaxDelayLines; i++) { + state.prev_feedback_output[i] = + state.prev_feedback_output[i] * state.hf_decay_prev_gain[i] + + state.fdn_delay_lines[i].Read() * state.hf_decay_gain[i]; + } + + Common::FixedPoint<50, 14> pre_delay_sample{ + state.pre_delay_line.Read() * Common::FixedPoint<50, 14>::from_base(params.late_gain)}; + + std::array, ReverbInfo::MaxDelayLines> mix_matrix{ + state.prev_feedback_output[2] + state.prev_feedback_output[1] + pre_delay_sample, + -state.prev_feedback_output[0] - state.prev_feedback_output[3] + pre_delay_sample, + state.prev_feedback_output[0] - state.prev_feedback_output[3] + pre_delay_sample, + state.prev_feedback_output[1] - state.prev_feedback_output[2] + pre_delay_sample, + }; + + std::array, ReverbInfo::MaxDelayLines> out_line_samples{}; + for (u32 i = 0; i < ReverbInfo::MaxDelayLines; i++) { + out_line_samples[i] = Axfx2AllPassTick(state.decay_delay_lines[i], + state.fdn_delay_lines[i], mix_matrix[i]); + } + const auto dry_gain{Common::FixedPoint<50, 14>::from_base(params.dry_gain)}; + const auto wet_gain{Common::FixedPoint<50, 14>::from_base(params.wet_gain)}; + + const auto out_channels{std::min(Channels, size_t(4))}; + for (u32 channel = 0; channel < out_channels; channel++) { + auto in_sample{inputs[channel][channel] * dry_gain}; + auto out_sample{((output_samples[channel] + out_line_samples[channel]) * wet_gain) / + 64}; + outputs[channel][sample_index] = (in_sample + out_sample).to_int(); + } + + if constexpr (Channels == 6) { + auto center{ + state.center_delay_line.Tick((out_line_samples[2] - out_line_samples[3]) * 0.5f)}; + auto in_sample{inputs[4][sample_index] * dry_gain}; + auto out_sample{((output_samples[4] + center) * wet_gain) / 64}; + + outputs[4][sample_index] = (in_sample + out_sample).to_int(); + + in_sample = inputs[5][sample_index] * dry_gain; + out_sample = ((output_samples[5] + out_line_samples[3]) * wet_gain) / 64; + + outputs[5][sample_index] = (in_sample + out_sample).to_int(); + } + } +} + +/** + * Apply a Reverb if enabled, according to the current state, on the input mix buffers, + * saving the results to the output mix buffers. + * + * @param params - Input parameters to use. + * @param state - State to use, must be initialized (see InitializeReverbEffect). + * @param enabled - If enabled, delay will be applied, otherwise input is copied to output. + * @param inputs - Input mix buffers to performan the reverb on. + * @param outputs - Output mix buffers to receive the reverbed samples. + * @param sample_count - Number of samples to process. + */ +static void ApplyReverbEffect(const ReverbInfo::ParameterVersion2& params, ReverbInfo::State& state, + const bool enabled, std::vector>& inputs, + std::vector>& outputs, const u32 sample_count) { + if (enabled) { + switch (params.channel_count) { + case 0: + return; + case 1: + ApplyReverbEffect<1>(params, state, inputs, outputs, sample_count); + break; + case 2: + ApplyReverbEffect<2>(params, state, inputs, outputs, sample_count); + break; + case 4: + ApplyReverbEffect<4>(params, state, inputs, outputs, sample_count); + break; + case 6: + ApplyReverbEffect<6>(params, state, inputs, outputs, sample_count); + break; + default: + ApplyReverbEffectBypass(inputs, outputs, params.channel_count, sample_count); + break; + } + } else { + ApplyReverbEffectBypass(inputs, outputs, params.channel_count, sample_count); + } +} + +void ReverbCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format( + "ReverbCommand\n\tenabled {} long_size_pre_delay_supported {}\n\tinputs: ", effect_enabled, + long_size_pre_delay_supported); + for (u32 i = 0; i < MaxChannels; i++) { + string += fmt::format("{:02X}, ", inputs[i]); + } + string += "\n\toutputs: "; + for (u32 i = 0; i < MaxChannels; i++) { + string += fmt::format("{:02X}, ", outputs[i]); + } + string += "\n"; +} + +void ReverbCommand::Process(const ADSP::CommandListProcessor& processor) { + std::vector> input_buffers(parameter.channel_count); + std::vector> output_buffers(parameter.channel_count); + + for (u32 i = 0; i < parameter.channel_count; i++) { + input_buffers[i] = processor.mix_buffers.subspan(inputs[i] * processor.sample_count, + processor.sample_count); + output_buffers[i] = processor.mix_buffers.subspan(outputs[i] * processor.sample_count, + processor.sample_count); + } + + auto state_{reinterpret_cast(state)}; + + if (effect_enabled) { + if (parameter.state == ReverbInfo::ParameterState::Updating) { + UpdateReverbEffectParameter(parameter, *state_); + } else if (parameter.state == ReverbInfo::ParameterState::Initialized) { + InitializeReverbEffect(parameter, *state_, workbuffer, long_size_pre_delay_supported); + } + } + ApplyReverbEffect(parameter, *state_, effect_enabled, input_buffers, output_buffers, + processor.sample_count); +} + +bool ReverbCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/reverb.h b/src/audio_core/renderer/command/effect/reverb.h new file mode 100755 index 000000000..d9c8f894d --- /dev/null +++ b/src/audio_core/renderer/command/effect/reverb.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/command/icommand.h" +#include "audio_core/renderer/effect/effect_reverb_info.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for a Reverb effect. Apply a reverb to inputs mix buffer, outputs receives + * the results. + */ +struct ReverbCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer offsets for each channel + std::array inputs; + /// Output mix buffer offsets for each channel + std::array outputs; + /// Input parameters + ReverbInfo::ParameterVersion2 parameter; + /// State, updated each call + CpuAddr state; + /// Game-supplied workbuffer (Unused) + CpuAddr workbuffer; + /// Is this effect enabled? + bool effect_enabled; + /// Is a longer pre-delay time supported? + bool long_size_pre_delay_supported; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/icommand.h b/src/audio_core/renderer/command/icommand.h new file mode 100755 index 000000000..fdbb99f5e --- /dev/null +++ b/src/audio_core/renderer/command/icommand.h @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/common/common.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +enum class CommandId : u8 { + /* 0x00 */ Invalid, + /* 0x01 */ DataSourcePcmInt16Version1, + /* 0x02 */ DataSourcePcmInt16Version2, + /* 0x03 */ DataSourcePcmFloatVersion1, + /* 0x04 */ DataSourcePcmFloatVersion2, + /* 0x05 */ DataSourceAdpcmVersion1, + /* 0x06 */ DataSourceAdpcmVersion2, + /* 0x07 */ Volume, + /* 0x08 */ VolumeRamp, + /* 0x09 */ BiquadFilter, + /* 0x0A */ Mix, + /* 0x0B */ MixRamp, + /* 0x0C */ MixRampGrouped, + /* 0x0D */ DepopPrepare, + /* 0x0E */ DepopForMixBuffers, + /* 0x0F */ Delay, + /* 0x10 */ Upsample, + /* 0x11 */ DownMix6chTo2ch, + /* 0x12 */ Aux, + /* 0x13 */ DeviceSink, + /* 0x14 */ CircularBufferSink, + /* 0x15 */ Reverb, + /* 0x16 */ I3dl2Reverb, + /* 0x17 */ Performance, + /* 0x18 */ ClearMixBuffer, + /* 0x19 */ CopyMixBuffer, + /* 0x1A */ LightLimiterVersion1, + /* 0x1B */ LightLimiterVersion2, + /* 0x1C */ MultiTapBiquadFilter, + /* 0x1D */ Capture, +}; + +constexpr u32 CommandMagic{0xCAFEBABE}; + +/** + * A command, generated by the host, and processed by the ADSP's AudioRenderer. + */ +struct ICommand { + virtual ~ICommand() = default; + + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + virtual void Dump(const ADSP::CommandListProcessor& processor, std::string& string) = 0; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + virtual void Process(const ADSP::CommandListProcessor& processor) = 0; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + virtual bool Verify(const ADSP::CommandListProcessor& processor) = 0; + + /// Command magic 0xCAFEBABE + u32 magic{}; + /// Command enabled + bool enabled{}; + /// Type of this command (see CommandId) + CommandId type{}; + /// Size of this command + s16 size{}; + /// Estimated processing time for this command + u32 estimated_process_time{}; + /// Node id of the voice or mix this command was generated from + u32 node_id{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/clear_mix.cpp b/src/audio_core/renderer/command/mix/clear_mix.cpp new file mode 100755 index 000000000..4f649d6a8 --- /dev/null +++ b/src/audio_core/renderer/command/mix/clear_mix.cpp @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/mix/clear_mix.h" + +namespace AudioCore::AudioRenderer { + +void ClearMixBufferCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("ClearMixBufferCommand\n"); +} + +void ClearMixBufferCommand::Process(const ADSP::CommandListProcessor& processor) { + memset(processor.mix_buffers.data(), 0, processor.mix_buffers.size_bytes()); +} + +bool ClearMixBufferCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/clear_mix.h b/src/audio_core/renderer/command/mix/clear_mix.h new file mode 100755 index 000000000..956ec0b65 --- /dev/null +++ b/src/audio_core/renderer/command/mix/clear_mix.h @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for a clearing the mix buffers. + * Used at the start of each command list. + */ +struct ClearMixBufferCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/copy_mix.cpp b/src/audio_core/renderer/command/mix/copy_mix.cpp new file mode 100755 index 000000000..1d49f1644 --- /dev/null +++ b/src/audio_core/renderer/command/mix/copy_mix.cpp @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/mix/copy_mix.h" + +namespace AudioCore::AudioRenderer { + +void CopyMixBufferCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("CopyMixBufferCommand\n\tinput {:02X} output {:02X}\n", input_index, + output_index); +} + +void CopyMixBufferCommand::Process(const ADSP::CommandListProcessor& processor) { + auto output{processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count)}; + auto input{processor.mix_buffers.subspan(input_index * processor.sample_count, + processor.sample_count)}; + std::memcpy(output.data(), input.data(), processor.sample_count * sizeof(s32)); +} + +bool CopyMixBufferCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/copy_mix.h b/src/audio_core/renderer/command/mix/copy_mix.h new file mode 100755 index 000000000..a59007fb6 --- /dev/null +++ b/src/audio_core/renderer/command/mix/copy_mix.h @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for a copying a mix buffer from input to output. + */ +struct CopyMixBufferCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer index + s16 input_index; + /// Output mix buffer index + s16 output_index; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/depop_for_mix_buffers.cpp b/src/audio_core/renderer/command/mix/depop_for_mix_buffers.cpp new file mode 100755 index 000000000..d68227dc5 --- /dev/null +++ b/src/audio_core/renderer/command/mix/depop_for_mix_buffers.cpp @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/common/common.h" +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/mix/depop_for_mix_buffers.h" + +namespace AudioCore::AudioRenderer { +/** + * Apply depopping. Add the depopped sample to each incoming new sample, decaying it each time + * according to decay. + * + * @param output - Output buffer to be depopped. + * @param depop_sample - Depopped sample to apply to output samples. + * @param decay_ - Amount to decay the depopped sample for every output sample. + * @param sample_count - Samples to process. + * @return Final decayed depop sample. + */ +static s32 ApplyDepopMix(std::span output, const s32 depop_sample, + Common::FixedPoint<49, 15>& decay_, const u32 sample_count) { + auto sample{std::abs(depop_sample)}; + auto decay{decay_.to_raw()}; + + if (depop_sample <= 0) { + for (u32 i = 0; i < sample_count; i++) { + sample = static_cast((static_cast(sample) * decay) >> 15); + output[i] -= sample; + } + return -sample; + } else { + for (u32 i = 0; i < sample_count; i++) { + sample = static_cast((static_cast(sample) * decay) >> 15); + output[i] += sample; + } + return sample; + } +} + +void DepopForMixBuffersCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("DepopForMixBuffersCommand\n\tinput {:02X} count {} decay {}\n", input, + count, decay.to_float()); +} + +void DepopForMixBuffersCommand::Process(const ADSP::CommandListProcessor& processor) { + std::span depop_buff{reinterpret_cast(depop_buffer), MaxMixBuffers}; + auto end_index{std::min(processor.buffer_count, input + count)}; + for (u32 index = input; index < end_index; index++) { + const auto depop_sample{depop_buff[index]}; + if (depop_sample != 0) { + auto input_buffer{processor.mix_buffers.subspan(index * processor.sample_count, + processor.sample_count)}; + depop_buff[index] = + ApplyDepopMix(input_buffer, depop_sample, decay, processor.sample_count); + } + } +} + +bool DepopForMixBuffersCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/depop_for_mix_buffers.h b/src/audio_core/renderer/command/mix/depop_for_mix_buffers.h new file mode 100755 index 000000000..e7268ff27 --- /dev/null +++ b/src/audio_core/renderer/command/mix/depop_for_mix_buffers.h @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for depopping a mix buffer. + * Adds a cumulation of previous samples to the current mix buffer with a decay. + */ +struct DepopForMixBuffersCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Starting input mix buffer index + u32 input; + /// Number of mix buffers to depop + u32 count; + /// Amount to decay the depop sample for each new sample + Common::FixedPoint<49, 15> decay; + /// Address of the depop buffer, holding the last sample for every mix buffer + CpuAddr depop_buffer; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/depop_prepare.cpp b/src/audio_core/renderer/command/mix/depop_prepare.cpp new file mode 100755 index 000000000..2ee076ef6 --- /dev/null +++ b/src/audio_core/renderer/command/mix/depop_prepare.cpp @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/mix/depop_prepare.h" +#include "audio_core/renderer/voice/voice_state.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { + +void DepopPrepareCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("DepopPrepareCommand\n\tinputs: "); + for (u32 i = 0; i < buffer_count; i++) { + string += fmt::format("{:02X}, ", inputs[i]); + } + string += "\n"; +} + +void DepopPrepareCommand::Process(const ADSP::CommandListProcessor& processor) { + auto samples{reinterpret_cast(previous_samples)}; + auto buffer{std::span(reinterpret_cast(depop_buffer), buffer_count)}; + + for (u32 i = 0; i < buffer_count; i++) { + if (samples[i]) { + buffer[inputs[i]] += samples[i]; + samples[i] = 0; + } + } +} + +bool DepopPrepareCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/depop_prepare.h b/src/audio_core/renderer/command/mix/depop_prepare.h new file mode 100755 index 000000000..a5465da9a --- /dev/null +++ b/src/audio_core/renderer/command/mix/depop_prepare.h @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for preparing depop. + * Adds the previusly output last samples to the depop buffer. + */ +struct DepopPrepareCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Depop buffer offset for each mix buffer + std::array inputs; + /// Pointer to the previous mix buffer samples + CpuAddr previous_samples; + /// Number of mix buffers to use + u32 buffer_count; + /// Pointer to the current depop values + CpuAddr depop_buffer; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/mix.cpp b/src/audio_core/renderer/command/mix/mix.cpp new file mode 100755 index 000000000..8ecf9b05a --- /dev/null +++ b/src/audio_core/renderer/command/mix/mix.cpp @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/mix/mix.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { +/** + * Mix input mix buffer into output mix buffer, with volume applied to the input. + * + * @tparam Q - Number of bits for fixed point operations. + * @param output - Output mix buffer. + * @param input - Input mix buffer. + * @param volume - Volume applied to the input. + * @param sample_count - Number of samples to process. + */ +template +static void ApplyMix(std::span output, std::span input, const f32 volume_, + const u32 sample_count) { + const Common::FixedPoint<64 - Q, Q> volume{volume_}; + for (u32 i = 0; i < sample_count; i++) { + output[i] = (output[i] + input[i] * volume).to_int(); + } +} + +void MixCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("MixCommand"); + string += fmt::format("\n\tinput {:02X}", input_index); + string += fmt::format("\n\toutput {:02X}", output_index); + string += fmt::format("\n\tvolume {:.8f}", volume); + string += "\n"; +} + +void MixCommand::Process(const ADSP::CommandListProcessor& processor) { + auto output{processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count)}; + auto input{processor.mix_buffers.subspan(input_index * processor.sample_count, + processor.sample_count)}; + + // If volume is 0, nothing will be added to the output, so just skip. + if (volume == 0.0f) { + return; + } + + switch (precision) { + case 15: + ApplyMix<15>(output, input, volume, processor.sample_count); + break; + + case 23: + ApplyMix<23>(output, input, volume, processor.sample_count); + break; + + default: + LOG_ERROR(Service_Audio, "Invalid precision {}", precision); + break; + } +} + +bool MixCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/mix.h b/src/audio_core/renderer/command/mix/mix.h new file mode 100755 index 000000000..0201cf171 --- /dev/null +++ b/src/audio_core/renderer/command/mix/mix.h @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for mixing an input mix buffer to an output mix buffer, with a volume + * applied to the input. + */ +struct MixCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Fixed point precision + u8 precision; + /// Input mix buffer index + s16 input_index; + /// Output mix buffer index + s16 output_index; + /// Mix volume applied to the input + f32 volume; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/mix_ramp.cpp b/src/audio_core/renderer/command/mix/mix_ramp.cpp new file mode 100755 index 000000000..ffdafa1c8 --- /dev/null +++ b/src/audio_core/renderer/command/mix/mix_ramp.cpp @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/mix/mix_ramp.h" +#include "common/fixed_point.h" +#include "common/logging/log.h" + +namespace AudioCore::AudioRenderer { +/** + * Mix input mix buffer into output mix buffer, with volume applied to the input. + * + * @tparam Q - Number of bits for fixed point operations. + * @param output - Output mix buffer. + * @param input - Input mix buffer. + * @param volume - Volume applied to the input. + * @param ramp - Ramp applied to volume every sample. + * @param sample_count - Number of samples to process. + * @return The final gained input sample, used for depopping. + */ +template +s32 ApplyMixRamp(std::span output, std::span input, const f32 volume_, + const f32 ramp_, const u32 sample_count) { + Common::FixedPoint<64 - Q, Q> volume{volume_}; + Common::FixedPoint<64 - Q, Q> sample{0}; + + if (ramp_ == 0.0f) { + for (u32 i = 0; i < sample_count; i++) { + sample = input[i] * volume; + output[i] = (output[i] + sample).to_int(); + } + } else { + Common::FixedPoint<64 - Q, Q> ramp{ramp_}; + for (u32 i = 0; i < sample_count; i++) { + sample = input[i] * volume; + output[i] = (output[i] + sample).to_int(); + volume += ramp; + } + } + return sample.to_int(); +} + +template s32 ApplyMixRamp<15>(std::span, std::span, const f32, const f32, + const u32); +template s32 ApplyMixRamp<23>(std::span, std::span, const f32, const f32, + const u32); + +void MixRampCommand::Dump(const ADSP::CommandListProcessor& processor, std::string& string) { + const auto ramp{(volume - prev_volume) / static_cast(processor.sample_count)}; + string += fmt::format("MixRampCommand"); + string += fmt::format("\n\tinput {:02X}", input_index); + string += fmt::format("\n\toutput {:02X}", output_index); + string += fmt::format("\n\tvolume {:.8f}", volume); + string += fmt::format("\n\tprev_volume {:.8f}", prev_volume); + string += fmt::format("\n\tramp {:.8f}", ramp); + string += "\n"; +} + +void MixRampCommand::Process(const ADSP::CommandListProcessor& processor) { + auto output{processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count)}; + auto input{processor.mix_buffers.subspan(input_index * processor.sample_count, + processor.sample_count)}; + const auto ramp{(volume - prev_volume) / static_cast(processor.sample_count)}; + auto prev_sample_ptr{reinterpret_cast(previous_sample)}; + + // If previous volume and ramp are both 0, nothing will be added to the output, so just skip. + if (prev_volume == 0.0f && ramp == 0.0f) { + *prev_sample_ptr = 0; + return; + } + + switch (precision) { + case 15: + *prev_sample_ptr = + ApplyMixRamp<15>(output, input, prev_volume, ramp, processor.sample_count); + break; + + case 23: + *prev_sample_ptr = + ApplyMixRamp<23>(output, input, prev_volume, ramp, processor.sample_count); + break; + + default: + LOG_ERROR(Service_Audio, "Invalid precision {}", precision); + break; + } +} + +bool MixRampCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/mix_ramp.h b/src/audio_core/renderer/command/mix/mix_ramp.h new file mode 100755 index 000000000..770f57e80 --- /dev/null +++ b/src/audio_core/renderer/command/mix/mix_ramp.h @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for mixing an input mix buffer to an output mix buffer, with a volume + * applied to the input, and volume ramping to smooth out the transition. + */ +struct MixRampCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Fixed point precision + u8 precision; + /// Input mix buffer index + s16 input_index; + /// Output mix buffer index + s16 output_index; + /// Previous mix volume + f32 prev_volume; + /// Current mix volume + f32 volume; + /// Pointer to the previous sample buffer, used for depopping + CpuAddr previous_sample; +}; + +/** + * Mix input mix buffer into output mix buffer, with volume applied to the input. + * @tparam Q - Number of bits for fixed point operations. + * @param output - Output mix buffer. + * @param input - Input mix buffer. + * @param volume - Volume applied to the input. + * @param ramp - Ramp applied to volume every sample. + * @param sample_count - Number of samples to process. + * @return The final gained input sample, used for depopping. + */ +template +s32 ApplyMixRamp(std::span output, std::span input, const f32 volume_, + const f32 ramp_, const u32 sample_count); + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/mix_ramp_grouped.cpp b/src/audio_core/renderer/command/mix/mix_ramp_grouped.cpp new file mode 100755 index 000000000..43dbef9fc --- /dev/null +++ b/src/audio_core/renderer/command/mix/mix_ramp_grouped.cpp @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/mix/mix_ramp.h" +#include "audio_core/renderer/command/mix/mix_ramp_grouped.h" + +namespace AudioCore::AudioRenderer { + +void MixRampGroupedCommand::Dump(const ADSP::CommandListProcessor& processor, std::string& string) { + string += "MixRampGroupedCommand"; + for (u32 i = 0; i < buffer_count; i++) { + string += fmt::format("\n\t{}", i); + const auto ramp{(volumes[i] - prev_volumes[i]) / static_cast(processor.sample_count)}; + string += fmt::format("\n\t\tinput {:02X}", inputs[i]); + string += fmt::format("\n\t\toutput {:02X}", outputs[i]); + string += fmt::format("\n\t\tvolume {:.8f}", volumes[i]); + string += fmt::format("\n\t\tprev_volume {:.8f}", prev_volumes[i]); + string += fmt::format("\n\t\tramp {:.8f}", ramp); + string += "\n"; + } +} + +void MixRampGroupedCommand::Process(const ADSP::CommandListProcessor& processor) { + std::span prev_samples = {reinterpret_cast(previous_samples), MaxMixBuffers}; + + for (u32 i = 0; i < buffer_count; i++) { + auto last_sample{0}; + if (prev_volumes[i] != 0.0f || volumes[i] != 0.0f) { + const auto output{processor.mix_buffers.subspan(outputs[i] * processor.sample_count, + processor.sample_count)}; + const auto input{processor.mix_buffers.subspan(inputs[i] * processor.sample_count, + processor.sample_count)}; + const auto ramp{(volumes[i] - prev_volumes[i]) / + static_cast(processor.sample_count)}; + + if (prev_volumes[i] == 0.0f && ramp == 0.0f) { + prev_samples[i] = 0; + continue; + } + + switch (precision) { + case 15: + last_sample = + ApplyMixRamp<15>(output, input, prev_volumes[i], ramp, processor.sample_count); + break; + case 23: + last_sample = + ApplyMixRamp<23>(output, input, prev_volumes[i], ramp, processor.sample_count); + break; + default: + LOG_ERROR(Service_Audio, "Invalid precision {}", precision); + break; + } + } + + prev_samples[i] = last_sample; + } +} + +bool MixRampGroupedCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/mix_ramp_grouped.h b/src/audio_core/renderer/command/mix/mix_ramp_grouped.h new file mode 100755 index 000000000..027276e5a --- /dev/null +++ b/src/audio_core/renderer/command/mix/mix_ramp_grouped.h @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for mixing multiple input mix buffers to multiple output mix buffers, with + * a volume applied to the input, and volume ramping to smooth out the transition. + */ +struct MixRampGroupedCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Fixed point precision + u8 precision; + /// Number of mix buffers to mix + u32 buffer_count; + /// Input mix buffer indexes for each mix buffer + std::array inputs; + /// Output mix buffer indexes for each mix buffer + std::array outputs; + /// Previous mix vloumes for each mix buffer + std::array prev_volumes; + /// Current mix vloumes for each mix buffer + std::array volumes; + /// Pointer to the previous sample buffer, used for depop + CpuAddr previous_samples; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/volume.cpp b/src/audio_core/renderer/command/mix/volume.cpp new file mode 100755 index 000000000..b045fb062 --- /dev/null +++ b/src/audio_core/renderer/command/mix/volume.cpp @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/mix/volume.h" +#include "common/fixed_point.h" +#include "common/logging/log.h" + +namespace AudioCore::AudioRenderer { +/** + * Apply volume to the input mix buffer, saving to the output buffer. + * + * @tparam Q - Number of bits for fixed point operations. + * @param output - Output mix buffer. + * @param input - Input mix buffer. + * @param volume - Volume applied to the input. + * @param sample_count - Number of samples to process. + */ +template +static void ApplyUniformGain(std::span output, std::span input, const f32 volume, + const u32 sample_count) { + if (volume == 1.0f) { + std::memcpy(output.data(), input.data(), input.size_bytes()); + } else { + const Common::FixedPoint<64 - Q, Q> gain{volume}; + for (u32 i = 0; i < sample_count; i++) { + output[i] = (input[i] * gain).to_int(); + } + } +} + +void VolumeCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("VolumeCommand"); + string += fmt::format("\n\tinput {:02X}", input_index); + string += fmt::format("\n\toutput {:02X}", output_index); + string += fmt::format("\n\tvolume {:.8f}", volume); + string += "\n"; +} + +void VolumeCommand::Process(const ADSP::CommandListProcessor& processor) { + // If input and output buffers are the same, and the volume is 1.0f, this won't do + // anything, so just skip. + if (input_index == output_index && volume == 1.0f) { + return; + } + + auto output{processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count)}; + auto input{processor.mix_buffers.subspan(input_index * processor.sample_count, + processor.sample_count)}; + + switch (precision) { + case 15: + ApplyUniformGain<15>(output, input, volume, processor.sample_count); + break; + + case 23: + ApplyUniformGain<23>(output, input, volume, processor.sample_count); + break; + + default: + LOG_ERROR(Service_Audio, "Invalid precision {}", precision); + break; + } +} + +bool VolumeCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/volume.h b/src/audio_core/renderer/command/mix/volume.h new file mode 100755 index 000000000..6ae9fb794 --- /dev/null +++ b/src/audio_core/renderer/command/mix/volume.h @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for applying volume to a mix buffer. + */ +struct VolumeCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Fixed point precision + u8 precision; + /// Input mix buffer index + s16 input_index; + /// Output mix buffer index + s16 output_index; + /// Mix volume applied to the input + f32 volume; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/volume_ramp.cpp b/src/audio_core/renderer/command/mix/volume_ramp.cpp new file mode 100755 index 000000000..424307148 --- /dev/null +++ b/src/audio_core/renderer/command/mix/volume_ramp.cpp @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/mix/volume_ramp.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { +/** + * Apply volume with ramping to the input mix buffer, saving to the output buffer. + * + * @tparam Q - Number of bits for fixed point operations. + * @param output - Output mix buffers. + * @param input - Input mix buffers. + * @param volume - Volume applied to the input. + * @param ramp - Ramp applied to volume every sample. + * @param sample_count - Number of samples to process. + */ +template +static void ApplyLinearEnvelopeGain(std::span output, std::span input, + const f32 volume, const f32 ramp_, const u32 sample_count) { + if (volume == 0.0f && ramp_ == 0.0f) { + std::memset(output.data(), 0, output.size_bytes()); + } else if (volume == 1.0f && ramp_ == 0.0f) { + std::memcpy(output.data(), input.data(), output.size_bytes()); + } else if (ramp_ == 0.0f) { + const Common::FixedPoint<64 - Q, Q> gain{volume}; + for (u32 i = 0; i < sample_count; i++) { + output[i] = (input[i] * gain).to_int(); + } + } else { + Common::FixedPoint<64 - Q, Q> gain{volume}; + const Common::FixedPoint<64 - Q, Q> ramp{ramp_}; + for (u32 i = 0; i < sample_count; i++) { + output[i] = (input[i] * gain).to_int(); + gain += ramp; + } + } +} + +void VolumeRampCommand::Dump(const ADSP::CommandListProcessor& processor, std::string& string) { + const auto ramp{(volume - prev_volume) / static_cast(processor.sample_count)}; + string += fmt::format("VolumeRampCommand"); + string += fmt::format("\n\tinput {:02X}", input_index); + string += fmt::format("\n\toutput {:02X}", output_index); + string += fmt::format("\n\tvolume {:.8f}", volume); + string += fmt::format("\n\tprev_volume {:.8f}", prev_volume); + string += fmt::format("\n\tramp {:.8f}", ramp); + string += "\n"; +} + +void VolumeRampCommand::Process(const ADSP::CommandListProcessor& processor) { + auto output{processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count)}; + auto input{processor.mix_buffers.subspan(input_index * processor.sample_count, + processor.sample_count)}; + const auto ramp{(volume - prev_volume) / static_cast(processor.sample_count)}; + + // If input and output buffers are the same, and the volume is 1.0f, and there's no ramping, + // this won't do anything, so just skip. + if (input_index == output_index && prev_volume == 1.0f && ramp == 0.0f) { + return; + } + + switch (precision) { + case 15: + ApplyLinearEnvelopeGain<15>(output, input, prev_volume, ramp, processor.sample_count); + break; + + case 23: + ApplyLinearEnvelopeGain<23>(output, input, prev_volume, ramp, processor.sample_count); + break; + + default: + LOG_ERROR(Service_Audio, "Invalid precision {}", precision); + break; + } +} + +bool VolumeRampCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/volume_ramp.h b/src/audio_core/renderer/command/mix/volume_ramp.h new file mode 100755 index 000000000..77b61547e --- /dev/null +++ b/src/audio_core/renderer/command/mix/volume_ramp.h @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for applying volume to a mix buffer, with ramping for the volume to smooth + * out the transition. + */ +struct VolumeRampCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Fixed point precision + u8 precision; + /// Input mix buffer index + s16 input_index; + /// Output mix buffer index + s16 output_index; + /// Previous mix volume applied to the input + f32 prev_volume; + /// Current mix volume applied to the input + f32 volume; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/performance/performance.cpp b/src/audio_core/renderer/command/performance/performance.cpp new file mode 100755 index 000000000..985958b03 --- /dev/null +++ b/src/audio_core/renderer/command/performance/performance.cpp @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/performance/performance.h" +#include "core/core.h" +#include "core/core_timing.h" +#include "core/core_timing_util.h" + +namespace AudioCore::AudioRenderer { + +void PerformanceCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("PerformanceCommand\n\tstate {}\n", static_cast(state)); +} + +void PerformanceCommand::Process(const ADSP::CommandListProcessor& processor) { + auto base{entry_address.translated_address}; + if (state == PerformanceState::Start) { + auto start_time_ptr{reinterpret_cast(base + entry_address.entry_start_time_offset)}; + *start_time_ptr = static_cast( + Core::Timing::CyclesToUs(processor.system->CoreTiming().GetClockTicks() - + processor.start_time - processor.current_processing_time) + .count()); + } else if (state == PerformanceState::Stop) { + auto processed_time_ptr{ + reinterpret_cast(base + entry_address.entry_processed_time_offset)}; + auto entry_count_ptr{ + reinterpret_cast(base + entry_address.header_entry_count_offset)}; + + *processed_time_ptr = static_cast( + Core::Timing::CyclesToUs(processor.system->CoreTiming().GetClockTicks() - + processor.start_time - processor.current_processing_time) + .count()); + (*entry_count_ptr)++; + } +} + +bool PerformanceCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/performance/performance.h b/src/audio_core/renderer/command/performance/performance.h new file mode 100755 index 000000000..11a7d6c08 --- /dev/null +++ b/src/audio_core/renderer/command/performance/performance.h @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "audio_core/renderer/performance/performance_entry_addresses.h" +#include "audio_core/renderer/performance/performance_manager.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for writing AudioRenderer performance metrics back to the sysmodule. + */ +struct PerformanceCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// State of the performance + PerformanceState state; + /// Pointers to be written + PerformanceEntryAddresses entry_address; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.cpp b/src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.cpp new file mode 100755 index 000000000..1fd90308a --- /dev/null +++ b/src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.cpp @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/resample/downmix_6ch_to_2ch.h" + +namespace AudioCore::AudioRenderer { + +void DownMix6chTo2chCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("DownMix6chTo2chCommand\n\tinputs: "); + for (u32 i = 0; i < MaxChannels; i++) { + string += fmt::format("{:02X}, ", inputs[i]); + } + string += "\n\toutputs: "; + for (u32 i = 0; i < MaxChannels; i++) { + string += fmt::format("{:02X}, ", outputs[i]); + } + string += "\n"; +} + +void DownMix6chTo2chCommand::Process(const ADSP::CommandListProcessor& processor) { + auto in_front_left{ + processor.mix_buffers.subspan(inputs[0] * processor.sample_count, processor.sample_count)}; + auto in_front_right{ + processor.mix_buffers.subspan(inputs[1] * processor.sample_count, processor.sample_count)}; + auto in_center{ + processor.mix_buffers.subspan(inputs[2] * processor.sample_count, processor.sample_count)}; + auto in_lfe{ + processor.mix_buffers.subspan(inputs[3] * processor.sample_count, processor.sample_count)}; + auto in_back_left{ + processor.mix_buffers.subspan(inputs[4] * processor.sample_count, processor.sample_count)}; + auto in_back_right{ + processor.mix_buffers.subspan(inputs[5] * processor.sample_count, processor.sample_count)}; + + auto out_front_left{ + processor.mix_buffers.subspan(outputs[0] * processor.sample_count, processor.sample_count)}; + auto out_front_right{ + processor.mix_buffers.subspan(outputs[1] * processor.sample_count, processor.sample_count)}; + auto out_center{ + processor.mix_buffers.subspan(outputs[2] * processor.sample_count, processor.sample_count)}; + auto out_lfe{ + processor.mix_buffers.subspan(outputs[3] * processor.sample_count, processor.sample_count)}; + auto out_back_left{ + processor.mix_buffers.subspan(outputs[4] * processor.sample_count, processor.sample_count)}; + auto out_back_right{ + processor.mix_buffers.subspan(outputs[5] * processor.sample_count, processor.sample_count)}; + + for (u32 i = 0; i < processor.sample_count; i++) { + const auto left_sample{(in_front_left[i] * down_mix_coeff[0] + + in_center[i] * down_mix_coeff[1] + in_lfe[i] * down_mix_coeff[2] + + in_back_left[i] * down_mix_coeff[3]) + .to_int()}; + + const auto right_sample{(in_front_right[i] * down_mix_coeff[0] + + in_center[i] * down_mix_coeff[1] + in_lfe[i] * down_mix_coeff[2] + + in_back_right[i] * down_mix_coeff[3]) + .to_int()}; + + out_front_left[i] = left_sample; + out_front_right[i] = right_sample; + } + + std::memset(out_center.data(), 0, out_center.size_bytes()); + std::memset(out_lfe.data(), 0, out_lfe.size_bytes()); + std::memset(out_back_left.data(), 0, out_back_left.size_bytes()); + std::memset(out_back_right.data(), 0, out_back_right.size_bytes()); +} + +bool DownMix6chTo2chCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.h b/src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.h new file mode 100755 index 000000000..fa6207f6f --- /dev/null +++ b/src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.h @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for downmixing 6 channels to 2. + * Channels: + * 0 - front left + * 1 - front right + * 2 - center + * 3 - lfe + * 4 - back left + * 5 - back right + */ +struct DownMix6chTo2chCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer offsets for each channel + std::array inputs; + /// Output mix buffer offsets for each channel + std::array outputs; + /// Coefficients used for downmixing + std::array, 4> down_mix_coeff; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/resample/resample.cpp b/src/audio_core/renderer/command/resample/resample.cpp new file mode 100755 index 000000000..070c9d2b8 --- /dev/null +++ b/src/audio_core/renderer/command/resample/resample.cpp @@ -0,0 +1,883 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/command/resample/resample.h" + +namespace AudioCore::AudioRenderer { + +static void ResampleLowQuality(std::span output, std::span input, + const Common::FixedPoint<49, 15>& sample_rate_ratio, + Common::FixedPoint<49, 15>& fraction, const u32 samples_to_write) { + if (sample_rate_ratio == 1.0f) { + for (u32 i = 0; i < samples_to_write; i++) { + output[i] = input[i]; + } + } else { + u32 read_index{0}; + for (u32 i = 0; i < samples_to_write; i++) { + output[i] = input[read_index + (fraction >= 0.5f)]; + fraction += sample_rate_ratio; + read_index += static_cast(fraction.to_int_floor()); + fraction.clear_int(); + } + } +} + +static void ResampleNormalQuality(std::span output, std::span input, + const Common::FixedPoint<49, 15>& sample_rate_ratio, + Common::FixedPoint<49, 15>& fraction, + const u32 samples_to_write) { + static constexpr std::array lut0 = { + 0.20141602f, 0.59283447f, 0.20513916f, 0.00009155f, 0.19772339f, 0.59277344f, 0.20889282f, + 0.00027466f, 0.19406128f, 0.59262085f, 0.21264648f, 0.00045776f, 0.19039917f, 0.59240723f, + 0.21646118f, 0.00067139f, 0.18679810f, 0.59213257f, 0.22030640f, 0.00085449f, 0.18322754f, + 0.59176636f, 0.22415161f, 0.00103760f, 0.17968750f, 0.59133911f, 0.22802734f, 0.00125122f, + 0.17617798f, 0.59085083f, 0.23193359f, 0.00146484f, 0.17269897f, 0.59027100f, 0.23583984f, + 0.00167847f, 0.16925049f, 0.58963013f, 0.23977661f, 0.00189209f, 0.16583252f, 0.58892822f, + 0.24374390f, 0.00210571f, 0.16244507f, 0.58816528f, 0.24774170f, 0.00234985f, 0.15908813f, + 0.58731079f, 0.25173950f, 0.00256348f, 0.15576172f, 0.58639526f, 0.25576782f, 0.00280762f, + 0.15249634f, 0.58541870f, 0.25979614f, 0.00308228f, 0.14923096f, 0.58435059f, 0.26385498f, + 0.00332642f, 0.14602661f, 0.58325195f, 0.26794434f, 0.00360107f, 0.14285278f, 0.58206177f, + 0.27203369f, 0.00387573f, 0.13973999f, 0.58078003f, 0.27612305f, 0.00418091f, 0.13662720f, + 0.57946777f, 0.28024292f, 0.00448608f, 0.13357544f, 0.57806396f, 0.28436279f, 0.00479126f, + 0.13052368f, 0.57662964f, 0.28851318f, 0.00512695f, 0.12753296f, 0.57510376f, 0.29266357f, + 0.00546265f, 0.12460327f, 0.57351685f, 0.29681396f, 0.00579834f, 0.12167358f, 0.57183838f, + 0.30099487f, 0.00616455f, 0.11880493f, 0.57012939f, 0.30517578f, 0.00656128f, 0.11596680f, + 0.56835938f, 0.30935669f, 0.00695801f, 0.11318970f, 0.56649780f, 0.31353760f, 0.00735474f, + 0.11041260f, 0.56457520f, 0.31771851f, 0.00778198f, 0.10769653f, 0.56262207f, 0.32192993f, + 0.00823975f, 0.10501099f, 0.56057739f, 0.32614136f, 0.00869751f, 0.10238647f, 0.55847168f, + 0.33032227f, 0.00915527f, 0.09976196f, 0.55633545f, 0.33453369f, 0.00967407f, 0.09722900f, + 0.55410767f, 0.33874512f, 0.01019287f, 0.09469604f, 0.55181885f, 0.34295654f, 0.01071167f, + 0.09222412f, 0.54949951f, 0.34713745f, 0.01126099f, 0.08978271f, 0.54708862f, 0.35134888f, + 0.01184082f, 0.08737183f, 0.54464722f, 0.35552979f, 0.01245117f, 0.08499146f, 0.54214478f, + 0.35974121f, 0.01306152f, 0.08267212f, 0.53958130f, 0.36392212f, 0.01370239f, 0.08041382f, + 0.53695679f, 0.36810303f, 0.01437378f, 0.07815552f, 0.53427124f, 0.37225342f, 0.01507568f, + 0.07595825f, 0.53155518f, 0.37640381f, 0.01577759f, 0.07379150f, 0.52877808f, 0.38055420f, + 0.01651001f, 0.07165527f, 0.52593994f, 0.38470459f, 0.01727295f, 0.06958008f, 0.52307129f, + 0.38882446f, 0.01806641f, 0.06753540f, 0.52014160f, 0.39294434f, 0.01889038f, 0.06552124f, + 0.51715088f, 0.39703369f, 0.01974487f, 0.06356812f, 0.51409912f, 0.40112305f, 0.02059937f, + 0.06164551f, 0.51101685f, 0.40518188f, 0.02148438f, 0.05975342f, 0.50790405f, 0.40921021f, + 0.02243042f, 0.05789185f, 0.50473022f, 0.41323853f, 0.02337646f, 0.05609131f, 0.50152588f, + 0.41726685f, 0.02435303f, 0.05432129f, 0.49826050f, 0.42123413f, 0.02539062f, 0.05258179f, + 0.49493408f, 0.42520142f, 0.02642822f, 0.05087280f, 0.49160767f, 0.42913818f, 0.02749634f, + 0.04922485f, 0.48822021f, 0.43307495f, 0.02859497f, 0.04760742f, 0.48477173f, 0.43695068f, + 0.02975464f, 0.04602051f, 0.48132324f, 0.44082642f, 0.03091431f, 0.04446411f, 0.47781372f, + 0.44467163f, 0.03210449f, 0.04293823f, 0.47424316f, 0.44845581f, 0.03335571f, 0.04147339f, + 0.47067261f, 0.45223999f, 0.03460693f, 0.04003906f, 0.46704102f, 0.45599365f, 0.03591919f, + 0.03863525f, 0.46340942f, 0.45971680f, 0.03726196f, 0.03726196f, 0.45971680f, 0.46340942f, + 0.03863525f, 0.03591919f, 0.45599365f, 0.46704102f, 0.04003906f, 0.03460693f, 0.45223999f, + 0.47067261f, 0.04147339f, 0.03335571f, 0.44845581f, 0.47424316f, 0.04293823f, 0.03210449f, + 0.44467163f, 0.47781372f, 0.04446411f, 0.03091431f, 0.44082642f, 0.48132324f, 0.04602051f, + 0.02975464f, 0.43695068f, 0.48477173f, 0.04760742f, 0.02859497f, 0.43307495f, 0.48822021f, + 0.04922485f, 0.02749634f, 0.42913818f, 0.49160767f, 0.05087280f, 0.02642822f, 0.42520142f, + 0.49493408f, 0.05258179f, 0.02539062f, 0.42123413f, 0.49826050f, 0.05432129f, 0.02435303f, + 0.41726685f, 0.50152588f, 0.05609131f, 0.02337646f, 0.41323853f, 0.50473022f, 0.05789185f, + 0.02243042f, 0.40921021f, 0.50790405f, 0.05975342f, 0.02148438f, 0.40518188f, 0.51101685f, + 0.06164551f, 0.02059937f, 0.40112305f, 0.51409912f, 0.06356812f, 0.01974487f, 0.39703369f, + 0.51715088f, 0.06552124f, 0.01889038f, 0.39294434f, 0.52014160f, 0.06753540f, 0.01806641f, + 0.38882446f, 0.52307129f, 0.06958008f, 0.01727295f, 0.38470459f, 0.52593994f, 0.07165527f, + 0.01651001f, 0.38055420f, 0.52877808f, 0.07379150f, 0.01577759f, 0.37640381f, 0.53155518f, + 0.07595825f, 0.01507568f, 0.37225342f, 0.53427124f, 0.07815552f, 0.01437378f, 0.36810303f, + 0.53695679f, 0.08041382f, 0.01370239f, 0.36392212f, 0.53958130f, 0.08267212f, 0.01306152f, + 0.35974121f, 0.54214478f, 0.08499146f, 0.01245117f, 0.35552979f, 0.54464722f, 0.08737183f, + 0.01184082f, 0.35134888f, 0.54708862f, 0.08978271f, 0.01126099f, 0.34713745f, 0.54949951f, + 0.09222412f, 0.01071167f, 0.34295654f, 0.55181885f, 0.09469604f, 0.01019287f, 0.33874512f, + 0.55410767f, 0.09722900f, 0.00967407f, 0.33453369f, 0.55633545f, 0.09976196f, 0.00915527f, + 0.33032227f, 0.55847168f, 0.10238647f, 0.00869751f, 0.32614136f, 0.56057739f, 0.10501099f, + 0.00823975f, 0.32192993f, 0.56262207f, 0.10769653f, 0.00778198f, 0.31771851f, 0.56457520f, + 0.11041260f, 0.00735474f, 0.31353760f, 0.56649780f, 0.11318970f, 0.00695801f, 0.30935669f, + 0.56835938f, 0.11596680f, 0.00656128f, 0.30517578f, 0.57012939f, 0.11880493f, 0.00616455f, + 0.30099487f, 0.57183838f, 0.12167358f, 0.00579834f, 0.29681396f, 0.57351685f, 0.12460327f, + 0.00546265f, 0.29266357f, 0.57510376f, 0.12753296f, 0.00512695f, 0.28851318f, 0.57662964f, + 0.13052368f, 0.00479126f, 0.28436279f, 0.57806396f, 0.13357544f, 0.00448608f, 0.28024292f, + 0.57946777f, 0.13662720f, 0.00418091f, 0.27612305f, 0.58078003f, 0.13973999f, 0.00387573f, + 0.27203369f, 0.58206177f, 0.14285278f, 0.00360107f, 0.26794434f, 0.58325195f, 0.14602661f, + 0.00332642f, 0.26385498f, 0.58435059f, 0.14923096f, 0.00308228f, 0.25979614f, 0.58541870f, + 0.15249634f, 0.00280762f, 0.25576782f, 0.58639526f, 0.15576172f, 0.00256348f, 0.25173950f, + 0.58731079f, 0.15908813f, 0.00234985f, 0.24774170f, 0.58816528f, 0.16244507f, 0.00210571f, + 0.24374390f, 0.58892822f, 0.16583252f, 0.00189209f, 0.23977661f, 0.58963013f, 0.16925049f, + 0.00167847f, 0.23583984f, 0.59027100f, 0.17269897f, 0.00146484f, 0.23193359f, 0.59085083f, + 0.17617798f, 0.00125122f, 0.22802734f, 0.59133911f, 0.17968750f, 0.00103760f, 0.22415161f, + 0.59176636f, 0.18322754f, 0.00085449f, 0.22030640f, 0.59213257f, 0.18679810f, 0.00067139f, + 0.21646118f, 0.59240723f, 0.19039917f, 0.00045776f, 0.21264648f, 0.59262085f, 0.19406128f, + 0.00027466f, 0.20889282f, 0.59277344f, 0.19772339f, 0.00009155f, 0.20513916f, 0.59283447f, + 0.20141602f, + }; + + static constexpr std::array lut1 = { + 0.00207520f, 0.99606323f, 0.00210571f, -0.00015259f, -0.00610352f, 0.99578857f, + 0.00646973f, -0.00045776f, -0.01000977f, 0.99526978f, 0.01095581f, -0.00079346f, + -0.01373291f, 0.99444580f, 0.01562500f, -0.00109863f, -0.01733398f, 0.99337769f, + 0.02041626f, -0.00143433f, -0.02075195f, 0.99203491f, 0.02539062f, -0.00177002f, + -0.02404785f, 0.99041748f, 0.03051758f, -0.00210571f, -0.02719116f, 0.98855591f, + 0.03582764f, -0.00244141f, -0.03021240f, 0.98641968f, 0.04125977f, -0.00280762f, + -0.03308105f, 0.98400879f, 0.04687500f, -0.00314331f, -0.03579712f, 0.98135376f, + 0.05261230f, -0.00350952f, -0.03839111f, 0.97842407f, 0.05856323f, -0.00390625f, + -0.04083252f, 0.97521973f, 0.06463623f, -0.00427246f, -0.04315186f, 0.97180176f, + 0.07086182f, -0.00466919f, -0.04534912f, 0.96810913f, 0.07727051f, -0.00509644f, + -0.04742432f, 0.96414185f, 0.08383179f, -0.00549316f, -0.04934692f, 0.95996094f, + 0.09054565f, -0.00592041f, -0.05114746f, 0.95550537f, 0.09741211f, -0.00637817f, + -0.05285645f, 0.95083618f, 0.10443115f, -0.00683594f, -0.05441284f, 0.94589233f, + 0.11160278f, -0.00732422f, -0.05584717f, 0.94073486f, 0.11892700f, -0.00781250f, + -0.05718994f, 0.93533325f, 0.12643433f, -0.00830078f, -0.05841064f, 0.92968750f, + 0.13406372f, -0.00881958f, -0.05953979f, 0.92382812f, 0.14184570f, -0.00936890f, + -0.06054688f, 0.91772461f, 0.14978027f, -0.00991821f, -0.06146240f, 0.91143799f, + 0.15783691f, -0.01046753f, -0.06225586f, 0.90490723f, 0.16607666f, -0.01104736f, + -0.06295776f, 0.89816284f, 0.17443848f, -0.01165771f, -0.06356812f, 0.89120483f, + 0.18292236f, -0.01229858f, -0.06408691f, 0.88403320f, 0.19155884f, -0.01293945f, + -0.06451416f, 0.87667847f, 0.20034790f, -0.01358032f, -0.06484985f, 0.86914062f, + 0.20925903f, -0.01428223f, -0.06509399f, 0.86138916f, 0.21829224f, -0.01495361f, + -0.06527710f, 0.85345459f, 0.22744751f, -0.01568604f, -0.06536865f, 0.84533691f, + 0.23675537f, -0.01641846f, -0.06536865f, 0.83703613f, 0.24615479f, -0.01718140f, + -0.06533813f, 0.82858276f, 0.25567627f, -0.01794434f, -0.06518555f, 0.81991577f, + 0.26531982f, -0.01873779f, -0.06500244f, 0.81112671f, 0.27505493f, -0.01956177f, + -0.06472778f, 0.80215454f, 0.28491211f, -0.02038574f, -0.06442261f, 0.79306030f, + 0.29489136f, -0.02124023f, -0.06402588f, 0.78378296f, 0.30496216f, -0.02209473f, + -0.06359863f, 0.77438354f, 0.31512451f, -0.02297974f, -0.06307983f, 0.76486206f, + 0.32537842f, -0.02389526f, -0.06253052f, 0.75518799f, 0.33569336f, -0.02481079f, + -0.06195068f, 0.74539185f, 0.34613037f, -0.02575684f, -0.06130981f, 0.73547363f, + 0.35662842f, -0.02670288f, -0.06060791f, 0.72543335f, 0.36721802f, -0.02767944f, + -0.05987549f, 0.71527100f, 0.37786865f, -0.02865601f, -0.05911255f, 0.70504761f, + 0.38858032f, -0.02966309f, -0.05831909f, 0.69470215f, 0.39935303f, -0.03067017f, + -0.05746460f, 0.68426514f, 0.41018677f, -0.03170776f, -0.05661011f, 0.67373657f, + 0.42108154f, -0.03271484f, -0.05569458f, 0.66311646f, 0.43200684f, -0.03378296f, + -0.05477905f, 0.65246582f, 0.44299316f, -0.03482056f, -0.05383301f, 0.64169312f, + 0.45401001f, -0.03588867f, -0.05285645f, 0.63088989f, 0.46505737f, -0.03695679f, + -0.05187988f, 0.62002563f, 0.47613525f, -0.03802490f, -0.05087280f, 0.60910034f, + 0.48721313f, -0.03912354f, -0.04983521f, 0.59814453f, 0.49832153f, -0.04019165f, + -0.04879761f, 0.58712769f, 0.50946045f, -0.04129028f, -0.04772949f, 0.57611084f, + 0.52056885f, -0.04235840f, -0.04669189f, 0.56503296f, 0.53170776f, -0.04345703f, + -0.04562378f, 0.55392456f, 0.54281616f, -0.04452515f, -0.04452515f, 0.54281616f, + 0.55392456f, -0.04562378f, -0.04345703f, 0.53170776f, 0.56503296f, -0.04669189f, + -0.04235840f, 0.52056885f, 0.57611084f, -0.04772949f, -0.04129028f, 0.50946045f, + 0.58712769f, -0.04879761f, -0.04019165f, 0.49832153f, 0.59814453f, -0.04983521f, + -0.03912354f, 0.48721313f, 0.60910034f, -0.05087280f, -0.03802490f, 0.47613525f, + 0.62002563f, -0.05187988f, -0.03695679f, 0.46505737f, 0.63088989f, -0.05285645f, + -0.03588867f, 0.45401001f, 0.64169312f, -0.05383301f, -0.03482056f, 0.44299316f, + 0.65246582f, -0.05477905f, -0.03378296f, 0.43200684f, 0.66311646f, -0.05569458f, + -0.03271484f, 0.42108154f, 0.67373657f, -0.05661011f, -0.03170776f, 0.41018677f, + 0.68426514f, -0.05746460f, -0.03067017f, 0.39935303f, 0.69470215f, -0.05831909f, + -0.02966309f, 0.38858032f, 0.70504761f, -0.05911255f, -0.02865601f, 0.37786865f, + 0.71527100f, -0.05987549f, -0.02767944f, 0.36721802f, 0.72543335f, -0.06060791f, + -0.02670288f, 0.35662842f, 0.73547363f, -0.06130981f, -0.02575684f, 0.34613037f, + 0.74539185f, -0.06195068f, -0.02481079f, 0.33569336f, 0.75518799f, -0.06253052f, + -0.02389526f, 0.32537842f, 0.76486206f, -0.06307983f, -0.02297974f, 0.31512451f, + 0.77438354f, -0.06359863f, -0.02209473f, 0.30496216f, 0.78378296f, -0.06402588f, + -0.02124023f, 0.29489136f, 0.79306030f, -0.06442261f, -0.02038574f, 0.28491211f, + 0.80215454f, -0.06472778f, -0.01956177f, 0.27505493f, 0.81112671f, -0.06500244f, + -0.01873779f, 0.26531982f, 0.81991577f, -0.06518555f, -0.01794434f, 0.25567627f, + 0.82858276f, -0.06533813f, -0.01718140f, 0.24615479f, 0.83703613f, -0.06536865f, + -0.01641846f, 0.23675537f, 0.84533691f, -0.06536865f, -0.01568604f, 0.22744751f, + 0.85345459f, -0.06527710f, -0.01495361f, 0.21829224f, 0.86138916f, -0.06509399f, + -0.01428223f, 0.20925903f, 0.86914062f, -0.06484985f, -0.01358032f, 0.20034790f, + 0.87667847f, -0.06451416f, -0.01293945f, 0.19155884f, 0.88403320f, -0.06408691f, + -0.01229858f, 0.18292236f, 0.89120483f, -0.06356812f, -0.01165771f, 0.17443848f, + 0.89816284f, -0.06295776f, -0.01104736f, 0.16607666f, 0.90490723f, -0.06225586f, + -0.01046753f, 0.15783691f, 0.91143799f, -0.06146240f, -0.00991821f, 0.14978027f, + 0.91772461f, -0.06054688f, -0.00936890f, 0.14184570f, 0.92382812f, -0.05953979f, + -0.00881958f, 0.13406372f, 0.92968750f, -0.05841064f, -0.00830078f, 0.12643433f, + 0.93533325f, -0.05718994f, -0.00781250f, 0.11892700f, 0.94073486f, -0.05584717f, + -0.00732422f, 0.11160278f, 0.94589233f, -0.05441284f, -0.00683594f, 0.10443115f, + 0.95083618f, -0.05285645f, -0.00637817f, 0.09741211f, 0.95550537f, -0.05114746f, + -0.00592041f, 0.09054565f, 0.95996094f, -0.04934692f, -0.00549316f, 0.08383179f, + 0.96414185f, -0.04742432f, -0.00509644f, 0.07727051f, 0.96810913f, -0.04534912f, + -0.00466919f, 0.07086182f, 0.97180176f, -0.04315186f, -0.00427246f, 0.06463623f, + 0.97521973f, -0.04083252f, -0.00390625f, 0.05856323f, 0.97842407f, -0.03839111f, + -0.00350952f, 0.05261230f, 0.98135376f, -0.03579712f, -0.00314331f, 0.04687500f, + 0.98400879f, -0.03308105f, -0.00280762f, 0.04125977f, 0.98641968f, -0.03021240f, + -0.00244141f, 0.03582764f, 0.98855591f, -0.02719116f, -0.00210571f, 0.03051758f, + 0.99041748f, -0.02404785f, -0.00177002f, 0.02539062f, 0.99203491f, -0.02075195f, + -0.00143433f, 0.02041626f, 0.99337769f, -0.01733398f, -0.00109863f, 0.01562500f, + 0.99444580f, -0.01373291f, -0.00079346f, 0.01095581f, 0.99526978f, -0.01000977f, + -0.00045776f, 0.00646973f, 0.99578857f, -0.00610352f, -0.00015259f, 0.00210571f, + 0.99606323f, -0.00207520f, + }; + + static constexpr std::array lut2 = { + 0.09750366f, 0.80221558f, 0.10159302f, -0.00097656f, 0.09350586f, 0.80203247f, + 0.10580444f, -0.00103760f, 0.08959961f, 0.80169678f, 0.11010742f, -0.00115967f, + 0.08578491f, 0.80117798f, 0.11447144f, -0.00128174f, 0.08203125f, 0.80047607f, + 0.11892700f, -0.00140381f, 0.07836914f, 0.79962158f, 0.12347412f, -0.00152588f, + 0.07479858f, 0.79861450f, 0.12814331f, -0.00164795f, 0.07135010f, 0.79742432f, + 0.13287354f, -0.00177002f, 0.06796265f, 0.79605103f, 0.13769531f, -0.00192261f, + 0.06469727f, 0.79452515f, 0.14260864f, -0.00204468f, 0.06149292f, 0.79284668f, + 0.14761353f, -0.00219727f, 0.05834961f, 0.79098511f, 0.15270996f, -0.00231934f, + 0.05532837f, 0.78894043f, 0.15789795f, -0.00247192f, 0.05236816f, 0.78674316f, + 0.16317749f, -0.00265503f, 0.04949951f, 0.78442383f, 0.16851807f, -0.00280762f, + 0.04672241f, 0.78189087f, 0.17398071f, -0.00299072f, 0.04400635f, 0.77920532f, + 0.17950439f, -0.00314331f, 0.04141235f, 0.77636719f, 0.18511963f, -0.00332642f, + 0.03887939f, 0.77337646f, 0.19082642f, -0.00350952f, 0.03640747f, 0.77023315f, + 0.19659424f, -0.00369263f, 0.03402710f, 0.76693726f, 0.20248413f, -0.00387573f, + 0.03173828f, 0.76348877f, 0.20843506f, -0.00405884f, 0.02951050f, 0.75985718f, + 0.21444702f, -0.00427246f, 0.02737427f, 0.75610352f, 0.22055054f, -0.00445557f, + 0.02529907f, 0.75219727f, 0.22674561f, -0.00466919f, 0.02331543f, 0.74816895f, + 0.23300171f, -0.00485229f, 0.02139282f, 0.74398804f, 0.23931885f, -0.00506592f, + 0.01956177f, 0.73965454f, 0.24572754f, -0.00531006f, 0.01779175f, 0.73519897f, + 0.25219727f, -0.00552368f, 0.01605225f, 0.73059082f, 0.25872803f, -0.00570679f, + 0.01440430f, 0.72586060f, 0.26535034f, -0.00592041f, 0.01281738f, 0.72100830f, + 0.27203369f, -0.00616455f, 0.01132202f, 0.71600342f, 0.27877808f, -0.00637817f, + 0.00988770f, 0.71090698f, 0.28558350f, -0.00656128f, 0.00851440f, 0.70565796f, + 0.29244995f, -0.00677490f, 0.00720215f, 0.70031738f, 0.29934692f, -0.00701904f, + 0.00592041f, 0.69485474f, 0.30633545f, -0.00723267f, 0.00469971f, 0.68927002f, + 0.31338501f, -0.00741577f, 0.00357056f, 0.68356323f, 0.32046509f, -0.00762939f, + 0.00247192f, 0.67773438f, 0.32760620f, -0.00787354f, 0.00143433f, 0.67184448f, + 0.33477783f, -0.00808716f, 0.00045776f, 0.66583252f, 0.34197998f, -0.00827026f, + -0.00048828f, 0.65972900f, 0.34924316f, -0.00845337f, -0.00134277f, 0.65353394f, + 0.35656738f, -0.00863647f, -0.00216675f, 0.64721680f, 0.36389160f, -0.00885010f, + -0.00296021f, 0.64083862f, 0.37127686f, -0.00903320f, -0.00369263f, 0.63433838f, + 0.37869263f, -0.00921631f, -0.00436401f, 0.62777710f, 0.38613892f, -0.00933838f, + -0.00497437f, 0.62115479f, 0.39361572f, -0.00949097f, -0.00558472f, 0.61444092f, + 0.40109253f, -0.00964355f, -0.00613403f, 0.60763550f, 0.40859985f, -0.00979614f, + -0.00665283f, 0.60076904f, 0.41610718f, -0.00991821f, -0.00714111f, 0.59384155f, + 0.42364502f, -0.01000977f, -0.00756836f, 0.58685303f, 0.43121338f, -0.01013184f, + -0.00796509f, 0.57977295f, 0.43875122f, -0.01022339f, -0.00833130f, 0.57266235f, + 0.44631958f, -0.01028442f, -0.00866699f, 0.56552124f, 0.45388794f, -0.01034546f, + -0.00897217f, 0.55831909f, 0.46145630f, -0.01040649f, -0.00921631f, 0.55105591f, + 0.46902466f, -0.01040649f, -0.00946045f, 0.54373169f, 0.47659302f, -0.01040649f, + -0.00967407f, 0.53640747f, 0.48413086f, -0.01037598f, -0.00985718f, 0.52902222f, + 0.49166870f, -0.01037598f, -0.01000977f, 0.52160645f, 0.49917603f, -0.01031494f, + -0.01013184f, 0.51416016f, 0.50668335f, -0.01025391f, -0.01025391f, 0.50668335f, + 0.51416016f, -0.01013184f, -0.01031494f, 0.49917603f, 0.52160645f, -0.01000977f, + -0.01037598f, 0.49166870f, 0.52902222f, -0.00985718f, -0.01037598f, 0.48413086f, + 0.53640747f, -0.00967407f, -0.01040649f, 0.47659302f, 0.54373169f, -0.00946045f, + -0.01040649f, 0.46902466f, 0.55105591f, -0.00921631f, -0.01040649f, 0.46145630f, + 0.55831909f, -0.00897217f, -0.01034546f, 0.45388794f, 0.56552124f, -0.00866699f, + -0.01028442f, 0.44631958f, 0.57266235f, -0.00833130f, -0.01022339f, 0.43875122f, + 0.57977295f, -0.00796509f, -0.01013184f, 0.43121338f, 0.58685303f, -0.00756836f, + -0.01000977f, 0.42364502f, 0.59384155f, -0.00714111f, -0.00991821f, 0.41610718f, + 0.60076904f, -0.00665283f, -0.00979614f, 0.40859985f, 0.60763550f, -0.00613403f, + -0.00964355f, 0.40109253f, 0.61444092f, -0.00558472f, -0.00949097f, 0.39361572f, + 0.62115479f, -0.00497437f, -0.00933838f, 0.38613892f, 0.62777710f, -0.00436401f, + -0.00921631f, 0.37869263f, 0.63433838f, -0.00369263f, -0.00903320f, 0.37127686f, + 0.64083862f, -0.00296021f, -0.00885010f, 0.36389160f, 0.64721680f, -0.00216675f, + -0.00863647f, 0.35656738f, 0.65353394f, -0.00134277f, -0.00845337f, 0.34924316f, + 0.65972900f, -0.00048828f, -0.00827026f, 0.34197998f, 0.66583252f, 0.00045776f, + -0.00808716f, 0.33477783f, 0.67184448f, 0.00143433f, -0.00787354f, 0.32760620f, + 0.67773438f, 0.00247192f, -0.00762939f, 0.32046509f, 0.68356323f, 0.00357056f, + -0.00741577f, 0.31338501f, 0.68927002f, 0.00469971f, -0.00723267f, 0.30633545f, + 0.69485474f, 0.00592041f, -0.00701904f, 0.29934692f, 0.70031738f, 0.00720215f, + -0.00677490f, 0.29244995f, 0.70565796f, 0.00851440f, -0.00656128f, 0.28558350f, + 0.71090698f, 0.00988770f, -0.00637817f, 0.27877808f, 0.71600342f, 0.01132202f, + -0.00616455f, 0.27203369f, 0.72100830f, 0.01281738f, -0.00592041f, 0.26535034f, + 0.72586060f, 0.01440430f, -0.00570679f, 0.25872803f, 0.73059082f, 0.01605225f, + -0.00552368f, 0.25219727f, 0.73519897f, 0.01779175f, -0.00531006f, 0.24572754f, + 0.73965454f, 0.01956177f, -0.00506592f, 0.23931885f, 0.74398804f, 0.02139282f, + -0.00485229f, 0.23300171f, 0.74816895f, 0.02331543f, -0.00466919f, 0.22674561f, + 0.75219727f, 0.02529907f, -0.00445557f, 0.22055054f, 0.75610352f, 0.02737427f, + -0.00427246f, 0.21444702f, 0.75985718f, 0.02951050f, -0.00405884f, 0.20843506f, + 0.76348877f, 0.03173828f, -0.00387573f, 0.20248413f, 0.76693726f, 0.03402710f, + -0.00369263f, 0.19659424f, 0.77023315f, 0.03640747f, -0.00350952f, 0.19082642f, + 0.77337646f, 0.03887939f, -0.00332642f, 0.18511963f, 0.77636719f, 0.04141235f, + -0.00314331f, 0.17950439f, 0.77920532f, 0.04400635f, -0.00299072f, 0.17398071f, + 0.78189087f, 0.04672241f, -0.00280762f, 0.16851807f, 0.78442383f, 0.04949951f, + -0.00265503f, 0.16317749f, 0.78674316f, 0.05236816f, -0.00247192f, 0.15789795f, + 0.78894043f, 0.05532837f, -0.00231934f, 0.15270996f, 0.79098511f, 0.05834961f, + -0.00219727f, 0.14761353f, 0.79284668f, 0.06149292f, -0.00204468f, 0.14260864f, + 0.79452515f, 0.06469727f, -0.00192261f, 0.13769531f, 0.79605103f, 0.06796265f, + -0.00177002f, 0.13287354f, 0.79742432f, 0.07135010f, -0.00164795f, 0.12814331f, + 0.79861450f, 0.07479858f, -0.00152588f, 0.12347412f, 0.79962158f, 0.07836914f, + -0.00140381f, 0.11892700f, 0.80047607f, 0.08203125f, -0.00128174f, 0.11447144f, + 0.80117798f, 0.08578491f, -0.00115967f, 0.11010742f, 0.80169678f, 0.08959961f, + -0.00103760f, 0.10580444f, 0.80203247f, 0.09350586f, -0.00097656f, 0.10159302f, + 0.80221558f, 0.09750366f, + }; + + const auto get_lut = [&]() -> std::span { + if (sample_rate_ratio <= 1.0f) { + return std::span(lut2.data(), lut2.size()); + } else if (sample_rate_ratio < 1.3f) { + return std::span(lut1.data(), lut1.size()); + } else { + return std::span(lut0.data(), lut0.size()); + } + }; + + auto lut{get_lut()}; + u32 read_index{0}; + for (u32 i = 0; i < samples_to_write; i++) { + const auto lut_index{(fraction.get_frac() >> 8) * 4}; + const Common::FixedPoint<56, 8> sample0{input[read_index + 0] * lut[lut_index + 0]}; + const Common::FixedPoint<56, 8> sample1{input[read_index + 1] * lut[lut_index + 1]}; + const Common::FixedPoint<56, 8> sample2{input[read_index + 2] * lut[lut_index + 2]}; + const Common::FixedPoint<56, 8> sample3{input[read_index + 3] * lut[lut_index + 3]}; + output[i] = (sample0 + sample1 + sample2 + sample3).to_int_floor(); + fraction += sample_rate_ratio; + read_index += static_cast(fraction.to_int_floor()); + fraction.clear_int(); + } +} + +static void ResampleHighQuality(std::span output, std::span input, + const Common::FixedPoint<49, 15>& sample_rate_ratio, + Common::FixedPoint<49, 15>& fraction, const u32 samples_to_write) { + static constexpr std::array lut0 = { + -0.01776123f, -0.00070190f, 0.26672363f, 0.50006104f, 0.26956177f, 0.00024414f, + -0.01800537f, 0.00000000f, -0.01748657f, -0.00164795f, 0.26388550f, 0.50003052f, + 0.27236938f, 0.00122070f, -0.01824951f, -0.00003052f, -0.01724243f, -0.00256348f, + 0.26107788f, 0.49996948f, 0.27520752f, 0.00219727f, -0.01849365f, -0.00003052f, + -0.01699829f, -0.00344849f, 0.25823975f, 0.49984741f, 0.27801514f, 0.00320435f, + -0.01873779f, -0.00006104f, -0.01675415f, -0.00433350f, 0.25543213f, 0.49972534f, + 0.28085327f, 0.00424194f, -0.01898193f, -0.00006104f, -0.01651001f, -0.00518799f, + 0.25259399f, 0.49954224f, 0.28366089f, 0.00527954f, -0.01922607f, -0.00009155f, + -0.01626587f, -0.00604248f, 0.24978638f, 0.49932861f, 0.28646851f, 0.00634766f, + -0.01947021f, -0.00012207f, -0.01602173f, -0.00686646f, 0.24697876f, 0.49908447f, + 0.28930664f, 0.00744629f, -0.01971436f, -0.00015259f, -0.01574707f, -0.00765991f, + 0.24414062f, 0.49877930f, 0.29211426f, 0.00854492f, -0.01995850f, -0.00015259f, + -0.01550293f, -0.00845337f, 0.24133301f, 0.49847412f, 0.29492188f, 0.00967407f, + -0.02020264f, -0.00018311f, -0.01525879f, -0.00921631f, 0.23852539f, 0.49810791f, + 0.29772949f, 0.01083374f, -0.02044678f, -0.00021362f, -0.01501465f, -0.00997925f, + 0.23571777f, 0.49774170f, 0.30050659f, 0.01199341f, -0.02069092f, -0.00024414f, + -0.01477051f, -0.01071167f, 0.23291016f, 0.49731445f, 0.30331421f, 0.01318359f, + -0.02093506f, -0.00027466f, -0.01452637f, -0.01141357f, 0.23010254f, 0.49685669f, + 0.30609131f, 0.01437378f, -0.02117920f, -0.00030518f, -0.01428223f, -0.01211548f, + 0.22732544f, 0.49636841f, 0.30886841f, 0.01559448f, -0.02142334f, -0.00033569f, + -0.01403809f, -0.01278687f, 0.22451782f, 0.49581909f, 0.31164551f, 0.01684570f, + -0.02163696f, -0.00039673f, -0.01379395f, -0.01345825f, 0.22174072f, 0.49526978f, + 0.31442261f, 0.01809692f, -0.02188110f, -0.00042725f, -0.01358032f, -0.01409912f, + 0.21896362f, 0.49465942f, 0.31719971f, 0.01937866f, -0.02209473f, -0.00045776f, + -0.01333618f, -0.01473999f, 0.21618652f, 0.49404907f, 0.31994629f, 0.02069092f, + -0.02233887f, -0.00048828f, -0.01309204f, -0.01535034f, 0.21343994f, 0.49337769f, + 0.32269287f, 0.02203369f, -0.02255249f, -0.00054932f, -0.01284790f, -0.01596069f, + 0.21066284f, 0.49267578f, 0.32543945f, 0.02337646f, -0.02279663f, -0.00057983f, + -0.01263428f, -0.01654053f, 0.20791626f, 0.49194336f, 0.32818604f, 0.02471924f, + -0.02301025f, -0.00064087f, -0.01239014f, -0.01708984f, 0.20516968f, 0.49118042f, + 0.33090210f, 0.02612305f, -0.02322388f, -0.00067139f, -0.01214600f, -0.01763916f, + 0.20242310f, 0.49035645f, 0.33361816f, 0.02752686f, -0.02343750f, -0.00073242f, + -0.01193237f, -0.01818848f, 0.19970703f, 0.48953247f, 0.33633423f, 0.02896118f, + -0.02365112f, -0.00079346f, -0.01168823f, -0.01867676f, 0.19696045f, 0.48864746f, + 0.33901978f, 0.03039551f, -0.02386475f, -0.00082397f, -0.01147461f, -0.01919556f, + 0.19427490f, 0.48776245f, 0.34170532f, 0.03186035f, -0.02407837f, -0.00088501f, + -0.01123047f, -0.01968384f, 0.19155884f, 0.48681641f, 0.34439087f, 0.03335571f, + -0.02429199f, -0.00094604f, -0.01101685f, -0.02014160f, 0.18887329f, 0.48583984f, + 0.34704590f, 0.03485107f, -0.02447510f, -0.00100708f, -0.01080322f, -0.02059937f, + 0.18615723f, 0.48483276f, 0.34970093f, 0.03637695f, -0.02468872f, -0.00106812f, + -0.01058960f, -0.02102661f, 0.18350220f, 0.48379517f, 0.35235596f, 0.03793335f, + -0.02487183f, -0.00112915f, -0.01034546f, -0.02145386f, 0.18081665f, 0.48272705f, + 0.35498047f, 0.03948975f, -0.02505493f, -0.00119019f, -0.01013184f, -0.02188110f, + 0.17816162f, 0.48162842f, 0.35760498f, 0.04107666f, -0.02523804f, -0.00125122f, + -0.00991821f, -0.02227783f, 0.17550659f, 0.48049927f, 0.36019897f, 0.04269409f, + -0.02542114f, -0.00131226f, -0.00970459f, -0.02264404f, 0.17288208f, 0.47933960f, + 0.36279297f, 0.04431152f, -0.02560425f, -0.00140381f, -0.00952148f, -0.02301025f, + 0.17025757f, 0.47814941f, 0.36538696f, 0.04595947f, -0.02578735f, -0.00146484f, + -0.00930786f, -0.02337646f, 0.16763306f, 0.47689819f, 0.36795044f, 0.04763794f, + -0.02593994f, -0.00152588f, -0.00909424f, -0.02371216f, 0.16503906f, 0.47564697f, + 0.37048340f, 0.04931641f, -0.02609253f, -0.00161743f, -0.00888062f, -0.02401733f, + 0.16244507f, 0.47436523f, 0.37304688f, 0.05102539f, -0.02627563f, -0.00170898f, + -0.00869751f, -0.02435303f, 0.15988159f, 0.47302246f, 0.37554932f, 0.05276489f, + -0.02642822f, -0.00177002f, -0.00848389f, -0.02462769f, 0.15731812f, 0.47167969f, + 0.37805176f, 0.05450439f, -0.02658081f, -0.00186157f, -0.00830078f, -0.02493286f, + 0.15475464f, 0.47027588f, 0.38055420f, 0.05627441f, -0.02670288f, -0.00195312f, + -0.00808716f, -0.02520752f, 0.15222168f, 0.46887207f, 0.38302612f, 0.05804443f, + -0.02685547f, -0.00204468f, -0.00790405f, -0.02545166f, 0.14968872f, 0.46743774f, + 0.38546753f, 0.05987549f, -0.02697754f, -0.00213623f, -0.00772095f, -0.02569580f, + 0.14718628f, 0.46594238f, 0.38790894f, 0.06170654f, -0.02709961f, -0.00222778f, + -0.00753784f, -0.02593994f, 0.14468384f, 0.46444702f, 0.39031982f, 0.06353760f, + -0.02722168f, -0.00231934f, -0.00735474f, -0.02615356f, 0.14218140f, 0.46289062f, + 0.39273071f, 0.06539917f, -0.02734375f, -0.00241089f, -0.00717163f, -0.02636719f, + 0.13970947f, 0.46133423f, 0.39511108f, 0.06729126f, -0.02743530f, -0.00250244f, + -0.00698853f, -0.02655029f, 0.13726807f, 0.45974731f, 0.39749146f, 0.06918335f, + -0.02755737f, -0.00259399f, -0.00680542f, -0.02673340f, 0.13479614f, 0.45812988f, + 0.39984131f, 0.07113647f, -0.02764893f, -0.00271606f, -0.00662231f, -0.02691650f, + 0.13238525f, 0.45648193f, 0.40216064f, 0.07305908f, -0.02774048f, -0.00280762f, + -0.00643921f, -0.02706909f, 0.12997437f, 0.45480347f, 0.40447998f, 0.07504272f, + -0.02780151f, -0.00292969f, -0.00628662f, -0.02722168f, 0.12756348f, 0.45309448f, + 0.40676880f, 0.07699585f, -0.02789307f, -0.00305176f, -0.00610352f, -0.02734375f, + 0.12518311f, 0.45135498f, 0.40902710f, 0.07901001f, -0.02795410f, -0.00314331f, + -0.00595093f, -0.02746582f, 0.12280273f, 0.44958496f, 0.41128540f, 0.08102417f, + -0.02801514f, -0.00326538f, -0.00579834f, -0.02758789f, 0.12045288f, 0.44778442f, + 0.41351318f, 0.08306885f, -0.02804565f, -0.00338745f, -0.00561523f, -0.02770996f, + 0.11813354f, 0.44598389f, 0.41571045f, 0.08511353f, -0.02810669f, -0.00350952f, + -0.00546265f, -0.02780151f, 0.11581421f, 0.44412231f, 0.41787720f, 0.08718872f, + -0.02813721f, -0.00363159f, -0.00531006f, -0.02786255f, 0.11349487f, 0.44226074f, + 0.42004395f, 0.08929443f, -0.02816772f, -0.00375366f, -0.00515747f, -0.02795410f, + 0.11120605f, 0.44036865f, 0.42218018f, 0.09140015f, -0.02816772f, -0.00387573f, + -0.00500488f, -0.02801514f, 0.10894775f, 0.43844604f, 0.42431641f, 0.09353638f, + -0.02819824f, -0.00402832f, -0.00485229f, -0.02807617f, 0.10668945f, 0.43649292f, + 0.42639160f, 0.09570312f, -0.02819824f, -0.00415039f, -0.00469971f, -0.02810669f, + 0.10446167f, 0.43453979f, 0.42846680f, 0.09786987f, -0.02819824f, -0.00427246f, + -0.00457764f, -0.02813721f, 0.10223389f, 0.43252563f, 0.43051147f, 0.10003662f, + -0.02816772f, -0.00442505f, -0.00442505f, -0.02816772f, 0.10003662f, 0.43051147f, + 0.43252563f, 0.10223389f, -0.02813721f, -0.00457764f, -0.00427246f, -0.02819824f, + 0.09786987f, 0.42846680f, 0.43453979f, 0.10446167f, -0.02810669f, -0.00469971f, + -0.00415039f, -0.02819824f, 0.09570312f, 0.42639160f, 0.43649292f, 0.10668945f, + -0.02807617f, -0.00485229f, -0.00402832f, -0.02819824f, 0.09353638f, 0.42431641f, + 0.43844604f, 0.10894775f, -0.02801514f, -0.00500488f, -0.00387573f, -0.02816772f, + 0.09140015f, 0.42218018f, 0.44036865f, 0.11120605f, -0.02795410f, -0.00515747f, + -0.00375366f, -0.02816772f, 0.08929443f, 0.42004395f, 0.44226074f, 0.11349487f, + -0.02786255f, -0.00531006f, -0.00363159f, -0.02813721f, 0.08718872f, 0.41787720f, + 0.44412231f, 0.11581421f, -0.02780151f, -0.00546265f, -0.00350952f, -0.02810669f, + 0.08511353f, 0.41571045f, 0.44598389f, 0.11813354f, -0.02770996f, -0.00561523f, + -0.00338745f, -0.02804565f, 0.08306885f, 0.41351318f, 0.44778442f, 0.12045288f, + -0.02758789f, -0.00579834f, -0.00326538f, -0.02801514f, 0.08102417f, 0.41128540f, + 0.44958496f, 0.12280273f, -0.02746582f, -0.00595093f, -0.00314331f, -0.02795410f, + 0.07901001f, 0.40902710f, 0.45135498f, 0.12518311f, -0.02734375f, -0.00610352f, + -0.00305176f, -0.02789307f, 0.07699585f, 0.40676880f, 0.45309448f, 0.12756348f, + -0.02722168f, -0.00628662f, -0.00292969f, -0.02780151f, 0.07504272f, 0.40447998f, + 0.45480347f, 0.12997437f, -0.02706909f, -0.00643921f, -0.00280762f, -0.02774048f, + 0.07305908f, 0.40216064f, 0.45648193f, 0.13238525f, -0.02691650f, -0.00662231f, + -0.00271606f, -0.02764893f, 0.07113647f, 0.39984131f, 0.45812988f, 0.13479614f, + -0.02673340f, -0.00680542f, -0.00259399f, -0.02755737f, 0.06918335f, 0.39749146f, + 0.45974731f, 0.13726807f, -0.02655029f, -0.00698853f, -0.00250244f, -0.02743530f, + 0.06729126f, 0.39511108f, 0.46133423f, 0.13970947f, -0.02636719f, -0.00717163f, + -0.00241089f, -0.02734375f, 0.06539917f, 0.39273071f, 0.46289062f, 0.14218140f, + -0.02615356f, -0.00735474f, -0.00231934f, -0.02722168f, 0.06353760f, 0.39031982f, + 0.46444702f, 0.14468384f, -0.02593994f, -0.00753784f, -0.00222778f, -0.02709961f, + 0.06170654f, 0.38790894f, 0.46594238f, 0.14718628f, -0.02569580f, -0.00772095f, + -0.00213623f, -0.02697754f, 0.05987549f, 0.38546753f, 0.46743774f, 0.14968872f, + -0.02545166f, -0.00790405f, -0.00204468f, -0.02685547f, 0.05804443f, 0.38302612f, + 0.46887207f, 0.15222168f, -0.02520752f, -0.00808716f, -0.00195312f, -0.02670288f, + 0.05627441f, 0.38055420f, 0.47027588f, 0.15475464f, -0.02493286f, -0.00830078f, + -0.00186157f, -0.02658081f, 0.05450439f, 0.37805176f, 0.47167969f, 0.15731812f, + -0.02462769f, -0.00848389f, -0.00177002f, -0.02642822f, 0.05276489f, 0.37554932f, + 0.47302246f, 0.15988159f, -0.02435303f, -0.00869751f, -0.00170898f, -0.02627563f, + 0.05102539f, 0.37304688f, 0.47436523f, 0.16244507f, -0.02401733f, -0.00888062f, + -0.00161743f, -0.02609253f, 0.04931641f, 0.37048340f, 0.47564697f, 0.16503906f, + -0.02371216f, -0.00909424f, -0.00152588f, -0.02593994f, 0.04763794f, 0.36795044f, + 0.47689819f, 0.16763306f, -0.02337646f, -0.00930786f, -0.00146484f, -0.02578735f, + 0.04595947f, 0.36538696f, 0.47814941f, 0.17025757f, -0.02301025f, -0.00952148f, + -0.00140381f, -0.02560425f, 0.04431152f, 0.36279297f, 0.47933960f, 0.17288208f, + -0.02264404f, -0.00970459f, -0.00131226f, -0.02542114f, 0.04269409f, 0.36019897f, + 0.48049927f, 0.17550659f, -0.02227783f, -0.00991821f, -0.00125122f, -0.02523804f, + 0.04107666f, 0.35760498f, 0.48162842f, 0.17816162f, -0.02188110f, -0.01013184f, + -0.00119019f, -0.02505493f, 0.03948975f, 0.35498047f, 0.48272705f, 0.18081665f, + -0.02145386f, -0.01034546f, -0.00112915f, -0.02487183f, 0.03793335f, 0.35235596f, + 0.48379517f, 0.18350220f, -0.02102661f, -0.01058960f, -0.00106812f, -0.02468872f, + 0.03637695f, 0.34970093f, 0.48483276f, 0.18615723f, -0.02059937f, -0.01080322f, + -0.00100708f, -0.02447510f, 0.03485107f, 0.34704590f, 0.48583984f, 0.18887329f, + -0.02014160f, -0.01101685f, -0.00094604f, -0.02429199f, 0.03335571f, 0.34439087f, + 0.48681641f, 0.19155884f, -0.01968384f, -0.01123047f, -0.00088501f, -0.02407837f, + 0.03186035f, 0.34170532f, 0.48776245f, 0.19427490f, -0.01919556f, -0.01147461f, + -0.00082397f, -0.02386475f, 0.03039551f, 0.33901978f, 0.48864746f, 0.19696045f, + -0.01867676f, -0.01168823f, -0.00079346f, -0.02365112f, 0.02896118f, 0.33633423f, + 0.48953247f, 0.19970703f, -0.01818848f, -0.01193237f, -0.00073242f, -0.02343750f, + 0.02752686f, 0.33361816f, 0.49035645f, 0.20242310f, -0.01763916f, -0.01214600f, + -0.00067139f, -0.02322388f, 0.02612305f, 0.33090210f, 0.49118042f, 0.20516968f, + -0.01708984f, -0.01239014f, -0.00064087f, -0.02301025f, 0.02471924f, 0.32818604f, + 0.49194336f, 0.20791626f, -0.01654053f, -0.01263428f, -0.00057983f, -0.02279663f, + 0.02337646f, 0.32543945f, 0.49267578f, 0.21066284f, -0.01596069f, -0.01284790f, + -0.00054932f, -0.02255249f, 0.02203369f, 0.32269287f, 0.49337769f, 0.21343994f, + -0.01535034f, -0.01309204f, -0.00048828f, -0.02233887f, 0.02069092f, 0.31994629f, + 0.49404907f, 0.21618652f, -0.01473999f, -0.01333618f, -0.00045776f, -0.02209473f, + 0.01937866f, 0.31719971f, 0.49465942f, 0.21896362f, -0.01409912f, -0.01358032f, + -0.00042725f, -0.02188110f, 0.01809692f, 0.31442261f, 0.49526978f, 0.22174072f, + -0.01345825f, -0.01379395f, -0.00039673f, -0.02163696f, 0.01684570f, 0.31164551f, + 0.49581909f, 0.22451782f, -0.01278687f, -0.01403809f, -0.00033569f, -0.02142334f, + 0.01559448f, 0.30886841f, 0.49636841f, 0.22732544f, -0.01211548f, -0.01428223f, + -0.00030518f, -0.02117920f, 0.01437378f, 0.30609131f, 0.49685669f, 0.23010254f, + -0.01141357f, -0.01452637f, -0.00027466f, -0.02093506f, 0.01318359f, 0.30331421f, + 0.49731445f, 0.23291016f, -0.01071167f, -0.01477051f, -0.00024414f, -0.02069092f, + 0.01199341f, 0.30050659f, 0.49774170f, 0.23571777f, -0.00997925f, -0.01501465f, + -0.00021362f, -0.02044678f, 0.01083374f, 0.29772949f, 0.49810791f, 0.23852539f, + -0.00921631f, -0.01525879f, -0.00018311f, -0.02020264f, 0.00967407f, 0.29492188f, + 0.49847412f, 0.24133301f, -0.00845337f, -0.01550293f, -0.00015259f, -0.01995850f, + 0.00854492f, 0.29211426f, 0.49877930f, 0.24414062f, -0.00765991f, -0.01574707f, + -0.00015259f, -0.01971436f, 0.00744629f, 0.28930664f, 0.49908447f, 0.24697876f, + -0.00686646f, -0.01602173f, -0.00012207f, -0.01947021f, 0.00634766f, 0.28646851f, + 0.49932861f, 0.24978638f, -0.00604248f, -0.01626587f, -0.00009155f, -0.01922607f, + 0.00527954f, 0.28366089f, 0.49954224f, 0.25259399f, -0.00518799f, -0.01651001f, + -0.00006104f, -0.01898193f, 0.00424194f, 0.28085327f, 0.49972534f, 0.25543213f, + -0.00433350f, -0.01675415f, -0.00006104f, -0.01873779f, 0.00320435f, 0.27801514f, + 0.49984741f, 0.25823975f, -0.00344849f, -0.01699829f, -0.00003052f, -0.01849365f, + 0.00219727f, 0.27520752f, 0.49996948f, 0.26107788f, -0.00256348f, -0.01724243f, + -0.00003052f, -0.01824951f, 0.00122070f, 0.27236938f, 0.50003052f, 0.26388550f, + -0.00164795f, -0.01748657f, 0.00000000f, -0.01800537f, 0.00024414f, 0.26956177f, + 0.50006104f, 0.26672363f, -0.00070190f, -0.01776123f, + }; + + static constexpr std::array lut1 = { + 0.01275635f, -0.07745361f, 0.18670654f, 0.75119019f, 0.19219971f, -0.07821655f, + 0.01272583f, 0.00000000f, 0.01281738f, -0.07666016f, 0.18124390f, 0.75106812f, + 0.19772339f, -0.07897949f, 0.01266479f, 0.00003052f, 0.01284790f, -0.07583618f, + 0.17581177f, 0.75088501f, 0.20330811f, -0.07971191f, 0.01257324f, 0.00006104f, + 0.01287842f, -0.07501221f, 0.17044067f, 0.75057983f, 0.20892334f, -0.08041382f, + 0.01248169f, 0.00009155f, 0.01290894f, -0.07415771f, 0.16510010f, 0.75018311f, + 0.21453857f, -0.08111572f, 0.01239014f, 0.00012207f, 0.01290894f, -0.07330322f, + 0.15979004f, 0.74966431f, 0.22021484f, -0.08178711f, 0.01229858f, 0.00015259f, + 0.01290894f, -0.07241821f, 0.15454102f, 0.74908447f, 0.22592163f, -0.08242798f, + 0.01217651f, 0.00018311f, 0.01290894f, -0.07150269f, 0.14932251f, 0.74838257f, + 0.23165894f, -0.08303833f, 0.01205444f, 0.00021362f, 0.01290894f, -0.07058716f, + 0.14416504f, 0.74755859f, 0.23742676f, -0.08364868f, 0.01193237f, 0.00024414f, + 0.01287842f, -0.06967163f, 0.13903809f, 0.74667358f, 0.24322510f, -0.08419800f, + 0.01177979f, 0.00027466f, 0.01284790f, -0.06872559f, 0.13397217f, 0.74566650f, + 0.24905396f, -0.08474731f, 0.01162720f, 0.00033569f, 0.01281738f, -0.06777954f, + 0.12893677f, 0.74456787f, 0.25491333f, -0.08526611f, 0.01147461f, 0.00036621f, + 0.01278687f, -0.06683350f, 0.12396240f, 0.74337769f, 0.26077271f, -0.08575439f, + 0.01129150f, 0.00042725f, 0.01275635f, -0.06585693f, 0.11901855f, 0.74206543f, + 0.26669312f, -0.08621216f, 0.01110840f, 0.00045776f, 0.01269531f, -0.06488037f, + 0.11413574f, 0.74069214f, 0.27261353f, -0.08663940f, 0.01092529f, 0.00051880f, + 0.01263428f, -0.06387329f, 0.10931396f, 0.73919678f, 0.27853394f, -0.08700562f, + 0.01071167f, 0.00057983f, 0.01257324f, -0.06286621f, 0.10452271f, 0.73760986f, + 0.28451538f, -0.08737183f, 0.01049805f, 0.00064087f, 0.01251221f, -0.06185913f, + 0.09979248f, 0.73593140f, 0.29049683f, -0.08770752f, 0.01025391f, 0.00067139f, + 0.01242065f, -0.06082153f, 0.09512329f, 0.73413086f, 0.29647827f, -0.08801270f, + 0.01000977f, 0.00073242f, 0.01232910f, -0.05981445f, 0.09051514f, 0.73226929f, + 0.30249023f, -0.08828735f, 0.00973511f, 0.00079346f, 0.01226807f, -0.05877686f, + 0.08593750f, 0.73028564f, 0.30853271f, -0.08850098f, 0.00949097f, 0.00088501f, + 0.01214600f, -0.05773926f, 0.08142090f, 0.72824097f, 0.31457520f, -0.08871460f, + 0.00918579f, 0.00094604f, 0.01205444f, -0.05670166f, 0.07696533f, 0.72607422f, + 0.32061768f, -0.08886719f, 0.00891113f, 0.00100708f, 0.01196289f, -0.05563354f, + 0.07257080f, 0.72381592f, 0.32669067f, -0.08898926f, 0.00860596f, 0.00106812f, + 0.01187134f, -0.05459595f, 0.06820679f, 0.72146606f, 0.33276367f, -0.08908081f, + 0.00827026f, 0.00115967f, 0.01174927f, -0.05352783f, 0.06393433f, 0.71902466f, + 0.33883667f, -0.08911133f, 0.00796509f, 0.00122070f, 0.01162720f, -0.05245972f, + 0.05969238f, 0.71649170f, 0.34494019f, -0.08914185f, 0.00759888f, 0.00131226f, + 0.01150513f, -0.05139160f, 0.05551147f, 0.71389771f, 0.35101318f, -0.08911133f, + 0.00726318f, 0.00137329f, 0.01138306f, -0.05032349f, 0.05139160f, 0.71118164f, + 0.35711670f, -0.08901978f, 0.00686646f, 0.00146484f, 0.01126099f, -0.04928589f, + 0.04733276f, 0.70837402f, 0.36322021f, -0.08892822f, 0.00650024f, 0.00155640f, + 0.01113892f, -0.04821777f, 0.04333496f, 0.70550537f, 0.36932373f, -0.08877563f, + 0.00610352f, 0.00164795f, 0.01101685f, -0.04714966f, 0.03939819f, 0.70251465f, + 0.37542725f, -0.08856201f, 0.00567627f, 0.00173950f, 0.01086426f, -0.04608154f, + 0.03549194f, 0.69946289f, 0.38153076f, -0.08834839f, 0.00527954f, 0.00183105f, + 0.01074219f, -0.04501343f, 0.03167725f, 0.69631958f, 0.38763428f, -0.08804321f, + 0.00482178f, 0.00192261f, 0.01058960f, -0.04394531f, 0.02792358f, 0.69308472f, + 0.39370728f, -0.08773804f, 0.00436401f, 0.00201416f, 0.01043701f, -0.04287720f, + 0.02420044f, 0.68975830f, 0.39981079f, -0.08737183f, 0.00390625f, 0.00210571f, + 0.01031494f, -0.04180908f, 0.02056885f, 0.68637085f, 0.40588379f, -0.08694458f, + 0.00344849f, 0.00222778f, 0.01016235f, -0.04074097f, 0.01699829f, 0.68289185f, + 0.41195679f, -0.08648682f, 0.00296021f, 0.00231934f, 0.01000977f, -0.03970337f, + 0.01345825f, 0.67932129f, 0.41802979f, -0.08596802f, 0.00244141f, 0.00244141f, + 0.00985718f, -0.03863525f, 0.01000977f, 0.67568970f, 0.42407227f, -0.08541870f, + 0.00192261f, 0.00253296f, 0.00970459f, -0.03759766f, 0.00662231f, 0.67196655f, + 0.43011475f, -0.08480835f, 0.00140381f, 0.00265503f, 0.00955200f, -0.03652954f, + 0.00326538f, 0.66815186f, 0.43612671f, -0.08416748f, 0.00085449f, 0.00277710f, + 0.00936890f, -0.03549194f, 0.00000000f, 0.66427612f, 0.44213867f, -0.08346558f, + 0.00027466f, 0.00289917f, 0.00921631f, -0.03445435f, -0.00320435f, 0.66030884f, + 0.44812012f, -0.08270264f, -0.00027466f, 0.00299072f, 0.00906372f, -0.03344727f, + -0.00634766f, 0.65631104f, 0.45407104f, -0.08190918f, -0.00088501f, 0.00311279f, + 0.00891113f, -0.03240967f, -0.00946045f, 0.65219116f, 0.46002197f, -0.08105469f, + -0.00146484f, 0.00323486f, 0.00872803f, -0.03140259f, -0.01248169f, 0.64801025f, + 0.46594238f, -0.08013916f, -0.00210571f, 0.00338745f, 0.00857544f, -0.03039551f, + -0.01544189f, 0.64376831f, 0.47183228f, -0.07919312f, -0.00271606f, 0.00350952f, + 0.00842285f, -0.02938843f, -0.01834106f, 0.63946533f, 0.47772217f, -0.07818604f, + -0.00335693f, 0.00363159f, 0.00823975f, -0.02838135f, -0.02117920f, 0.63507080f, + 0.48358154f, -0.07711792f, -0.00402832f, 0.00375366f, 0.00808716f, -0.02740479f, + -0.02395630f, 0.63061523f, 0.48937988f, -0.07598877f, -0.00469971f, 0.00390625f, + 0.00793457f, -0.02642822f, -0.02667236f, 0.62609863f, 0.49517822f, -0.07482910f, + -0.00537109f, 0.00402832f, 0.00775146f, -0.02545166f, -0.02932739f, 0.62152100f, + 0.50094604f, -0.07357788f, -0.00607300f, 0.00418091f, 0.00759888f, -0.02450562f, + -0.03192139f, 0.61685181f, 0.50665283f, -0.07229614f, -0.00677490f, 0.00430298f, + 0.00741577f, -0.02352905f, -0.03445435f, 0.61215210f, 0.51235962f, -0.07098389f, + -0.00750732f, 0.00445557f, 0.00726318f, -0.02258301f, -0.03689575f, 0.60736084f, + 0.51800537f, -0.06958008f, -0.00823975f, 0.00460815f, 0.00711060f, -0.02166748f, + -0.03930664f, 0.60253906f, 0.52362061f, -0.06811523f, -0.00897217f, 0.00476074f, + 0.00692749f, -0.02075195f, -0.04165649f, 0.59762573f, 0.52920532f, -0.06661987f, + -0.00973511f, 0.00488281f, 0.00677490f, -0.01983643f, -0.04394531f, 0.59268188f, + 0.53475952f, -0.06506348f, -0.01052856f, 0.00503540f, 0.00662231f, -0.01892090f, + -0.04617310f, 0.58767700f, 0.54025269f, -0.06344604f, -0.01129150f, 0.00518799f, + 0.00643921f, -0.01803589f, -0.04830933f, 0.58261108f, 0.54571533f, -0.06173706f, + -0.01208496f, 0.00534058f, 0.00628662f, -0.01715088f, -0.05041504f, 0.57748413f, + 0.55111694f, -0.05999756f, -0.01290894f, 0.00549316f, 0.00613403f, -0.01626587f, + -0.05245972f, 0.57232666f, 0.55648804f, -0.05819702f, -0.01373291f, 0.00564575f, + 0.00598145f, -0.01541138f, -0.05444336f, 0.56707764f, 0.56182861f, -0.05636597f, + -0.01455688f, 0.00582886f, 0.00582886f, -0.01455688f, -0.05636597f, 0.56182861f, + 0.56707764f, -0.05444336f, -0.01541138f, 0.00598145f, 0.00564575f, -0.01373291f, + -0.05819702f, 0.55648804f, 0.57232666f, -0.05245972f, -0.01626587f, 0.00613403f, + 0.00549316f, -0.01290894f, -0.05999756f, 0.55111694f, 0.57748413f, -0.05041504f, + -0.01715088f, 0.00628662f, 0.00534058f, -0.01208496f, -0.06173706f, 0.54571533f, + 0.58261108f, -0.04830933f, -0.01803589f, 0.00643921f, 0.00518799f, -0.01129150f, + -0.06344604f, 0.54025269f, 0.58767700f, -0.04617310f, -0.01892090f, 0.00662231f, + 0.00503540f, -0.01052856f, -0.06506348f, 0.53475952f, 0.59268188f, -0.04394531f, + -0.01983643f, 0.00677490f, 0.00488281f, -0.00973511f, -0.06661987f, 0.52920532f, + 0.59762573f, -0.04165649f, -0.02075195f, 0.00692749f, 0.00476074f, -0.00897217f, + -0.06811523f, 0.52362061f, 0.60253906f, -0.03930664f, -0.02166748f, 0.00711060f, + 0.00460815f, -0.00823975f, -0.06958008f, 0.51800537f, 0.60736084f, -0.03689575f, + -0.02258301f, 0.00726318f, 0.00445557f, -0.00750732f, -0.07098389f, 0.51235962f, + 0.61215210f, -0.03445435f, -0.02352905f, 0.00741577f, 0.00430298f, -0.00677490f, + -0.07229614f, 0.50665283f, 0.61685181f, -0.03192139f, -0.02450562f, 0.00759888f, + 0.00418091f, -0.00607300f, -0.07357788f, 0.50094604f, 0.62152100f, -0.02932739f, + -0.02545166f, 0.00775146f, 0.00402832f, -0.00537109f, -0.07482910f, 0.49517822f, + 0.62609863f, -0.02667236f, -0.02642822f, 0.00793457f, 0.00390625f, -0.00469971f, + -0.07598877f, 0.48937988f, 0.63061523f, -0.02395630f, -0.02740479f, 0.00808716f, + 0.00375366f, -0.00402832f, -0.07711792f, 0.48358154f, 0.63507080f, -0.02117920f, + -0.02838135f, 0.00823975f, 0.00363159f, -0.00335693f, -0.07818604f, 0.47772217f, + 0.63946533f, -0.01834106f, -0.02938843f, 0.00842285f, 0.00350952f, -0.00271606f, + -0.07919312f, 0.47183228f, 0.64376831f, -0.01544189f, -0.03039551f, 0.00857544f, + 0.00338745f, -0.00210571f, -0.08013916f, 0.46594238f, 0.64801025f, -0.01248169f, + -0.03140259f, 0.00872803f, 0.00323486f, -0.00146484f, -0.08105469f, 0.46002197f, + 0.65219116f, -0.00946045f, -0.03240967f, 0.00891113f, 0.00311279f, -0.00088501f, + -0.08190918f, 0.45407104f, 0.65631104f, -0.00634766f, -0.03344727f, 0.00906372f, + 0.00299072f, -0.00027466f, -0.08270264f, 0.44812012f, 0.66030884f, -0.00320435f, + -0.03445435f, 0.00921631f, 0.00289917f, 0.00027466f, -0.08346558f, 0.44213867f, + 0.66427612f, 0.00000000f, -0.03549194f, 0.00936890f, 0.00277710f, 0.00085449f, + -0.08416748f, 0.43612671f, 0.66815186f, 0.00326538f, -0.03652954f, 0.00955200f, + 0.00265503f, 0.00140381f, -0.08480835f, 0.43011475f, 0.67196655f, 0.00662231f, + -0.03759766f, 0.00970459f, 0.00253296f, 0.00192261f, -0.08541870f, 0.42407227f, + 0.67568970f, 0.01000977f, -0.03863525f, 0.00985718f, 0.00244141f, 0.00244141f, + -0.08596802f, 0.41802979f, 0.67932129f, 0.01345825f, -0.03970337f, 0.01000977f, + 0.00231934f, 0.00296021f, -0.08648682f, 0.41195679f, 0.68289185f, 0.01699829f, + -0.04074097f, 0.01016235f, 0.00222778f, 0.00344849f, -0.08694458f, 0.40588379f, + 0.68637085f, 0.02056885f, -0.04180908f, 0.01031494f, 0.00210571f, 0.00390625f, + -0.08737183f, 0.39981079f, 0.68975830f, 0.02420044f, -0.04287720f, 0.01043701f, + 0.00201416f, 0.00436401f, -0.08773804f, 0.39370728f, 0.69308472f, 0.02792358f, + -0.04394531f, 0.01058960f, 0.00192261f, 0.00482178f, -0.08804321f, 0.38763428f, + 0.69631958f, 0.03167725f, -0.04501343f, 0.01074219f, 0.00183105f, 0.00527954f, + -0.08834839f, 0.38153076f, 0.69946289f, 0.03549194f, -0.04608154f, 0.01086426f, + 0.00173950f, 0.00567627f, -0.08856201f, 0.37542725f, 0.70251465f, 0.03939819f, + -0.04714966f, 0.01101685f, 0.00164795f, 0.00610352f, -0.08877563f, 0.36932373f, + 0.70550537f, 0.04333496f, -0.04821777f, 0.01113892f, 0.00155640f, 0.00650024f, + -0.08892822f, 0.36322021f, 0.70837402f, 0.04733276f, -0.04928589f, 0.01126099f, + 0.00146484f, 0.00686646f, -0.08901978f, 0.35711670f, 0.71118164f, 0.05139160f, + -0.05032349f, 0.01138306f, 0.00137329f, 0.00726318f, -0.08911133f, 0.35101318f, + 0.71389771f, 0.05551147f, -0.05139160f, 0.01150513f, 0.00131226f, 0.00759888f, + -0.08914185f, 0.34494019f, 0.71649170f, 0.05969238f, -0.05245972f, 0.01162720f, + 0.00122070f, 0.00796509f, -0.08911133f, 0.33883667f, 0.71902466f, 0.06393433f, + -0.05352783f, 0.01174927f, 0.00115967f, 0.00827026f, -0.08908081f, 0.33276367f, + 0.72146606f, 0.06820679f, -0.05459595f, 0.01187134f, 0.00106812f, 0.00860596f, + -0.08898926f, 0.32669067f, 0.72381592f, 0.07257080f, -0.05563354f, 0.01196289f, + 0.00100708f, 0.00891113f, -0.08886719f, 0.32061768f, 0.72607422f, 0.07696533f, + -0.05670166f, 0.01205444f, 0.00094604f, 0.00918579f, -0.08871460f, 0.31457520f, + 0.72824097f, 0.08142090f, -0.05773926f, 0.01214600f, 0.00088501f, 0.00949097f, + -0.08850098f, 0.30853271f, 0.73028564f, 0.08593750f, -0.05877686f, 0.01226807f, + 0.00079346f, 0.00973511f, -0.08828735f, 0.30249023f, 0.73226929f, 0.09051514f, + -0.05981445f, 0.01232910f, 0.00073242f, 0.01000977f, -0.08801270f, 0.29647827f, + 0.73413086f, 0.09512329f, -0.06082153f, 0.01242065f, 0.00067139f, 0.01025391f, + -0.08770752f, 0.29049683f, 0.73593140f, 0.09979248f, -0.06185913f, 0.01251221f, + 0.00064087f, 0.01049805f, -0.08737183f, 0.28451538f, 0.73760986f, 0.10452271f, + -0.06286621f, 0.01257324f, 0.00057983f, 0.01071167f, -0.08700562f, 0.27853394f, + 0.73919678f, 0.10931396f, -0.06387329f, 0.01263428f, 0.00051880f, 0.01092529f, + -0.08663940f, 0.27261353f, 0.74069214f, 0.11413574f, -0.06488037f, 0.01269531f, + 0.00045776f, 0.01110840f, -0.08621216f, 0.26669312f, 0.74206543f, 0.11901855f, + -0.06585693f, 0.01275635f, 0.00042725f, 0.01129150f, -0.08575439f, 0.26077271f, + 0.74337769f, 0.12396240f, -0.06683350f, 0.01278687f, 0.00036621f, 0.01147461f, + -0.08526611f, 0.25491333f, 0.74456787f, 0.12893677f, -0.06777954f, 0.01281738f, + 0.00033569f, 0.01162720f, -0.08474731f, 0.24905396f, 0.74566650f, 0.13397217f, + -0.06872559f, 0.01284790f, 0.00027466f, 0.01177979f, -0.08419800f, 0.24322510f, + 0.74667358f, 0.13903809f, -0.06967163f, 0.01287842f, 0.00024414f, 0.01193237f, + -0.08364868f, 0.23742676f, 0.74755859f, 0.14416504f, -0.07058716f, 0.01290894f, + 0.00021362f, 0.01205444f, -0.08303833f, 0.23165894f, 0.74838257f, 0.14932251f, + -0.07150269f, 0.01290894f, 0.00018311f, 0.01217651f, -0.08242798f, 0.22592163f, + 0.74908447f, 0.15454102f, -0.07241821f, 0.01290894f, 0.00015259f, 0.01229858f, + -0.08178711f, 0.22021484f, 0.74966431f, 0.15979004f, -0.07330322f, 0.01290894f, + 0.00012207f, 0.01239014f, -0.08111572f, 0.21453857f, 0.75018311f, 0.16510010f, + -0.07415771f, 0.01290894f, 0.00009155f, 0.01248169f, -0.08041382f, 0.20892334f, + 0.75057983f, 0.17044067f, -0.07501221f, 0.01287842f, 0.00006104f, 0.01257324f, + -0.07971191f, 0.20330811f, 0.75088501f, 0.17581177f, -0.07583618f, 0.01284790f, + 0.00003052f, 0.01266479f, -0.07897949f, 0.19772339f, 0.75106812f, 0.18124390f, + -0.07666016f, 0.01281738f, 0.00000000f, 0.01272583f, -0.07821655f, 0.19219971f, + 0.75119019f, 0.18670654f, -0.07745361f, 0.01275635f, + }; + + static constexpr std::array lut2 = { + -0.00036621f, 0.00143433f, -0.00408936f, 0.99996948f, 0.00247192f, -0.00048828f, + 0.00006104f, 0.00000000f, -0.00079346f, 0.00329590f, -0.01052856f, 0.99975586f, + 0.00918579f, -0.00241089f, 0.00051880f, -0.00003052f, -0.00122070f, 0.00512695f, + -0.01684570f, 0.99929810f, 0.01605225f, -0.00439453f, 0.00097656f, -0.00006104f, + -0.00161743f, 0.00689697f, -0.02297974f, 0.99862671f, 0.02304077f, -0.00640869f, + 0.00143433f, -0.00009155f, -0.00201416f, 0.00866699f, -0.02899170f, 0.99774170f, + 0.03018188f, -0.00845337f, 0.00192261f, -0.00015259f, -0.00238037f, 0.01037598f, + -0.03488159f, 0.99664307f, 0.03741455f, -0.01055908f, 0.00241089f, -0.00018311f, + -0.00274658f, 0.01202393f, -0.04061890f, 0.99533081f, 0.04483032f, -0.01266479f, + 0.00292969f, -0.00024414f, -0.00308228f, 0.01364136f, -0.04620361f, 0.99377441f, + 0.05233765f, -0.01483154f, 0.00344849f, -0.00027466f, -0.00341797f, 0.01522827f, + -0.05163574f, 0.99200439f, 0.05999756f, -0.01699829f, 0.00396729f, -0.00033569f, + -0.00375366f, 0.01678467f, -0.05691528f, 0.99002075f, 0.06777954f, -0.01922607f, + 0.00451660f, -0.00039673f, -0.00405884f, 0.01828003f, -0.06207275f, 0.98782349f, + 0.07568359f, -0.02145386f, 0.00506592f, -0.00042725f, -0.00436401f, 0.01971436f, + -0.06707764f, 0.98541260f, 0.08370972f, -0.02374268f, 0.00564575f, -0.00048828f, + -0.00463867f, 0.02114868f, -0.07192993f, 0.98278809f, 0.09185791f, -0.02603149f, + 0.00622559f, -0.00054932f, -0.00494385f, 0.02252197f, -0.07666016f, 0.97991943f, + 0.10012817f, -0.02835083f, 0.00680542f, -0.00061035f, -0.00518799f, 0.02383423f, + -0.08123779f, 0.97686768f, 0.10848999f, -0.03073120f, 0.00738525f, -0.00070190f, + -0.00543213f, 0.02511597f, -0.08566284f, 0.97360229f, 0.11700439f, -0.03308105f, + 0.00799561f, -0.00076294f, -0.00567627f, 0.02636719f, -0.08993530f, 0.97012329f, + 0.12561035f, -0.03549194f, 0.00860596f, -0.00082397f, -0.00592041f, 0.02755737f, + -0.09405518f, 0.96643066f, 0.13436890f, -0.03790283f, 0.00924683f, -0.00091553f, + -0.00613403f, 0.02868652f, -0.09805298f, 0.96252441f, 0.14318848f, -0.04034424f, + 0.00985718f, -0.00097656f, -0.00631714f, 0.02981567f, -0.10189819f, 0.95843506f, + 0.15213013f, -0.04281616f, 0.01049805f, -0.00106812f, -0.00653076f, 0.03085327f, + -0.10559082f, 0.95413208f, 0.16119385f, -0.04528809f, 0.01113892f, -0.00112915f, + -0.00671387f, 0.03189087f, -0.10916138f, 0.94961548f, 0.17034912f, -0.04779053f, + 0.01181030f, -0.00122070f, -0.00686646f, 0.03286743f, -0.11254883f, 0.94491577f, + 0.17959595f, -0.05029297f, 0.01248169f, -0.00131226f, -0.00701904f, 0.03378296f, + -0.11584473f, 0.94000244f, 0.18893433f, -0.05279541f, 0.01315308f, -0.00140381f, + -0.00717163f, 0.03466797f, -0.11895752f, 0.93490601f, 0.19839478f, -0.05532837f, + 0.01382446f, -0.00149536f, -0.00732422f, 0.03552246f, -0.12194824f, 0.92962646f, + 0.20791626f, -0.05786133f, 0.01449585f, -0.00158691f, -0.00744629f, 0.03631592f, + -0.12478638f, 0.92413330f, 0.21752930f, -0.06042480f, 0.01519775f, -0.00167847f, + -0.00753784f, 0.03707886f, -0.12750244f, 0.91848755f, 0.22723389f, -0.06298828f, + 0.01586914f, -0.00177002f, -0.00765991f, 0.03781128f, -0.13006592f, 0.91262817f, + 0.23703003f, -0.06555176f, 0.01657104f, -0.00189209f, -0.00775146f, 0.03848267f, + -0.13250732f, 0.90658569f, 0.24691772f, -0.06808472f, 0.01727295f, -0.00198364f, + -0.00784302f, 0.03909302f, -0.13479614f, 0.90036011f, 0.25683594f, -0.07064819f, + 0.01797485f, -0.00210571f, -0.00790405f, 0.03970337f, -0.13696289f, 0.89395142f, + 0.26687622f, -0.07321167f, 0.01870728f, -0.00219727f, -0.00796509f, 0.04025269f, + -0.13900757f, 0.88739014f, 0.27694702f, -0.07577515f, 0.01940918f, -0.00231934f, + -0.00802612f, 0.04077148f, -0.14089966f, 0.88064575f, 0.28710938f, -0.07833862f, + 0.02011108f, -0.00244141f, -0.00808716f, 0.04122925f, -0.14263916f, 0.87374878f, + 0.29733276f, -0.08090210f, 0.02084351f, -0.00253296f, -0.00811768f, 0.04165649f, + -0.14428711f, 0.86666870f, 0.30761719f, -0.08343506f, 0.02154541f, -0.00265503f, + -0.00814819f, 0.04205322f, -0.14578247f, 0.85940552f, 0.31793213f, -0.08596802f, + 0.02227783f, -0.00277710f, -0.00814819f, 0.04238892f, -0.14715576f, 0.85202026f, + 0.32833862f, -0.08847046f, 0.02297974f, -0.00289917f, -0.00817871f, 0.04272461f, + -0.14840698f, 0.84445190f, 0.33874512f, -0.09097290f, 0.02371216f, -0.00302124f, + -0.00817871f, 0.04299927f, -0.14953613f, 0.83673096f, 0.34924316f, -0.09347534f, + 0.02441406f, -0.00314331f, -0.00817871f, 0.04321289f, -0.15054321f, 0.82888794f, + 0.35977173f, -0.09594727f, 0.02514648f, -0.00326538f, -0.00814819f, 0.04342651f, + -0.15142822f, 0.82086182f, 0.37033081f, -0.09838867f, 0.02584839f, -0.00341797f, + -0.00814819f, 0.04357910f, -0.15219116f, 0.81271362f, 0.38092041f, -0.10079956f, + 0.02655029f, -0.00354004f, -0.00811768f, 0.04373169f, -0.15283203f, 0.80441284f, + 0.39154053f, -0.10321045f, 0.02725220f, -0.00366211f, -0.00808716f, 0.04382324f, + -0.15338135f, 0.79598999f, 0.40219116f, -0.10559082f, 0.02795410f, -0.00381470f, + -0.00805664f, 0.04388428f, -0.15377808f, 0.78741455f, 0.41287231f, -0.10794067f, + 0.02865601f, -0.00393677f, -0.00799561f, 0.04388428f, -0.15408325f, 0.77871704f, + 0.42358398f, -0.11026001f, 0.02935791f, -0.00405884f, -0.00793457f, 0.04388428f, + -0.15426636f, 0.76989746f, 0.43429565f, -0.11251831f, 0.03002930f, -0.00421143f, + -0.00787354f, 0.04385376f, -0.15435791f, 0.76095581f, 0.44500732f, -0.11477661f, + 0.03070068f, -0.00433350f, -0.00781250f, 0.04379272f, -0.15435791f, 0.75192261f, + 0.45574951f, -0.11697388f, 0.03137207f, -0.00448608f, -0.00775146f, 0.04367065f, + -0.15420532f, 0.74273682f, 0.46649170f, -0.11914062f, 0.03201294f, -0.00460815f, + -0.00769043f, 0.04354858f, -0.15399170f, 0.73345947f, 0.47723389f, -0.12127686f, + 0.03268433f, -0.00473022f, -0.00759888f, 0.04339600f, -0.15365601f, 0.72406006f, + 0.48794556f, -0.12335205f, 0.03329468f, -0.00488281f, -0.00750732f, 0.04321289f, + -0.15322876f, 0.71456909f, 0.49868774f, -0.12539673f, 0.03393555f, -0.00500488f, + -0.00741577f, 0.04296875f, -0.15270996f, 0.70498657f, 0.50936890f, -0.12738037f, + 0.03454590f, -0.00515747f, -0.00732422f, 0.04272461f, -0.15209961f, 0.69528198f, + 0.52008057f, -0.12930298f, 0.03515625f, -0.00527954f, -0.00723267f, 0.04248047f, + -0.15136719f, 0.68551636f, 0.53076172f, -0.13119507f, 0.03573608f, -0.00543213f, + -0.00714111f, 0.04217529f, -0.15057373f, 0.67565918f, 0.54138184f, -0.13299561f, + 0.03631592f, -0.00555420f, -0.00701904f, 0.04183960f, -0.14968872f, 0.66571045f, + 0.55200195f, -0.13476562f, 0.03689575f, -0.00567627f, -0.00692749f, 0.04150391f, + -0.14871216f, 0.65567017f, 0.56259155f, -0.13647461f, 0.03741455f, -0.00582886f, + -0.00680542f, 0.04113770f, -0.14767456f, 0.64556885f, 0.57315063f, -0.13812256f, + 0.03796387f, -0.00595093f, -0.00668335f, 0.04074097f, -0.14651489f, 0.63540649f, + 0.58364868f, -0.13970947f, 0.03845215f, -0.00607300f, -0.00656128f, 0.04031372f, + -0.14529419f, 0.62518311f, 0.59411621f, -0.14120483f, 0.03897095f, -0.00619507f, + -0.00643921f, 0.03988647f, -0.14401245f, 0.61486816f, 0.60452271f, -0.14263916f, + 0.03942871f, -0.00631714f, -0.00631714f, 0.03942871f, -0.14263916f, 0.60452271f, + 0.61486816f, -0.14401245f, 0.03988647f, -0.00643921f, -0.00619507f, 0.03897095f, + -0.14120483f, 0.59411621f, 0.62518311f, -0.14529419f, 0.04031372f, -0.00656128f, + -0.00607300f, 0.03845215f, -0.13970947f, 0.58364868f, 0.63540649f, -0.14651489f, + 0.04074097f, -0.00668335f, -0.00595093f, 0.03796387f, -0.13812256f, 0.57315063f, + 0.64556885f, -0.14767456f, 0.04113770f, -0.00680542f, -0.00582886f, 0.03741455f, + -0.13647461f, 0.56259155f, 0.65567017f, -0.14871216f, 0.04150391f, -0.00692749f, + -0.00567627f, 0.03689575f, -0.13476562f, 0.55200195f, 0.66571045f, -0.14968872f, + 0.04183960f, -0.00701904f, -0.00555420f, 0.03631592f, -0.13299561f, 0.54138184f, + 0.67565918f, -0.15057373f, 0.04217529f, -0.00714111f, -0.00543213f, 0.03573608f, + -0.13119507f, 0.53076172f, 0.68551636f, -0.15136719f, 0.04248047f, -0.00723267f, + -0.00527954f, 0.03515625f, -0.12930298f, 0.52008057f, 0.69528198f, -0.15209961f, + 0.04272461f, -0.00732422f, -0.00515747f, 0.03454590f, -0.12738037f, 0.50936890f, + 0.70498657f, -0.15270996f, 0.04296875f, -0.00741577f, -0.00500488f, 0.03393555f, + -0.12539673f, 0.49868774f, 0.71456909f, -0.15322876f, 0.04321289f, -0.00750732f, + -0.00488281f, 0.03329468f, -0.12335205f, 0.48794556f, 0.72406006f, -0.15365601f, + 0.04339600f, -0.00759888f, -0.00473022f, 0.03268433f, -0.12127686f, 0.47723389f, + 0.73345947f, -0.15399170f, 0.04354858f, -0.00769043f, -0.00460815f, 0.03201294f, + -0.11914062f, 0.46649170f, 0.74273682f, -0.15420532f, 0.04367065f, -0.00775146f, + -0.00448608f, 0.03137207f, -0.11697388f, 0.45574951f, 0.75192261f, -0.15435791f, + 0.04379272f, -0.00781250f, -0.00433350f, 0.03070068f, -0.11477661f, 0.44500732f, + 0.76095581f, -0.15435791f, 0.04385376f, -0.00787354f, -0.00421143f, 0.03002930f, + -0.11251831f, 0.43429565f, 0.76989746f, -0.15426636f, 0.04388428f, -0.00793457f, + -0.00405884f, 0.02935791f, -0.11026001f, 0.42358398f, 0.77871704f, -0.15408325f, + 0.04388428f, -0.00799561f, -0.00393677f, 0.02865601f, -0.10794067f, 0.41287231f, + 0.78741455f, -0.15377808f, 0.04388428f, -0.00805664f, -0.00381470f, 0.02795410f, + -0.10559082f, 0.40219116f, 0.79598999f, -0.15338135f, 0.04382324f, -0.00808716f, + -0.00366211f, 0.02725220f, -0.10321045f, 0.39154053f, 0.80441284f, -0.15283203f, + 0.04373169f, -0.00811768f, -0.00354004f, 0.02655029f, -0.10079956f, 0.38092041f, + 0.81271362f, -0.15219116f, 0.04357910f, -0.00814819f, -0.00341797f, 0.02584839f, + -0.09838867f, 0.37033081f, 0.82086182f, -0.15142822f, 0.04342651f, -0.00814819f, + -0.00326538f, 0.02514648f, -0.09594727f, 0.35977173f, 0.82888794f, -0.15054321f, + 0.04321289f, -0.00817871f, -0.00314331f, 0.02441406f, -0.09347534f, 0.34924316f, + 0.83673096f, -0.14953613f, 0.04299927f, -0.00817871f, -0.00302124f, 0.02371216f, + -0.09097290f, 0.33874512f, 0.84445190f, -0.14840698f, 0.04272461f, -0.00817871f, + -0.00289917f, 0.02297974f, -0.08847046f, 0.32833862f, 0.85202026f, -0.14715576f, + 0.04238892f, -0.00814819f, -0.00277710f, 0.02227783f, -0.08596802f, 0.31793213f, + 0.85940552f, -0.14578247f, 0.04205322f, -0.00814819f, -0.00265503f, 0.02154541f, + -0.08343506f, 0.30761719f, 0.86666870f, -0.14428711f, 0.04165649f, -0.00811768f, + -0.00253296f, 0.02084351f, -0.08090210f, 0.29733276f, 0.87374878f, -0.14263916f, + 0.04122925f, -0.00808716f, -0.00244141f, 0.02011108f, -0.07833862f, 0.28710938f, + 0.88064575f, -0.14089966f, 0.04077148f, -0.00802612f, -0.00231934f, 0.01940918f, + -0.07577515f, 0.27694702f, 0.88739014f, -0.13900757f, 0.04025269f, -0.00796509f, + -0.00219727f, 0.01870728f, -0.07321167f, 0.26687622f, 0.89395142f, -0.13696289f, + 0.03970337f, -0.00790405f, -0.00210571f, 0.01797485f, -0.07064819f, 0.25683594f, + 0.90036011f, -0.13479614f, 0.03909302f, -0.00784302f, -0.00198364f, 0.01727295f, + -0.06808472f, 0.24691772f, 0.90658569f, -0.13250732f, 0.03848267f, -0.00775146f, + -0.00189209f, 0.01657104f, -0.06555176f, 0.23703003f, 0.91262817f, -0.13006592f, + 0.03781128f, -0.00765991f, -0.00177002f, 0.01586914f, -0.06298828f, 0.22723389f, + 0.91848755f, -0.12750244f, 0.03707886f, -0.00753784f, -0.00167847f, 0.01519775f, + -0.06042480f, 0.21752930f, 0.92413330f, -0.12478638f, 0.03631592f, -0.00744629f, + -0.00158691f, 0.01449585f, -0.05786133f, 0.20791626f, 0.92962646f, -0.12194824f, + 0.03552246f, -0.00732422f, -0.00149536f, 0.01382446f, -0.05532837f, 0.19839478f, + 0.93490601f, -0.11895752f, 0.03466797f, -0.00717163f, -0.00140381f, 0.01315308f, + -0.05279541f, 0.18893433f, 0.94000244f, -0.11584473f, 0.03378296f, -0.00701904f, + -0.00131226f, 0.01248169f, -0.05029297f, 0.17959595f, 0.94491577f, -0.11254883f, + 0.03286743f, -0.00686646f, -0.00122070f, 0.01181030f, -0.04779053f, 0.17034912f, + 0.94961548f, -0.10916138f, 0.03189087f, -0.00671387f, -0.00112915f, 0.01113892f, + -0.04528809f, 0.16119385f, 0.95413208f, -0.10559082f, 0.03085327f, -0.00653076f, + -0.00106812f, 0.01049805f, -0.04281616f, 0.15213013f, 0.95843506f, -0.10189819f, + 0.02981567f, -0.00631714f, -0.00097656f, 0.00985718f, -0.04034424f, 0.14318848f, + 0.96252441f, -0.09805298f, 0.02868652f, -0.00613403f, -0.00091553f, 0.00924683f, + -0.03790283f, 0.13436890f, 0.96643066f, -0.09405518f, 0.02755737f, -0.00592041f, + -0.00082397f, 0.00860596f, -0.03549194f, 0.12561035f, 0.97012329f, -0.08993530f, + 0.02636719f, -0.00567627f, -0.00076294f, 0.00799561f, -0.03308105f, 0.11700439f, + 0.97360229f, -0.08566284f, 0.02511597f, -0.00543213f, -0.00070190f, 0.00738525f, + -0.03073120f, 0.10848999f, 0.97686768f, -0.08123779f, 0.02383423f, -0.00518799f, + -0.00061035f, 0.00680542f, -0.02835083f, 0.10012817f, 0.97991943f, -0.07666016f, + 0.02252197f, -0.00494385f, -0.00054932f, 0.00622559f, -0.02603149f, 0.09185791f, + 0.98278809f, -0.07192993f, 0.02114868f, -0.00463867f, -0.00048828f, 0.00564575f, + -0.02374268f, 0.08370972f, 0.98541260f, -0.06707764f, 0.01971436f, -0.00436401f, + -0.00042725f, 0.00506592f, -0.02145386f, 0.07568359f, 0.98782349f, -0.06207275f, + 0.01828003f, -0.00405884f, -0.00039673f, 0.00451660f, -0.01922607f, 0.06777954f, + 0.99002075f, -0.05691528f, 0.01678467f, -0.00375366f, -0.00033569f, 0.00396729f, + -0.01699829f, 0.05999756f, 0.99200439f, -0.05163574f, 0.01522827f, -0.00341797f, + -0.00027466f, 0.00344849f, -0.01483154f, 0.05233765f, 0.99377441f, -0.04620361f, + 0.01364136f, -0.00308228f, -0.00024414f, 0.00292969f, -0.01266479f, 0.04483032f, + 0.99533081f, -0.04061890f, 0.01202393f, -0.00274658f, -0.00018311f, 0.00241089f, + -0.01055908f, 0.03741455f, 0.99664307f, -0.03488159f, 0.01037598f, -0.00238037f, + -0.00015259f, 0.00192261f, -0.00845337f, 0.03018188f, 0.99774170f, -0.02899170f, + 0.00866699f, -0.00201416f, -0.00009155f, 0.00143433f, -0.00640869f, 0.02304077f, + 0.99862671f, -0.02297974f, 0.00689697f, -0.00161743f, -0.00006104f, 0.00097656f, + -0.00439453f, 0.01605225f, 0.99929810f, -0.01684570f, 0.00512695f, -0.00122070f, + -0.00003052f, 0.00051880f, -0.00241089f, 0.00918579f, 0.99975586f, -0.01052856f, + 0.00329590f, -0.00079346f, 0.00000000f, 0.00006104f, -0.00048828f, 0.00247192f, + 0.99996948f, -0.00408936f, 0.00143433f, -0.00036621f, + }; + + const auto get_lut = [&]() -> std::span { + if (sample_rate_ratio <= 1.0f) { + return std::span(lut2.data(), lut2.size()); + } else if (sample_rate_ratio < 1.3f) { + return std::span(lut1.data(), lut1.size()); + } else { + return std::span(lut0.data(), lut0.size()); + } + }; + + auto lut{get_lut()}; + u32 read_index{0}; + for (u32 i = 0; i < samples_to_write; i++) { + const auto lut_index{(fraction.get_frac() >> 8) * 8}; + const Common::FixedPoint<56, 8> sample0{input[read_index + 0] * lut[lut_index + 0]}; + const Common::FixedPoint<56, 8> sample1{input[read_index + 1] * lut[lut_index + 1]}; + const Common::FixedPoint<56, 8> sample2{input[read_index + 2] * lut[lut_index + 2]}; + const Common::FixedPoint<56, 8> sample3{input[read_index + 3] * lut[lut_index + 3]}; + const Common::FixedPoint<56, 8> sample4{input[read_index + 4] * lut[lut_index + 4]}; + const Common::FixedPoint<56, 8> sample5{input[read_index + 5] * lut[lut_index + 5]}; + const Common::FixedPoint<56, 8> sample6{input[read_index + 6] * lut[lut_index + 6]}; + const Common::FixedPoint<56, 8> sample7{input[read_index + 7] * lut[lut_index + 7]}; + output[i] = (sample0 + sample1 + sample2 + sample3 + sample4 + sample5 + sample6 + sample7) + .to_int_floor(); + fraction += sample_rate_ratio; + read_index += static_cast(fraction.to_int_floor()); + fraction.clear_int(); + } +} + +void Resample(std::span output, std::span input, + const Common::FixedPoint<49, 15>& sample_rate_ratio, + Common::FixedPoint<49, 15>& fraction, const u32 samples_to_write, + const SrcQuality src_quality) { + + switch (src_quality) { + case SrcQuality::Low: + ResampleLowQuality(output, input, sample_rate_ratio, fraction, samples_to_write); + break; + case SrcQuality::Medium: + ResampleNormalQuality(output, input, sample_rate_ratio, fraction, samples_to_write); + break; + case SrcQuality::High: + ResampleHighQuality(output, input, sample_rate_ratio, fraction, samples_to_write); + break; + } +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/resample/resample.h b/src/audio_core/renderer/command/resample/resample.h new file mode 100755 index 000000000..ba9209b82 --- /dev/null +++ b/src/audio_core/renderer/command/resample/resample.h @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "common/common_types.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { +/** + * Resample an input buffer into an output buffer, according to the sample_rate_ratio. + * + * @param output - Output buffer. + * @param input - Input buffer. + * @param sample_rate_ratio - Ratio for resampling. + e.g 32000/48000 = 0.666 input samples read per output. + * @param fraction - Current read fraction, written to and should be passed back in for + * multiple calls. + * @param samples_to_write - Number of samples to write. + * @param src_quality - Resampling quality. + */ +void Resample(std::span output, std::span input, + const Common::FixedPoint<49, 15>& sample_rate_ratio, + Common::FixedPoint<49, 15>& fraction, u32 samples_to_write, SrcQuality src_quality); + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/resample/upsample.cpp b/src/audio_core/renderer/command/resample/upsample.cpp new file mode 100755 index 000000000..6c3ff31f7 --- /dev/null +++ b/src/audio_core/renderer/command/resample/upsample.cpp @@ -0,0 +1,262 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/resample/upsample.h" +#include "audio_core/renderer/upsampler/upsampler_info.h" + +namespace AudioCore::AudioRenderer { +/** + * Upsampling impl. Input must be 8K, 16K or 32K, output is 48K. + * + * @param output - Output buffer. + * @param input - Input buffer. + * @param target_sample_count - Number of samples for output. + * @param state - Upsampler state, updated each call. + */ +static void SrcProcessFrame(std::span output, std::span input, + const u32 target_sample_count, const u32 source_sample_count, + UpsamplerState* state) { + constexpr u32 WindowSize = 10; + constexpr std::array, WindowSize> SincWindow1{ + 51.93359375f, -18.80078125f, 9.73046875f, -5.33203125f, 2.84375f, + -1.41015625f, 0.62109375f, -0.2265625f, 0.0625f, -0.00390625f, + }; + constexpr std::array, WindowSize> SincWindow2{ + 105.35546875f, -24.52734375f, 11.9609375f, -6.515625f, 3.52734375f, + -1.796875f, 0.828125f, -0.32421875f, 0.1015625f, -0.015625f, + }; + constexpr std::array, WindowSize> SincWindow3{ + 122.08203125f, -16.47656250f, 7.68359375f, -4.15625000f, 2.26171875f, + -1.16796875f, 0.54687500f, -0.22265625f, 0.07421875f, -0.01171875f, + }; + constexpr std::array, WindowSize> SincWindow4{ + 23.73437500f, -9.62109375f, 5.07812500f, -2.78125000f, 1.46875000f, + -0.71484375f, 0.30859375f, -0.10546875f, 0.02734375f, 0.00000000f, + }; + constexpr std::array, WindowSize> SincWindow5{ + 80.62500000f, -24.67187500f, 12.44921875f, -6.80859375f, 3.66406250f, + -1.83984375f, 0.83203125f, -0.31640625f, 0.09375000f, -0.01171875f, + }; + + if (!state->initialized) { + switch (source_sample_count) { + case 40: + state->window_size = WindowSize; + state->ratio = 6.0f; + state->history.fill(0); + break; + + case 80: + state->window_size = WindowSize; + state->ratio = 3.0f; + state->history.fill(0); + break; + + case 160: + state->window_size = WindowSize; + state->ratio = 1.5f; + state->history.fill(0); + break; + + default: + LOG_ERROR(Service_Audio, "Invalid upsampling source count {}!", source_sample_count); + // This continues anyway, but let's assume 160 for sanity + state->window_size = WindowSize; + state->ratio = 1.5f; + state->history.fill(0); + break; + } + + state->history_input_index = 0; + state->history_output_index = 9; + state->history_start_index = 0; + state->history_end_index = UpsamplerState::HistorySize - 1; + state->initialized = true; + } + + if (target_sample_count == 0) { + return; + } + + u32 read_index{0}; + + auto increment = [&]() -> void { + state->history[state->history_input_index] = input[read_index++]; + state->history_input_index = + static_cast((state->history_input_index + 1) % UpsamplerState::HistorySize); + state->history_output_index = + static_cast((state->history_output_index + 1) % UpsamplerState::HistorySize); + }; + + auto calculate_sample = [&state](std::span> coeffs1, + std::span> coeffs2) -> s32 { + auto output_index{state->history_output_index}; + auto start_pos{output_index - state->history_start_index + 1U}; + auto end_pos{10U}; + + if (start_pos < 10) { + end_pos = start_pos; + } + + u64 prev_contrib{0}; + u32 coeff_index{0}; + for (; coeff_index < end_pos; coeff_index++, output_index--) { + prev_contrib += static_cast(state->history[output_index].to_raw()) * + coeffs1[coeff_index].to_raw(); + } + + auto end_index{state->history_end_index}; + for (; start_pos < 9; start_pos++, coeff_index++, end_index--) { + prev_contrib += static_cast(state->history[end_index].to_raw()) * + coeffs1[coeff_index].to_raw(); + } + + output_index = + static_cast((state->history_output_index + 1) % UpsamplerState::HistorySize); + start_pos = state->history_end_index - output_index + 1U; + end_pos = 10U; + + if (start_pos < 10) { + end_pos = start_pos; + } + + u64 next_contrib{0}; + coeff_index = 0; + for (; coeff_index < end_pos; coeff_index++, output_index++) { + next_contrib += static_cast(state->history[output_index].to_raw()) * + coeffs2[coeff_index].to_raw(); + } + + auto start_index{state->history_start_index}; + for (; start_pos < 9; start_pos++, start_index++, coeff_index++) { + next_contrib += static_cast(state->history[start_index].to_raw()) * + coeffs2[coeff_index].to_raw(); + } + + return static_cast(((prev_contrib >> 15) + (next_contrib >> 15)) >> 8); + }; + + switch (state->ratio.to_int_floor()) { + // 40 -> 240 + case 6: + for (u32 write_index = 0; write_index < target_sample_count; write_index++) { + switch (state->sample_index) { + case 0: + increment(); + output[write_index] = state->history[state->history_output_index].to_int_floor(); + break; + + case 1: + output[write_index] = calculate_sample(SincWindow3, SincWindow4); + break; + + case 2: + output[write_index] = calculate_sample(SincWindow2, SincWindow1); + break; + + case 3: + output[write_index] = calculate_sample(SincWindow5, SincWindow5); + break; + + case 4: + output[write_index] = calculate_sample(SincWindow1, SincWindow2); + break; + + case 5: + output[write_index] = calculate_sample(SincWindow4, SincWindow3); + break; + } + state->sample_index = static_cast((state->sample_index + 1) % 6); + } + break; + + // 80 -> 240 + case 3: + for (u32 write_index = 0; write_index < target_sample_count; write_index++) { + switch (state->sample_index) { + case 0: + increment(); + output[write_index] = state->history[state->history_output_index].to_int_floor(); + break; + + case 1: + output[write_index] = calculate_sample(SincWindow2, SincWindow1); + break; + + case 2: + output[write_index] = calculate_sample(SincWindow1, SincWindow2); + break; + } + state->sample_index = static_cast((state->sample_index + 1) % 3); + } + break; + + // 160 -> 240 + default: + for (u32 write_index = 0; write_index < target_sample_count; write_index++) { + switch (state->sample_index) { + case 0: + increment(); + output[write_index] = state->history[state->history_output_index].to_int_floor(); + break; + + case 1: + output[write_index] = calculate_sample(SincWindow1, SincWindow2); + break; + + case 2: + increment(); + output[write_index] = calculate_sample(SincWindow2, SincWindow1); + break; + } + state->sample_index = static_cast((state->sample_index + 1) % 3); + } + + break; + } +} + +auto UpsampleCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) -> void { + string += fmt::format("UpsampleCommand\n\tsource_sample_count {} source_sample_rate {}", + source_sample_count, source_sample_rate); + const auto upsampler{reinterpret_cast(upsampler_info)}; + if (upsampler != nullptr) { + string += fmt::format("\n\tUpsampler\n\t\tenabled {} sample count {}\n\tinputs: ", + upsampler->enabled, upsampler->sample_count); + for (u32 i = 0; i < upsampler->input_count; i++) { + string += fmt::format("{:02X}, ", upsampler->inputs[i]); + } + } + string += "\n"; +} + +void UpsampleCommand::Process(const ADSP::CommandListProcessor& processor) { + const auto info{reinterpret_cast(upsampler_info)}; + const auto input_count{std::min(info->input_count, buffer_count)}; + const std::span inputs_{reinterpret_cast(inputs), input_count}; + + for (u32 i = 0; i < input_count; i++) { + const auto channel{inputs_[i]}; + + if (channel >= 0 && channel < static_cast(processor.buffer_count)) { + auto state{&info->states[i]}; + std::span output{ + reinterpret_cast(samples_buffer + info->sample_count * channel * sizeof(s32)), + info->sample_count}; + auto input{processor.mix_buffers.subspan(channel * processor.sample_count, + processor.sample_count)}; + + SrcProcessFrame(output, input, info->sample_count, source_sample_count, state); + } + } +} + +bool UpsampleCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/resample/upsample.h b/src/audio_core/renderer/command/resample/upsample.h new file mode 100755 index 000000000..bfc94e8af --- /dev/null +++ b/src/audio_core/renderer/command/resample/upsample.h @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for upsampling a mix buffer to 48Khz. + * Input must be 8Khz, 16Khz or 32Khz, and output will be 48Khz. + */ +struct UpsampleCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Pointer to the output samples buffer. + CpuAddr samples_buffer; + /// Pointer to input mix buffer indexes. + CpuAddr inputs; + /// Number of input mix buffers. + u32 buffer_count; + /// Unknown, unused. + u32 unk_20; + /// Source data sample count. + u32 source_sample_count; + /// Source data sample rate. + u32 source_sample_rate; + /// Pointer to the upsampler info for this command. + CpuAddr upsampler_info; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/sink/circular_buffer.cpp b/src/audio_core/renderer/command/sink/circular_buffer.cpp new file mode 100755 index 000000000..ded5afc94 --- /dev/null +++ b/src/audio_core/renderer/command/sink/circular_buffer.cpp @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/sink/circular_buffer.h" +#include "core/memory.h" + +namespace AudioCore::AudioRenderer { + +void CircularBufferSinkCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format( + "CircularBufferSinkCommand\n\tinput_count {} ring size {:04X} ring pos {:04X}\n\tinputs: ", + input_count, size, pos); + for (u32 i = 0; i < input_count; i++) { + string += fmt::format("{:02X}, ", inputs[i]); + } + string += "\n"; +} + +void CircularBufferSinkCommand::Process(const ADSP::CommandListProcessor& processor) { + constexpr s32 min{std::numeric_limits::min()}; + constexpr s32 max{std::numeric_limits::max()}; + + std::vector output(processor.sample_count); + for (u32 channel = 0; channel < input_count; channel++) { + auto input{processor.mix_buffers.subspan(inputs[channel] * processor.sample_count, + processor.sample_count)}; + for (u32 sample_index = 0; sample_index < processor.sample_count; sample_index++) { + output[sample_index] = static_cast(std::clamp(input[sample_index], min, max)); + } + + processor.memory->WriteBlockUnsafe(address + pos, output.data(), + output.size() * sizeof(s16)); + pos += static_cast(processor.sample_count * sizeof(s16)); + if (pos >= size) { + pos = 0; + } + } +} + +bool CircularBufferSinkCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/sink/circular_buffer.h b/src/audio_core/renderer/command/sink/circular_buffer.h new file mode 100755 index 000000000..e7d5be26e --- /dev/null +++ b/src/audio_core/renderer/command/sink/circular_buffer.h @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for sinking samples to a circular buffer. + */ +struct CircularBufferSinkCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Number of input mix buffers + u32 input_count; + /// Input mix buffer indexes + std::array inputs; + /// Circular buffer address + CpuAddr address; + /// Circular buffer size + u32 size; + /// Current buffer offset + u32 pos; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/sink/device.cpp b/src/audio_core/renderer/command/sink/device.cpp new file mode 100755 index 000000000..056c38cfa --- /dev/null +++ b/src/audio_core/renderer/command/sink/device.cpp @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/sink/device.h" +#include "audio_core/sink/sink.h" + +namespace AudioCore::AudioRenderer { + +void DeviceSinkCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("DeviceSinkCommand\n\t{} session {} input_count {}\n\tinputs: ", + std::string_view(name), session_id, input_count); + for (u32 i = 0; i < input_count; i++) { + string += fmt::format("{:02X}, ", inputs[i]); + } + string += "\n"; +} + +void DeviceSinkCommand::Process(const ADSP::CommandListProcessor& processor) { + constexpr s32 min = std::numeric_limits::min(); + constexpr s32 max = std::numeric_limits::max(); + + auto stream{processor.GetOutputSinkStream()}; + const auto num_channels{stream->GetDeviceChannels()}; + + Sink::SinkBuffer out_buffer{ + .frames{TargetSampleCount}, + .frames_played{0}, + .tag{0}, + .consumed{false}, + }; + + std::vector samples(out_buffer.frames * num_channels); + + for (u32 channel = 0; channel < num_channels; channel++) { + const auto offset{inputs[channel] * out_buffer.frames}; + + for (u32 index = 0; index < out_buffer.frames; index++) { + samples[index * num_channels + channel] = + static_cast(std::clamp(sample_buffer[offset + index], min, max)); + } + } + + out_buffer.tag = reinterpret_cast(samples.data()); + stream->AppendBuffer(out_buffer, samples); +} + +bool DeviceSinkCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/sink/device.h b/src/audio_core/renderer/command/sink/device.h new file mode 100755 index 000000000..1099bcf8c --- /dev/null +++ b/src/audio_core/renderer/command/sink/device.h @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for sinking samples to an output device. + */ +struct DeviceSinkCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Device name + char name[0x100]; + /// System session id (unused) + s32 session_id; + /// Sample buffer to sink + std::span sample_buffer; + /// Number of input channels + u32 input_count; + /// Mix buffer indexes for each channel + std::array inputs; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_aux_info.cpp b/src/audio_core/renderer/effect/effect_aux_info.cpp new file mode 100755 index 000000000..b77e5039e --- /dev/null +++ b/src/audio_core/renderer/effect/effect_aux_info.cpp @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/effect/effect_aux_info.h" + +namespace AudioCore::AudioRenderer { + +void AuxInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + if (buffer_unmapped || in_params.is_new) { + const bool send_unmapped{!pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], in_specific->send_buffer_info_address, + sizeof(AuxBufferInfo) + in_specific->count_max * sizeof(s32))}; + const bool return_unmapped{!pool_mapper.TryAttachBuffer( + error_info, workbuffers[1], in_specific->return_buffer_info_address, + sizeof(AuxBufferInfo) + in_specific->count_max * sizeof(s32))}; + + buffer_unmapped = send_unmapped || return_unmapped; + + if (!buffer_unmapped) { + auto send{workbuffers[0].GetReference(false)}; + send_buffer_info = send + sizeof(AuxInfoDsp); + send_buffer = send + sizeof(AuxBufferInfo); + + auto ret{workbuffers[1].GetReference(false)}; + return_buffer_info = ret + sizeof(AuxInfoDsp); + return_buffer = ret + sizeof(AuxBufferInfo); + } + } else { + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + } +} + +void AuxInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + std::memcpy(params, in_specific, sizeof(ParameterVersion2)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + if (buffer_unmapped || in_params.is_new) { + const bool send_unmapped{!pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], params->send_buffer_info_address, + sizeof(AuxBufferInfo) + params->count_max * sizeof(s32))}; + const bool return_unmapped{!pool_mapper.TryAttachBuffer( + error_info, workbuffers[1], params->return_buffer_info_address, + sizeof(AuxBufferInfo) + params->count_max * sizeof(s32))}; + + buffer_unmapped = send_unmapped || return_unmapped; + + if (!buffer_unmapped) { + auto send{workbuffers[0].GetReference(false)}; + send_buffer_info = send + sizeof(AuxInfoDsp); + send_buffer = send + sizeof(AuxBufferInfo); + + auto ret{workbuffers[1].GetReference(false)}; + return_buffer_info = ret + sizeof(AuxInfoDsp); + return_buffer = ret + sizeof(AuxBufferInfo); + } + } else { + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + } +} + +void AuxInfo::UpdateForCommandGeneration() { + if (enabled) { + usage_state = UsageState::Enabled; + } else { + usage_state = UsageState::Disabled; + } +} + +void AuxInfo::InitializeResultState(EffectResultState& result_state) {} + +void AuxInfo::UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) {} + +CpuAddr AuxInfo::GetWorkbuffer(s32 index) { + return workbuffers[index].GetReference(true); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_aux_info.h b/src/audio_core/renderer/effect/effect_aux_info.h new file mode 100755 index 000000000..4d3d9e3d9 --- /dev/null +++ b/src/audio_core/renderer/effect/effect_aux_info.h @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/effect/effect_info_base.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Auxiliary Buffer used for Aux commands. + * Send and return buffers are available (names from the game's perspective). + * Send is read by the host, containing a buffer of samples to be used for whatever purpose. + * Return is written by the host, writing a mix buffer back to the game. + * This allows the game to use pre-processed samples skipping the other render processing, + * and to examine or modify what the audio renderer has generated. + */ +class AuxInfo : public EffectInfoBase { +public: + struct ParameterVersion1 { + /* 0x00 */ std::array inputs; + /* 0x18 */ std::array outputs; + /* 0x30 */ u32 mix_buffer_count; + /* 0x34 */ u32 sample_rate; + /* 0x38 */ u32 count_max; + /* 0x3C */ u32 mix_buffer_count_max; + /* 0x40 */ CpuAddr send_buffer_info_address; + /* 0x48 */ CpuAddr send_buffer_address; + /* 0x50 */ CpuAddr return_buffer_info_address; + /* 0x58 */ CpuAddr return_buffer_address; + /* 0x60 */ u32 mix_buffer_sample_size; + /* 0x64 */ u32 sample_count; + /* 0x68 */ u32 mix_buffer_sample_count; + }; + static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1), + "AuxInfo::ParameterVersion1 has the wrong size!"); + + struct ParameterVersion2 { + /* 0x00 */ std::array inputs; + /* 0x18 */ std::array outputs; + /* 0x30 */ u32 mix_buffer_count; + /* 0x34 */ u32 sample_rate; + /* 0x38 */ u32 count_max; + /* 0x3C */ u32 mix_buffer_count_max; + /* 0x40 */ CpuAddr send_buffer_info_address; + /* 0x48 */ CpuAddr send_buffer_address; + /* 0x50 */ CpuAddr return_buffer_info_address; + /* 0x58 */ CpuAddr return_buffer_address; + /* 0x60 */ u32 mix_buffer_sample_size; + /* 0x64 */ u32 sample_count; + /* 0x68 */ u32 mix_buffer_sample_count; + }; + static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2), + "AuxInfo::ParameterVersion2 has the wrong size!"); + + struct AuxInfoDsp { + /* 0x00 */ u32 read_offset; + /* 0x04 */ u32 write_offset; + /* 0x08 */ u32 lost_sample_count; + /* 0x0C */ u32 total_sample_count; + /* 0x10 */ char unk10[0x30]; + }; + static_assert(sizeof(AuxInfoDsp) == 0x40, "AuxInfo::AuxInfoDsp has the wrong size!"); + + struct AuxBufferInfo { + /* 0x00 */ AuxInfoDsp cpu_info; + /* 0x40 */ AuxInfoDsp dsp_info; + }; + static_assert(sizeof(AuxBufferInfo) == 0x80, "AuxInfo::AuxBufferInfo has the wrong size!"); + + /** + * Update the info with new parameters, version 1. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info with new parameters, version 2. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info after command generation. Usually only changes its state. + */ + void UpdateForCommandGeneration() override; + + /** + * Initialize a new result state. Version 2 only, unused. + * + * @param result_state - Result state to initialize. + */ + void InitializeResultState(EffectResultState& result_state) override; + + /** + * Update the host-side state with the ADSP-side state. Version 2 only, unused. + * + * @param cpu_state - Host-side result state to update. + * @param dsp_state - AudioRenderer-side result state to update from. + */ + void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override; + + /** + * Get a workbuffer assigned to this effect with the given index. + * + * @param index - Workbuffer index. + * @return Address of the buffer. + */ + CpuAddr GetWorkbuffer(s32 index) override; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_biquad_filter_info.cpp b/src/audio_core/renderer/effect/effect_biquad_filter_info.cpp new file mode 100755 index 000000000..9ee3247a8 --- /dev/null +++ b/src/audio_core/renderer/effect/effect_biquad_filter_info.cpp @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/effect/effect_biquad_filter_info.h" + +namespace AudioCore::AudioRenderer { + +void BiquadFilterInfo::Update(BehaviorInfo::ErrorInfo& error_info, + const InParameterVersion1& in_params, const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void BiquadFilterInfo::Update(BehaviorInfo::ErrorInfo& error_info, + const InParameterVersion2& in_params, const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + std::memcpy(params, in_specific, sizeof(ParameterVersion2)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void BiquadFilterInfo::UpdateForCommandGeneration() { + if (enabled) { + usage_state = UsageState::Enabled; + } else { + usage_state = UsageState::Disabled; + } + + auto params{reinterpret_cast(parameter.data())}; + params->state = ParameterState::Updated; +} + +void BiquadFilterInfo::InitializeResultState(EffectResultState& result_state) {} + +void BiquadFilterInfo::UpdateResultState(EffectResultState& cpu_state, + EffectResultState& dsp_state) {} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_biquad_filter_info.h b/src/audio_core/renderer/effect/effect_biquad_filter_info.h new file mode 100755 index 000000000..f53fd5bab --- /dev/null +++ b/src/audio_core/renderer/effect/effect_biquad_filter_info.h @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/effect/effect_info_base.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { + +class BiquadFilterInfo : public EffectInfoBase { +public: + struct ParameterVersion1 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ std::array b; + /* 0x12 */ std::array a; + /* 0x16 */ s8 channel_count; + /* 0x17 */ ParameterState state; + }; + static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1), + "BiquadFilterInfo::ParameterVersion1 has the wrong size!"); + + struct ParameterVersion2 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ std::array b; + /* 0x12 */ std::array a; + /* 0x16 */ s8 channel_count; + /* 0x17 */ ParameterState state; + }; + static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2), + "BiquadFilterInfo::ParameterVersion2 has the wrong size!"); + + /** + * Update the info with new parameters, version 1. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info with new parameters, version 2. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info after command generation. Usually only changes its state. + */ + void UpdateForCommandGeneration() override; + + /** + * Initialize a new result state. Version 2 only, unused. + * + * @param result_state - Result state to initialize. + */ + void InitializeResultState(EffectResultState& result_state) override; + + /** + * Update the host-side state with the ADSP-side state. Version 2 only, unused. + * + * @param cpu_state - Host-side result state to update. + * @param dsp_state - AudioRenderer-side result state to update from. + */ + void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_buffer_mixer_info.cpp b/src/audio_core/renderer/effect/effect_buffer_mixer_info.cpp new file mode 100755 index 000000000..bebbc760f --- /dev/null +++ b/src/audio_core/renderer/effect/effect_buffer_mixer_info.cpp @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/effect/effect_buffer_mixer_info.h" + +namespace AudioCore::AudioRenderer { + +void BufferMixerInfo::Update(BehaviorInfo::ErrorInfo& error_info, + const InParameterVersion1& in_params, const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void BufferMixerInfo::Update(BehaviorInfo::ErrorInfo& error_info, + const InParameterVersion2& in_params, const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + std::memcpy(params, in_specific, sizeof(ParameterVersion2)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void BufferMixerInfo::UpdateForCommandGeneration() { + if (enabled) { + usage_state = UsageState::Enabled; + } else { + usage_state = UsageState::Disabled; + } +} + +void BufferMixerInfo::InitializeResultState(EffectResultState& result_state) {} + +void BufferMixerInfo::UpdateResultState(EffectResultState& cpu_state, + EffectResultState& dsp_state) {} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_buffer_mixer_info.h b/src/audio_core/renderer/effect/effect_buffer_mixer_info.h new file mode 100755 index 000000000..23eed4a8b --- /dev/null +++ b/src/audio_core/renderer/effect/effect_buffer_mixer_info.h @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/effect/effect_info_base.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { + +class BufferMixerInfo : public EffectInfoBase { +public: + struct ParameterVersion1 { + /* 0x00 */ std::array inputs; + /* 0x18 */ std::array outputs; + /* 0x30 */ std::array volumes; + /* 0x90 */ u32 mix_count; + }; + static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1), + "BufferMixerInfo::ParameterVersion1 has the wrong size!"); + + struct ParameterVersion2 { + /* 0x00 */ std::array inputs; + /* 0x18 */ std::array outputs; + /* 0x30 */ std::array volumes; + /* 0x90 */ u32 mix_count; + }; + static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2), + "BufferMixerInfo::ParameterVersion2 has the wrong size!"); + + /** + * Update the info with new parameters, version 1. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info with new parameters, version 2. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info after command generation. Usually only changes its state. + */ + void UpdateForCommandGeneration() override; + + /** + * Initialize a new result state. Version 2 only, unused. + * + * @param result_state - Result state to initialize. + */ + void InitializeResultState(EffectResultState& result_state) override; + + /** + * Update the host-side state with the ADSP-side state. Version 2 only, unused. + * + * @param cpu_state - Host-side result state to update. + * @param dsp_state - AudioRenderer-side result state to update from. + */ + void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_capture_info.cpp b/src/audio_core/renderer/effect/effect_capture_info.cpp new file mode 100755 index 000000000..70f43b792 --- /dev/null +++ b/src/audio_core/renderer/effect/effect_capture_info.cpp @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/effect/effect_aux_info.h" +#include "audio_core/renderer/effect/effect_capture_info.h" + +namespace AudioCore::AudioRenderer { + +void CaptureInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) { + auto in_specific{ + reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + std::memcpy(params, in_specific, sizeof(AuxInfo::ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + if (buffer_unmapped || in_params.is_new) { + buffer_unmapped = !pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], in_specific->send_buffer_info_address, + in_specific->count_max * sizeof(s32) + sizeof(AuxInfo::AuxBufferInfo)); + + if (!buffer_unmapped) { + const auto send_address{workbuffers[0].GetReference(false)}; + send_buffer_info = send_address + sizeof(AuxInfo::AuxInfoDsp); + send_buffer = send_address + sizeof(AuxInfo::AuxBufferInfo); + return_buffer_info = 0; + return_buffer = 0; + } + } else { + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + } +} + +void CaptureInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) { + auto in_specific{ + reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + std::memcpy(params, in_specific, sizeof(AuxInfo::ParameterVersion2)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + if (buffer_unmapped || in_params.is_new) { + buffer_unmapped = !pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], params->send_buffer_info_address, + params->count_max * sizeof(s32) + sizeof(AuxInfo::AuxBufferInfo)); + + if (!buffer_unmapped) { + const auto send_address{workbuffers[0].GetReference(false)}; + send_buffer_info = send_address + sizeof(AuxInfo::AuxInfoDsp); + send_buffer = send_address + sizeof(AuxInfo::AuxBufferInfo); + return_buffer_info = 0; + return_buffer = 0; + } + } else { + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + } +} + +void CaptureInfo::UpdateForCommandGeneration() { + if (enabled) { + usage_state = UsageState::Enabled; + } else { + usage_state = UsageState::Disabled; + } +} + +void CaptureInfo::InitializeResultState(EffectResultState& result_state) {} + +void CaptureInfo::UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) {} + +CpuAddr CaptureInfo::GetWorkbuffer(s32 index) { + return workbuffers[index].GetReference(true); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_capture_info.h b/src/audio_core/renderer/effect/effect_capture_info.h new file mode 100755 index 000000000..6fbed8e6b --- /dev/null +++ b/src/audio_core/renderer/effect/effect_capture_info.h @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/effect/effect_info_base.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { + +class CaptureInfo : public EffectInfoBase { +public: + /** + * Update the info with new parameters, version 1. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info with new parameters, version 2. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info after command generation. Usually only changes its state. + */ + void UpdateForCommandGeneration() override; + + /** + * Initialize a new result state. Version 2 only, unused. + * + * @param result_state - Result state to initialize. + */ + void InitializeResultState(EffectResultState& result_state) override; + + /** + * Update the host-side state with the ADSP-side state. Version 2 only, unused. + * + * @param cpu_state - Host-side result state to update. + * @param dsp_state - AudioRenderer-side result state to update from. + */ + void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override; + + /** + * Get a workbuffer assigned to this effect with the given index. + * + * @param index - Workbuffer index. + * @return Address of the buffer. + */ + CpuAddr GetWorkbuffer(s32 index) override; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_context.cpp b/src/audio_core/renderer/effect/effect_context.cpp new file mode 100755 index 000000000..74c7801c9 --- /dev/null +++ b/src/audio_core/renderer/effect/effect_context.cpp @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/effect/effect_context.h" + +namespace AudioCore::AudioRenderer { + +void EffectContext::Initialize(std::span effect_infos_, const u32 effect_count_, + std::span result_states_cpu_, + std::span result_states_dsp_, + const size_t dsp_state_count_) { + effect_infos = effect_infos_; + effect_count = effect_count_; + result_states_cpu = result_states_cpu_; + result_states_dsp = result_states_dsp_; + dsp_state_count = dsp_state_count_; +} + +EffectInfoBase& EffectContext::GetInfo(const u32 index) { + return effect_infos[index]; +} + +EffectResultState& EffectContext::GetResultState(const u32 index) { + return result_states_cpu[index]; +} + +EffectResultState& EffectContext::GetDspSharedResultState(const u32 index) { + return result_states_dsp[index]; +} + +u32 EffectContext::GetCount() const { + return effect_count; +} + +void EffectContext::UpdateStateByDspShared() { + for (size_t i = 0; i < dsp_state_count; i++) { + effect_infos[i].UpdateResultState(result_states_cpu[i], result_states_dsp[i]); + } +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_context.h b/src/audio_core/renderer/effect/effect_context.h new file mode 100755 index 000000000..85955bd9c --- /dev/null +++ b/src/audio_core/renderer/effect/effect_context.h @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/effect/effect_info_base.h" +#include "audio_core/renderer/effect/effect_result_state.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { + +class EffectContext { +public: + /** + * Initialize the effect context + * @param effect_infos List of effect infos for this context + * @param effect_count The number of effects in the list + * @param result_states_cpu The workbuffer of result states for the CPU for this context + * @param result_states_dsp The workbuffer of result states for the DSP for this context + * @param state_count The number of result states + */ + void Initialize(std::span effect_infos_, const u32 effect_count_, + std::span result_states_cpu_, + std::span result_states_dsp_, const size_t dsp_state_count); + + /** + * Get the EffectInfo for a given index + * @param index Which effect to return + * @return Pointer to the effect + */ + EffectInfoBase& GetInfo(const u32 index); + + /** + * Get the CPU result state for a given index + * @param index Which result to return + * @return Pointer to the effect result state + */ + EffectResultState& GetResultState(const u32 index); + + /** + * Get the DSP result state for a given index + * @param index Which result to return + * @return Pointer to the effect result state + */ + EffectResultState& GetDspSharedResultState(const u32 index); + + /** + * Get the number of effects in this context + * @return The number of effects + */ + u32 GetCount() const; + + /** + * Update the CPU and DSP result states for all effects + */ + void UpdateStateByDspShared(); + +private: + /// Workbuffer for all of the effects + std::span effect_infos{}; + /// Number of effects in the workbuffer + u32 effect_count{}; + /// Workbuffer of states for all effects, kept host-side and not directly modified, dsp states + /// are copied here on the next render frame + std::span result_states_cpu{}; + /// Workbuffer of states for all effects, used by the AudioRenderer to track effect state + /// between calls + std::span result_states_dsp{}; + /// Number of result states in the workbuffers + size_t dsp_state_count{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_delay_info.cpp b/src/audio_core/renderer/effect/effect_delay_info.cpp new file mode 100755 index 000000000..04e7c51b6 --- /dev/null +++ b/src/audio_core/renderer/effect/effect_delay_info.cpp @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/effect/effect_delay_info.h" + +namespace AudioCore::AudioRenderer { + +void DelayInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + if (IsChannelCountValid(in_specific->channel_count_max)) { + const auto old_state{params->state}; + std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + if (!IsChannelCountValid(in_specific->channel_count)) { + params->channel_count = params->channel_count_max; + } + + if (!IsChannelCountValid(in_specific->channel_count) || + old_state != ParameterState::Updated) { + params->state = old_state; + } + + if (buffer_unmapped || in_params.is_new) { + usage_state = UsageState::New; + params->state = ParameterState::Initialized; + buffer_unmapped = !pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size); + return; + } + } + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void DelayInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + if (IsChannelCountValid(in_specific->channel_count_max)) { + const auto old_state{params->state}; + std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + if (!IsChannelCountValid(in_specific->channel_count)) { + params->channel_count = params->channel_count_max; + } + + if (!IsChannelCountValid(in_specific->channel_count) || + old_state != ParameterState::Updated) { + params->state = old_state; + } + + if (buffer_unmapped || in_params.is_new) { + usage_state = UsageState::New; + params->state = ParameterState::Initialized; + buffer_unmapped = !pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size); + return; + } + } + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void DelayInfo::UpdateForCommandGeneration() { + if (enabled) { + usage_state = UsageState::Enabled; + } else { + usage_state = UsageState::Disabled; + } + + auto params{reinterpret_cast(parameter.data())}; + params->state = ParameterState::Updated; +} + +void DelayInfo::InitializeResultState(EffectResultState& result_state) {} + +void DelayInfo::UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) {} + +CpuAddr DelayInfo::GetWorkbuffer(s32 index) { + return GetSingleBuffer(index); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_delay_info.h b/src/audio_core/renderer/effect/effect_delay_info.h new file mode 100755 index 000000000..accc42a06 --- /dev/null +++ b/src/audio_core/renderer/effect/effect_delay_info.h @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/effect/effect_info_base.h" +#include "common/common_types.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { + +class DelayInfo : public EffectInfoBase { +public: + struct ParameterVersion1 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ u16 channel_count_max; + /* 0x0E */ u16 channel_count; + /* 0x10 */ u32 delay_time_max; + /* 0x14 */ u32 delay_time; + /* 0x18 */ Common::FixedPoint<18, 14> sample_rate; + /* 0x1C */ Common::FixedPoint<18, 14> in_gain; + /* 0x20 */ Common::FixedPoint<18, 14> feedback_gain; + /* 0x24 */ Common::FixedPoint<18, 14> wet_gain; + /* 0x28 */ Common::FixedPoint<18, 14> dry_gain; + /* 0x2C */ Common::FixedPoint<18, 14> channel_spread; + /* 0x30 */ Common::FixedPoint<18, 14> lowpass_amount; + /* 0x34 */ ParameterState state; + }; + static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1), + "DelayInfo::ParameterVersion1 has the wrong size!"); + + struct ParameterVersion2 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ s16 channel_count_max; + /* 0x0E */ s16 channel_count; + /* 0x10 */ s32 delay_time_max; + /* 0x14 */ s32 delay_time; + /* 0x18 */ s32 sample_rate; + /* 0x1C */ s32 in_gain; + /* 0x20 */ s32 feedback_gain; + /* 0x24 */ s32 wet_gain; + /* 0x28 */ s32 dry_gain; + /* 0x2C */ s32 channel_spread; + /* 0x30 */ s32 lowpass_amount; + /* 0x34 */ ParameterState state; + }; + static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2), + "DelayInfo::ParameterVersion2 has the wrong size!"); + + struct DelayLine { + Common::FixedPoint<50, 14> Read() const { + return buffer[buffer_pos]; + } + + void Write(const Common::FixedPoint<50, 14> value) { + buffer[buffer_pos] = value; + buffer_pos = static_cast((buffer_pos + 1) % buffer.size()); + } + + s32 sample_count_max{}; + s32 sample_count{}; + std::vector> buffer{}; + u32 buffer_pos{}; + Common::FixedPoint<18, 14> decay_rate{}; + }; + + struct State { + /* 0x000 */ std::array unk_000; + /* 0x020 */ std::array delay_lines; + /* 0x0B0 */ Common::FixedPoint<18, 14> feedback_gain; + /* 0x0B4 */ Common::FixedPoint<18, 14> delay_feedback_gain; + /* 0x0B8 */ Common::FixedPoint<18, 14> delay_feedback_cross_gain; + /* 0x0BC */ Common::FixedPoint<18, 14> lowpass_gain; + /* 0x0C0 */ Common::FixedPoint<18, 14> lowpass_feedback_gain; + /* 0x0C4 */ std::array, MaxChannels> lowpass_z; + }; + static_assert(sizeof(State) <= sizeof(EffectInfoBase::State), + "DelayInfo::State has the wrong size!"); + + /** + * Update the info with new parameters, version 1. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info with new parameters, version 2. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info after command generation. Usually only changes its state. + */ + void UpdateForCommandGeneration() override; + + /** + * Initialize a new result state. Version 2 only, unused. + * + * @param result_state - Result state to initialize. + */ + void InitializeResultState(EffectResultState& result_state) override; + + /** + * Update the host-side state with the ADSP-side state. Version 2 only, unused. + * + * @param cpu_state - Host-side result state to update. + * @param dsp_state - AudioRenderer-side result state to update from. + */ + void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override; + + /** + * Get a workbuffer assigned to this effect with the given index. + * + * @param index - Workbuffer index. + * @return Address of the buffer. + */ + CpuAddr GetWorkbuffer(s32 index) override; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_i3dl2_info.cpp b/src/audio_core/renderer/effect/effect_i3dl2_info.cpp new file mode 100755 index 000000000..883b7a3b9 --- /dev/null +++ b/src/audio_core/renderer/effect/effect_i3dl2_info.cpp @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/effect/effect_i3dl2_info.h" + +namespace AudioCore::AudioRenderer { + +void I3dl2ReverbInfo::Update(BehaviorInfo::ErrorInfo& error_info, + const InParameterVersion1& in_params, const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + if (IsChannelCountValid(in_specific->channel_count_max)) { + const auto old_state{params->state}; + std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + if (!IsChannelCountValid(in_specific->channel_count)) { + params->channel_count = params->channel_count_max; + } + + if (!IsChannelCountValid(in_specific->channel_count) || + old_state != ParameterState::Updated) { + params->state = old_state; + } + + if (buffer_unmapped || in_params.is_new) { + usage_state = UsageState::New; + params->state = ParameterState::Initialized; + buffer_unmapped = !pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size); + return; + } + } + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void I3dl2ReverbInfo::Update(BehaviorInfo::ErrorInfo& error_info, + const InParameterVersion2& in_params, const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + if (IsChannelCountValid(in_specific->channel_count_max)) { + const auto old_state{params->state}; + std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + if (!IsChannelCountValid(in_specific->channel_count)) { + params->channel_count = params->channel_count_max; + } + + if (!IsChannelCountValid(in_specific->channel_count) || + old_state != ParameterState::Updated) { + params->state = old_state; + } + + if (buffer_unmapped || in_params.is_new) { + usage_state = UsageState::New; + params->state = ParameterState::Initialized; + buffer_unmapped = !pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size); + return; + } + } + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void I3dl2ReverbInfo::UpdateForCommandGeneration() { + if (enabled) { + usage_state = UsageState::Enabled; + } else { + usage_state = UsageState::Disabled; + } + + auto params{reinterpret_cast(parameter.data())}; + params->state = ParameterState::Updated; +} + +void I3dl2ReverbInfo::InitializeResultState(EffectResultState& result_state) {} + +void I3dl2ReverbInfo::UpdateResultState(EffectResultState& cpu_state, + EffectResultState& dsp_state) {} + +CpuAddr I3dl2ReverbInfo::GetWorkbuffer(s32 index) { + return GetSingleBuffer(index); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_i3dl2_info.h b/src/audio_core/renderer/effect/effect_i3dl2_info.h new file mode 100755 index 000000000..7a088a627 --- /dev/null +++ b/src/audio_core/renderer/effect/effect_i3dl2_info.h @@ -0,0 +1,200 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/effect/effect_info_base.h" +#include "common/common_types.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { + +class I3dl2ReverbInfo : public EffectInfoBase { +public: + struct ParameterVersion1 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ u16 channel_count_max; + /* 0x0E */ u16 channel_count; + /* 0x10 */ char unk10[0x4]; + /* 0x14 */ u32 sample_rate; + /* 0x18 */ f32 room_HF_gain; + /* 0x1C */ f32 reference_HF; + /* 0x20 */ f32 late_reverb_decay_time; + /* 0x24 */ f32 late_reverb_HF_decay_ratio; + /* 0x28 */ f32 room_gain; + /* 0x2C */ f32 reflection_gain; + /* 0x30 */ f32 reverb_gain; + /* 0x34 */ f32 late_reverb_diffusion; + /* 0x38 */ f32 reflection_delay; + /* 0x3C */ f32 late_reverb_delay_time; + /* 0x40 */ f32 late_reverb_density; + /* 0x44 */ f32 dry_gain; + /* 0x48 */ ParameterState state; + /* 0x49 */ char unk49[0x3]; + }; + static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1), + "I3dl2ReverbInfo::ParameterVersion1 has the wrong size!"); + + struct ParameterVersion2 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ u16 channel_count_max; + /* 0x0E */ u16 channel_count; + /* 0x10 */ char unk10[0x4]; + /* 0x14 */ u32 sample_rate; + /* 0x18 */ f32 room_HF_gain; + /* 0x1C */ f32 reference_HF; + /* 0x20 */ f32 late_reverb_decay_time; + /* 0x24 */ f32 late_reverb_HF_decay_ratio; + /* 0x28 */ f32 room_gain; + /* 0x2C */ f32 reflection_gain; + /* 0x30 */ f32 reverb_gain; + /* 0x34 */ f32 late_reverb_diffusion; + /* 0x38 */ f32 reflection_delay; + /* 0x3C */ f32 late_reverb_delay_time; + /* 0x40 */ f32 late_reverb_density; + /* 0x44 */ f32 dry_gain; + /* 0x48 */ ParameterState state; + /* 0x49 */ char unk49[0x3]; + }; + static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2), + "I3dl2ReverbInfo::ParameterVersion2 has the wrong size!"); + + static constexpr u32 MaxDelayLines = 4; + static constexpr u32 MaxDelayTaps = 20; + + struct I3dl2DelayLine { + void Initialize(const s32 delay_time) { + max_delay = delay_time; + buffer.resize(delay_time + 1, 0); + buffer_end = &buffer[delay_time]; + output = &buffer[0]; + SetDelay(delay_time); + wet_gain = 0.0f; + } + + void SetDelay(const s32 delay_time) { + if (max_delay < delay_time) { + return; + } + delay = delay_time; + input = &buffer[(output - buffer.data() + delay) % (max_delay + 1)]; + } + + Common::FixedPoint<50, 14> Tick(const Common::FixedPoint<50, 14> sample) { + Write(sample); + + auto out_sample{Read()}; + + output++; + if (output >= buffer_end) { + output = buffer.data(); + } + + return out_sample; + } + + Common::FixedPoint<50, 14> Read() { + return *output; + } + + void Write(const Common::FixedPoint<50, 14> sample) { + *(input++) = sample; + if (input >= buffer_end) { + input = buffer.data(); + } + } + + Common::FixedPoint<50, 14> TapOut(const s32 index) { + auto out{input - (index + 1)}; + if (out < buffer.data()) { + out += max_delay + 1; + } + return *out; + } + + std::vector> buffer{}; + Common::FixedPoint<50, 14>* buffer_end{}; + s32 max_delay{}; + Common::FixedPoint<50, 14>* input{}; + Common::FixedPoint<50, 14>* output{}; + s32 delay{}; + f32 wet_gain{}; + }; + + struct State { + f32 lowpass_0; + f32 lowpass_1; + f32 lowpass_2; + I3dl2DelayLine early_delay_line; + std::array early_tap_steps; + f32 early_gain; + f32 late_gain; + s32 early_to_late_taps; + std::array fdn_delay_lines; + std::array decay_delay_lines0; + std::array decay_delay_lines1; + f32 last_reverb_echo; + I3dl2DelayLine center_delay_line; + std::array, MaxDelayLines> lowpass_coeff; + std::array shelf_filter; + f32 dry_gain; + }; + static_assert(sizeof(State) <= sizeof(EffectInfoBase::State), + "I3dl2ReverbInfo::State is too large!"); + + /** + * Update the info with new parameters, version 1. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info with new parameters, version 2. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info after command generation. Usually only changes its state. + */ + void UpdateForCommandGeneration() override; + + /** + * Initialize a new result state. Version 2 only, unused. + * + * @param result_state - Result state to initialize. + */ + void InitializeResultState(EffectResultState& result_state) override; + + /** + * Update the host-side state with the ADSP-side state. Version 2 only, unused. + * + * @param cpu_state - Host-side result state to update. + * @param dsp_state - AudioRenderer-side result state to update from. + */ + void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override; + + /** + * Get a workbuffer assigned to this effect with the given index. + * + * @param index - Workbuffer index. + * @return Address of the buffer. + */ + CpuAddr GetWorkbuffer(s32 index) override; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_info_base.h b/src/audio_core/renderer/effect/effect_info_base.h new file mode 100755 index 000000000..0a9836e75 --- /dev/null +++ b/src/audio_core/renderer/effect/effect_info_base.h @@ -0,0 +1,434 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/effect/effect_result_state.h" +#include "audio_core/renderer/memory/address_info.h" +#include "audio_core/renderer/memory/pool_mapper.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Base of all effects. Holds various data and functions used for all derived effects. + * Should not be used directly. + */ +class EffectInfoBase { +public: + enum class Type : u8 { + Invalid, + Mix, + Aux, + Delay, + Reverb, + I3dl2Reverb, + BiquadFilter, + LightLimiter, + Capture, + }; + + enum class UsageState { + Invalid, + New, + Enabled, + Disabled, + }; + + enum class OutStatus : u8 { + Invalid, + New, + Initialized, + Used, + Removed, + }; + + enum class ParameterState : u8 { + Initialized, + Updating, + Updated, + }; + + struct InParameterVersion1 { + /* 0x00 */ Type type; + /* 0x01 */ bool is_new; + /* 0x02 */ bool enabled; + /* 0x04 */ u32 mix_id; + /* 0x08 */ CpuAddr workbuffer; + /* 0x10 */ CpuAddr workbuffer_size; + /* 0x18 */ u32 process_order; + /* 0x1C */ char unk1C[0x4]; + /* 0x20 */ std::array specific; + }; + static_assert(sizeof(InParameterVersion1) == 0xC0, + "EffectInfoBase::InParameterVersion1 has the wrong size!"); + + struct InParameterVersion2 { + /* 0x00 */ Type type; + /* 0x01 */ bool is_new; + /* 0x02 */ bool enabled; + /* 0x04 */ u32 mix_id; + /* 0x08 */ CpuAddr workbuffer; + /* 0x10 */ CpuAddr workbuffer_size; + /* 0x18 */ u32 process_order; + /* 0x1C */ char unk1C[0x4]; + /* 0x20 */ std::array specific; + }; + static_assert(sizeof(InParameterVersion2) == 0xC0, + "EffectInfoBase::InParameterVersion2 has the wrong size!"); + + struct OutStatusVersion1 { + /* 0x00 */ OutStatus state; + /* 0x01 */ char unk01[0xF]; + }; + static_assert(sizeof(OutStatusVersion1) == 0x10, + "EffectInfoBase::OutStatusVersion1 has the wrong size!"); + + struct OutStatusVersion2 { + /* 0x00 */ OutStatus state; + /* 0x01 */ char unk01[0xF]; + /* 0x10 */ EffectResultState result_state; + }; + static_assert(sizeof(OutStatusVersion2) == 0x90, + "EffectInfoBase::OutStatusVersion2 has the wrong size!"); + + struct State { + std::array buffer; + }; + static_assert(sizeof(State) == 0x500, "EffectInfoBase::State has the wrong size!"); + + EffectInfoBase() { + Cleanup(); + } + + virtual ~EffectInfoBase() = default; + + /** + * Cleanup this effect, resetting it to a starting state. + */ + void Cleanup() { + type = Type::Invalid; + enabled = false; + mix_id = UnusedMixId; + process_order = InvalidProcessOrder; + buffer_unmapped = false; + parameter = {}; + for (auto& workbuffer : workbuffers) { + workbuffer.Setup(CpuAddr(0), 0); + } + } + + /** + * Forcibly unmap all assigned workbuffers from the AudioRenderer. + * + * @param pool_mapper - Mapper to unmap the buffers. + */ + void ForceUnmapBuffers(const PoolMapper& pool_mapper) { + for (auto& workbuffer : workbuffers) { + if (workbuffer.GetReference(false) != 0) { + pool_mapper.ForceUnmapPointer(workbuffer); + } + } + } + + /** + * Check if this effect is enabled. + * + * @return True if effect is enabled, otherwise false. + */ + bool IsEnabled() const { + return enabled; + } + + /** + * Check if this effect should not be generated. + * + * @return True if effect should be skipped, otherwise false. + */ + bool ShouldSkip() const { + return buffer_unmapped; + } + + /** + * Get the type of this effect. + * + * @return The type of this effect. See EffectInfoBase::Type + */ + Type GetType() const { + return type; + } + + /** + * Set the type of this effect. + * + * @param type_ - The new type of this effect. + */ + void SetType(const Type type_) { + type = type_; + } + + /** + * Get the mix id of this effect. + * + * @return Mix id of this effect. + */ + s32 GetMixId() const { + return mix_id; + } + + /** + * Get the processing order of this effect. + * + * @return Process order of this effect. + */ + s32 GetProcessingOrder() const { + return process_order; + } + + /** + * Get this effect's parameter data. + * + * @return Pointer to the parametter, must be cast to the correct type. + */ + u8* GetParameter() { + return parameter.data(); + } + + /** + * Get this effect's parameter data. + * + * @return Pointer to the parametter, must be cast to the correct type. + */ + u8* GetStateBuffer() { + return state.data(); + } + + /** + * Set this effect's usage state. + * + * @param usage - new usage state of this effect. + */ + void SetUsage(const UsageState usage) { + usage_state = usage; + } + + /** + * Check if this effects need to have its workbuffer information updated. + * Version 1. + * + * @param params - Input parameters. + * @return True if workbuffers need updating, otherwise false. + */ + bool ShouldUpdateWorkBufferInfo(const InParameterVersion1& params) const { + return buffer_unmapped || params.is_new; + } + + /** + * Check if this effects need to have its workbuffer information updated. + * Version 2. + * + * @param params - Input parameters. + * @return True if workbuffers need updating, otherwise false. + */ + bool ShouldUpdateWorkBufferInfo(const InParameterVersion2& params) const { + return buffer_unmapped || params.is_new; + } + + /** + * Get the current usage state of this effect. + * + * @return The current usage state. + */ + UsageState GetUsage() const { + return usage_state; + } + + /** + * Write the current state. Version 1. + * + * @param out_status - Status to write. + * @param renderer_active - Is the AudioRenderer active? + */ + void StoreStatus(OutStatusVersion1& out_status, const bool renderer_active) const { + if (renderer_active) { + if (usage_state != UsageState::Disabled) { + out_status.state = OutStatus::Used; + } else { + out_status.state = OutStatus::Removed; + } + } else if (usage_state == UsageState::New) { + out_status.state = OutStatus::Used; + } else { + out_status.state = OutStatus::Removed; + } + } + + /** + * Write the current state. Version 2. + * + * @param out_status - Status to write. + * @param renderer_active - Is the AudioRenderer active? + */ + void StoreStatus(OutStatusVersion2& out_status, const bool renderer_active) const { + if (renderer_active) { + if (usage_state != UsageState::Disabled) { + out_status.state = OutStatus::Used; + } else { + out_status.state = OutStatus::Removed; + } + } else if (usage_state == UsageState::New) { + out_status.state = OutStatus::Used; + } else { + out_status.state = OutStatus::Removed; + } + } + + /** + * Update the info with new parameters, version 1. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + virtual void Update(BehaviorInfo::ErrorInfo& error_info, + [[maybe_unused]] const InParameterVersion1& params, + [[maybe_unused]] const PoolMapper& pool_mapper) { + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + } + + /** + * Update the info with new parameters, version 2. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + virtual void Update(BehaviorInfo::ErrorInfo& error_info, + [[maybe_unused]] const InParameterVersion2& params, + [[maybe_unused]] const PoolMapper& pool_mapper) { + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + } + + /** + * Update the info after command generation. Usually only changes its state. + */ + virtual void UpdateForCommandGeneration() {} + + /** + * Initialize a new result state. Version 2 only, unused. + * + * @param result_state - Result state to initialize. + */ + virtual void InitializeResultState([[maybe_unused]] EffectResultState& result_state) {} + + /** + * Update the host-side state with the ADSP-side state. Version 2 only, unused. + * + * @param cpu_state - Host-side result state to update. + * @param dsp_state - AudioRenderer-side result state to update from. + */ + virtual void UpdateResultState([[maybe_unused]] EffectResultState& cpu_state, + [[maybe_unused]] EffectResultState& dsp_state) {} + + /** + * Get a workbuffer assigned to this effect with the given index. + * + * @param index - Workbuffer index. + * @return Address of the buffer. + */ + virtual CpuAddr GetWorkbuffer([[maybe_unused]] s32 index) { + return 0; + } + + /** + * Get the first workbuffer assigned to this effect. + * + * @param index - Workbuffer index. Unused. + * @return Address of the buffer. + */ + CpuAddr GetSingleBuffer([[maybe_unused]] const s32 index) { + if (enabled) { + return workbuffers[0].GetReference(true); + } + + if (usage_state != UsageState::Disabled) { + const auto ref{workbuffers[0].GetReference(false)}; + const auto size{workbuffers[0].GetSize()}; + if (ref != 0 && size > 0) { + // Invalidate DSP cache + } + } + return 0; + } + + /** + * Get the send buffer info, used by Aux and Capture. + * + * @return Address of the buffer info. + */ + CpuAddr GetSendBufferInfo() const { + return send_buffer_info; + } + + /** + * Get the send buffer, used by Aux and Capture. + * + * @return Address of the buffer. + */ + CpuAddr GetSendBuffer() const { + return send_buffer; + } + + /** + * Get the return buffer info, used by Aux and Capture. + * + * @return Address of the buffer info. + */ + CpuAddr GetReturnBufferInfo() const { + return return_buffer_info; + } + + /** + * Get the return buffer, used by Aux and Capture. + * + * @return Address of the buffer. + */ + CpuAddr GetReturnBuffer() const { + return return_buffer; + } + +protected: + /// Type of this effect. May be changed + Type type{Type::Invalid}; + /// Is this effect enabled? + bool enabled{}; + /// Are this effect's buffers unmapped? + bool buffer_unmapped{}; + /// Current usage state + UsageState usage_state{UsageState::Invalid}; + /// Mix id of this effect + s32 mix_id{UnusedMixId}; + /// Process order of this effect + s32 process_order{InvalidProcessOrder}; + /// Workbuffers assigned to this effect + std::array workbuffers{AddressInfo(CpuAddr(0), 0), AddressInfo(CpuAddr(0), 0)}; + /// Aux/Capture buffer info for reading + CpuAddr send_buffer_info; + /// Aux/Capture buffer for reading + CpuAddr send_buffer; + /// Aux/Capture buffer info for writing + CpuAddr return_buffer_info; + /// Aux/Capture buffer for writing + CpuAddr return_buffer; + /// Parameters of this effect + std::array parameter{}; + /// State of this effect used by the AudioRenderer across calls + std::array state{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_light_limiter_info.cpp b/src/audio_core/renderer/effect/effect_light_limiter_info.cpp new file mode 100755 index 000000000..40a6c9b34 --- /dev/null +++ b/src/audio_core/renderer/effect/effect_light_limiter_info.cpp @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/effect/effect_light_limiter_info.h" + +namespace AudioCore::AudioRenderer { + +void LightLimiterInfo::Update(BehaviorInfo::ErrorInfo& error_info, + const InParameterVersion1& in_params, const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + if (buffer_unmapped || in_params.is_new) { + usage_state = UsageState::New; + params->state = ParameterState::Initialized; + buffer_unmapped = !pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size); + } else { + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + } +} + +void LightLimiterInfo::Update(BehaviorInfo::ErrorInfo& error_info, + const InParameterVersion2& in_params, const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + if (buffer_unmapped || in_params.is_new) { + usage_state = UsageState::New; + params->state = ParameterState::Initialized; + buffer_unmapped = !pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size); + } else { + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + } +} + +void LightLimiterInfo::UpdateForCommandGeneration() { + if (enabled) { + usage_state = UsageState::Enabled; + } else { + usage_state = UsageState::Disabled; + } + + auto params{reinterpret_cast(parameter.data())}; + params->state = ParameterState::Updated; + params->statistics_reset_required = false; +} + +void LightLimiterInfo::InitializeResultState(EffectResultState& result_state) { + auto result_state_{reinterpret_cast(result_state.state.data())}; + + result_state_->channel_max_sample.fill(0); + result_state_->channel_compression_gain_min.fill(1.0f); +} + +void LightLimiterInfo::UpdateResultState(EffectResultState& cpu_state, + EffectResultState& dsp_state) { + auto cpu_statistics{reinterpret_cast(cpu_state.state.data())}; + auto dsp_statistics{reinterpret_cast(dsp_state.state.data())}; + + *cpu_statistics = *dsp_statistics; +} + +CpuAddr LightLimiterInfo::GetWorkbuffer(s32 index) { + return GetSingleBuffer(index); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_light_limiter_info.h b/src/audio_core/renderer/effect/effect_light_limiter_info.h new file mode 100755 index 000000000..af4b2b63b --- /dev/null +++ b/src/audio_core/renderer/effect/effect_light_limiter_info.h @@ -0,0 +1,133 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/effect/effect_info_base.h" +#include "common/common_types.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { + +class LightLimiterInfo : public EffectInfoBase { +public: + struct ParameterVersion1 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ u16 channel_count_max; + /* 0x0E */ u16 channel_count; + /* 0x0C */ u32 sample_rate; + /* 0x14 */ s32 look_ahead_time_max; + /* 0x18 */ s32 attack_time; + /* 0x1C */ s32 release_time; + /* 0x20 */ s32 look_ahead_time; + /* 0x24 */ f32 attack_coeff; + /* 0x28 */ f32 release_coeff; + /* 0x2C */ f32 threshold; + /* 0x30 */ f32 input_gain; + /* 0x34 */ f32 output_gain; + /* 0x38 */ s32 look_ahead_samples_min; + /* 0x3C */ s32 look_ahead_samples_max; + /* 0x40 */ ParameterState state; + /* 0x41 */ bool statistics_enabled; + /* 0x42 */ bool statistics_reset_required; + /* 0x43 */ char unk43[0x1]; + }; + static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1), + "LightLimiterInfo::ParameterVersion1 has the wrong size!"); + + struct ParameterVersion2 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ u16 channel_count_max; + /* 0x0E */ u16 channel_count; + /* 0x0C */ u32 sample_rate; + /* 0x14 */ s32 look_ahead_time_max; + /* 0x18 */ s32 attack_time; + /* 0x1C */ s32 release_time; + /* 0x20 */ s32 look_ahead_time; + /* 0x24 */ f32 attack_coeff; + /* 0x28 */ f32 release_coeff; + /* 0x2C */ f32 threshold; + /* 0x30 */ f32 input_gain; + /* 0x34 */ f32 output_gain; + /* 0x38 */ s32 look_ahead_samples_min; + /* 0x3C */ s32 look_ahead_samples_max; + /* 0x40 */ ParameterState state; + /* 0x41 */ bool statistics_enabled; + /* 0x42 */ bool statistics_reset_required; + /* 0x43 */ char unk43[0x1]; + }; + static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2), + "LightLimiterInfo::ParameterVersion2 has the wrong size!"); + + struct State { + std::array, MaxChannels> samples_average; + std::array, MaxChannels> compression_gain; + std::array look_ahead_sample_offsets; + std::array>, MaxChannels> look_ahead_sample_buffers; + }; + static_assert(sizeof(State) <= sizeof(EffectInfoBase::State), + "LightLimiterInfo::State has the wrong size!"); + + struct StatisticsInternal { + /* 0x00 */ std::array channel_max_sample; + /* 0x18 */ std::array channel_compression_gain_min; + }; + static_assert(sizeof(StatisticsInternal) == 0x30, + "LightLimiterInfo::StatisticsInternal has the wrong size!"); + + /** + * Update the info with new parameters, version 1. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info with new parameters, version 2. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info after command generation. Usually only changes its state. + */ + void UpdateForCommandGeneration() override; + + /** + * Initialize a new limiter statistics result state. Version 2 only. + * + * @param result_state - Result state to initialize. + */ + void InitializeResultState(EffectResultState& result_state) override; + + /** + * Update the host-side limiter statistics with the ADSP-side one. Version 2 only. + * + * @param cpu_state - Host-side result state to update. + * @param dsp_state - AudioRenderer-side result state to update from. + */ + void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override; + + /** + * Get a workbuffer assigned to this effect with the given index. + * + * @param index - Workbuffer index. + * @return Address of the buffer. + */ + CpuAddr GetWorkbuffer(s32 index) override; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_reset.h b/src/audio_core/renderer/effect/effect_reset.h new file mode 100755 index 000000000..9d496a03e --- /dev/null +++ b/src/audio_core/renderer/effect/effect_reset.h @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/renderer/effect/effect_aux_info.h" +#include "audio_core/renderer/effect/effect_biquad_filter_info.h" +#include "audio_core/renderer/effect/effect_buffer_mixer_info.h" +#include "audio_core/renderer/effect/effect_capture_info.h" +#include "audio_core/renderer/effect/effect_delay_info.h" +#include "audio_core/renderer/effect/effect_i3dl2_info.h" +#include "audio_core/renderer/effect/effect_light_limiter_info.h" +#include "audio_core/renderer/effect/effect_reverb_info.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Reset an effect, and create a new one of the given type. + * + * @param effect - Effect to reset and re-construct. + * @param type - Type of the new effect to create. + */ +static void ResetEffect(EffectInfoBase* effect, const EffectInfoBase::Type type) { + *effect = {}; + + switch (type) { + case EffectInfoBase::Type::Invalid: + std::construct_at(effect); + effect->SetType(EffectInfoBase::Type::Invalid); + break; + case EffectInfoBase::Type::Mix: + std::construct_at(reinterpret_cast(effect)); + effect->SetType(EffectInfoBase::Type::Mix); + break; + case EffectInfoBase::Type::Aux: + std::construct_at(reinterpret_cast(effect)); + effect->SetType(EffectInfoBase::Type::Aux); + break; + case EffectInfoBase::Type::Delay: + std::construct_at(reinterpret_cast(effect)); + effect->SetType(EffectInfoBase::Type::Delay); + break; + case EffectInfoBase::Type::Reverb: + std::construct_at(reinterpret_cast(effect)); + effect->SetType(EffectInfoBase::Type::Reverb); + break; + case EffectInfoBase::Type::I3dl2Reverb: + std::construct_at(reinterpret_cast(effect)); + effect->SetType(EffectInfoBase::Type::I3dl2Reverb); + break; + case EffectInfoBase::Type::BiquadFilter: + std::construct_at(reinterpret_cast(effect)); + effect->SetType(EffectInfoBase::Type::BiquadFilter); + break; + case EffectInfoBase::Type::LightLimiter: + std::construct_at(reinterpret_cast(effect)); + effect->SetType(EffectInfoBase::Type::LightLimiter); + break; + case EffectInfoBase::Type::Capture: + std::construct_at(reinterpret_cast(effect)); + effect->SetType(EffectInfoBase::Type::Capture); + break; + } +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_result_state.h b/src/audio_core/renderer/effect/effect_result_state.h new file mode 100755 index 000000000..ae096ad69 --- /dev/null +++ b/src/audio_core/renderer/effect/effect_result_state.h @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { + +struct EffectResultState { + std::array state; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_reverb_info.cpp b/src/audio_core/renderer/effect/effect_reverb_info.cpp new file mode 100755 index 000000000..864622941 --- /dev/null +++ b/src/audio_core/renderer/effect/effect_reverb_info.cpp @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/effect/effect_reverb_info.h" + +namespace AudioCore::AudioRenderer { + +void ReverbInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + if (IsChannelCountValid(in_specific->channel_count_max)) { + const auto old_state{params->state}; + std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + if (!IsChannelCountValid(in_specific->channel_count)) { + params->channel_count = params->channel_count_max; + } + + if (!IsChannelCountValid(in_specific->channel_count) || + old_state != ParameterState::Updated) { + params->state = old_state; + } + + if (buffer_unmapped || in_params.is_new) { + usage_state = UsageState::New; + params->state = ParameterState::Initialized; + buffer_unmapped = !pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size); + return; + } + } + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void ReverbInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + if (IsChannelCountValid(in_specific->channel_count_max)) { + const auto old_state{params->state}; + std::memcpy(params, in_specific, sizeof(ParameterVersion2)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + if (!IsChannelCountValid(in_specific->channel_count)) { + params->channel_count = params->channel_count_max; + } + + if (!IsChannelCountValid(in_specific->channel_count) || + old_state != ParameterState::Updated) { + params->state = old_state; + } + + if (buffer_unmapped || in_params.is_new) { + usage_state = UsageState::New; + params->state = ParameterState::Initialized; + buffer_unmapped = !pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size); + return; + } + } + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void ReverbInfo::UpdateForCommandGeneration() { + if (enabled) { + usage_state = UsageState::Enabled; + } else { + usage_state = UsageState::Disabled; + } + + auto params{reinterpret_cast(parameter.data())}; + params->state = ParameterState::Updated; +} + +void ReverbInfo::InitializeResultState(EffectResultState& result_state) {} + +void ReverbInfo::UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) {} + +CpuAddr ReverbInfo::GetWorkbuffer(s32 index) { + return GetSingleBuffer(index); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_reverb_info.h b/src/audio_core/renderer/effect/effect_reverb_info.h new file mode 100755 index 000000000..b4df9f6ef --- /dev/null +++ b/src/audio_core/renderer/effect/effect_reverb_info.h @@ -0,0 +1,190 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/effect/effect_info_base.h" +#include "common/common_types.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { + +class ReverbInfo : public EffectInfoBase { +public: + struct ParameterVersion1 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ u16 channel_count_max; + /* 0x0E */ u16 channel_count; + /* 0x10 */ u32 sample_rate; + /* 0x14 */ u32 early_mode; + /* 0x18 */ s32 early_gain; + /* 0x1C */ s32 pre_delay; + /* 0x20 */ s32 late_mode; + /* 0x24 */ s32 late_gain; + /* 0x28 */ s32 decay_time; + /* 0x2C */ s32 high_freq_Decay_ratio; + /* 0x30 */ s32 colouration; + /* 0x34 */ s32 base_gain; + /* 0x38 */ s32 wet_gain; + /* 0x3C */ s32 dry_gain; + /* 0x40 */ ParameterState state; + }; + static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1), + "ReverbInfo::ParameterVersion1 has the wrong size!"); + + struct ParameterVersion2 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ u16 channel_count_max; + /* 0x0E */ u16 channel_count; + /* 0x10 */ u32 sample_rate; + /* 0x14 */ u32 early_mode; + /* 0x18 */ s32 early_gain; + /* 0x1C */ s32 pre_delay; + /* 0x20 */ s32 late_mode; + /* 0x24 */ s32 late_gain; + /* 0x28 */ s32 decay_time; + /* 0x2C */ s32 high_freq_decay_ratio; + /* 0x30 */ s32 colouration; + /* 0x34 */ s32 base_gain; + /* 0x38 */ s32 wet_gain; + /* 0x3C */ s32 dry_gain; + /* 0x40 */ ParameterState state; + }; + static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2), + "ReverbInfo::ParameterVersion2 has the wrong size!"); + + static constexpr u32 MaxDelayLines = 4; + static constexpr u32 MaxDelayTaps = 10; + static constexpr u32 NumEarlyModes = 5; + static constexpr u32 NumLateModes = 5; + + struct ReverbDelayLine { + void Initialize(const s32 delay_time, const f32 decay_rate) { + buffer.resize(delay_time + 1, 0); + buffer_end = &buffer[delay_time]; + output = &buffer[0]; + decay = decay_rate; + sample_count_max = delay_time; + SetDelay(delay_time); + } + + void SetDelay(const s32 delay_time) { + if (sample_count_max < delay_time) { + return; + } + sample_count = delay_time; + input = &buffer[(output - buffer.data() + sample_count) % (sample_count_max + 1)]; + } + + Common::FixedPoint<50, 14> Tick(const Common::FixedPoint<50, 14> sample) { + Write(sample); + + auto out_sample{Read()}; + + output++; + if (output >= buffer_end) { + output = buffer.data(); + } + + return out_sample; + } + + Common::FixedPoint<50, 14> Read() { + return *output; + } + + void Write(const Common::FixedPoint<50, 14> sample) { + *(input++) = sample; + if (input >= buffer_end) { + input = buffer.data(); + } + } + + Common::FixedPoint<50, 14> TapOut(const s32 index) { + auto out{input - (index + 1)}; + if (out < buffer.data()) { + out += sample_count; + } + return *out; + } + + s32 sample_count{}; + s32 sample_count_max{}; + std::vector> buffer{}; + Common::FixedPoint<50, 14>* buffer_end; + Common::FixedPoint<50, 14>* input{}; + Common::FixedPoint<50, 14>* output{}; + Common::FixedPoint<50, 14> decay{}; + }; + + struct State { + ReverbDelayLine pre_delay_line; + ReverbDelayLine center_delay_line; + std::array early_delay_times; + std::array, MaxDelayTaps> early_gains; + s32 pre_delay_time; + std::array decay_delay_lines; + std::array fdn_delay_lines; + std::array, MaxDelayLines> hf_decay_gain; + std::array, MaxDelayLines> hf_decay_prev_gain; + std::array, MaxDelayLines> prev_feedback_output; + }; + static_assert(sizeof(State) <= sizeof(EffectInfoBase::State), + "ReverbInfo::State is too large!"); + + /** + * Update the info with new parameters, version 1. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info with new parameters, version 2. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info after command generation. Usually only changes its state. + */ + void UpdateForCommandGeneration() override; + + /** + * Initialize a new result state. Version 2 only, unused. + * + * @param result_state - Result state to initialize. + */ + void InitializeResultState(EffectResultState& result_state) override; + + /** + * Update the host-side state with the ADSP-side state. Version 2 only, unused. + * + * @param cpu_state - Host-side result state to update. + * @param dsp_state - AudioRenderer-side result state to update from. + */ + void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override; + + /** + * Get a workbuffer assigned to this effect with the given index. + * + * @param index - Workbuffer index. + * @return Address of the buffer. + */ + CpuAddr GetWorkbuffer(s32 index) override; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/memory/address_info.h b/src/audio_core/renderer/memory/address_info.h new file mode 100755 index 000000000..4cfefea8e --- /dev/null +++ b/src/audio_core/renderer/memory/address_info.h @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/renderer/memory/memory_pool_info.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { + +/** + * Represents a region of mapped or unmapped memory. + */ +class AddressInfo { +public: + AddressInfo() = default; + AddressInfo(CpuAddr cpu_address_, u64 size_) : cpu_address{cpu_address_}, size{size_} {} + + /** + * Setup a new AddressInfo. + * + * @param cpu_address - The CPU address of this region. + * @param size - The size of this region. + */ + void Setup(CpuAddr cpu_address_, u64 size_) { + cpu_address = cpu_address_; + size = size_; + memory_pool = nullptr; + dsp_address = 0; + } + + /** + * Get the CPU address. + * + * @return The CpuAddr address + */ + CpuAddr GetCpuAddr() const { + return cpu_address; + } + + /** + * Assign this region to a memory pool. + * + * @param memory_pool_ - Memory pool to assign. + * @return The CpuAddr address of this region. + */ + void SetPool(MemoryPoolInfo* memory_pool_) { + memory_pool = memory_pool_; + } + + /** + * Get the size of this region. + * + * @return The size of this region. + */ + u64 GetSize() const { + return size; + } + + /** + * Get the ADSP address for this region. + * + * @return The ADSP address for this region. + */ + CpuAddr GetForceMappedDspAddr() const { + return dsp_address; + } + + /** + * Set the ADSP address for this region. + * + * @param dsp_addr - The new ADSP address for this region. + */ + void SetForceMappedDspAddr(CpuAddr dsp_addr) { + dsp_address = dsp_addr; + } + + /** + * Check whether this region has an active memory pool. + * + * @return True if this region has a mapped memory pool, otherwise false. + */ + bool HasMappedMemoryPool() const { + return memory_pool != nullptr && memory_pool->GetDspAddress() != 0; + } + + /** + * Check whether this region is mapped to the ADSP. + * + * @return True if this region is mapped, otherwise false. + */ + bool IsMapped() const { + return HasMappedMemoryPool() || dsp_address != 0; + } + + /** + * Get a usable reference to this region of memory. + * + * @param mark_in_use - Whether this region should be marked as being in use. + * @return A valid memory address if valid, otherwise 0. + */ + CpuAddr GetReference(bool mark_in_use) { + if (!HasMappedMemoryPool()) { + return dsp_address; + } + + if (mark_in_use) { + memory_pool->SetUsed(true); + } + + return memory_pool->Translate(cpu_address, size); + } + +private: + /// CPU address of this region + CpuAddr cpu_address; + /// Size of this region + u64 size; + /// The memory this region is mapped to + MemoryPoolInfo* memory_pool; + /// ADSP address of this region + CpuAddr dsp_address; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/memory/memory_pool_info.cpp b/src/audio_core/renderer/memory/memory_pool_info.cpp new file mode 100755 index 000000000..9b7824af1 --- /dev/null +++ b/src/audio_core/renderer/memory/memory_pool_info.cpp @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/memory/memory_pool_info.h" + +namespace AudioCore::AudioRenderer { + +CpuAddr MemoryPoolInfo::GetCpuAddress() const { + return cpu_address; +} + +CpuAddr MemoryPoolInfo::GetDspAddress() const { + return dsp_address; +} + +u64 MemoryPoolInfo::GetSize() const { + return size; +} + +MemoryPoolInfo::Location MemoryPoolInfo::GetLocation() const { + return location; +} + +void MemoryPoolInfo::SetCpuAddress(const CpuAddr address, const u64 size_) { + cpu_address = address; + size = size_; +} + +void MemoryPoolInfo::SetDspAddress(const CpuAddr address) { + dsp_address = address; +} + +bool MemoryPoolInfo::Contains(const CpuAddr address_, const u64 size_) const { + return cpu_address <= address_ && (address_ + size_) <= (cpu_address + size); +} + +bool MemoryPoolInfo::IsMapped() const { + return dsp_address != 0; +} + +CpuAddr MemoryPoolInfo::Translate(const CpuAddr address, const u64 size_) const { + if (!Contains(address, size_)) { + return 0; + } + + if (!IsMapped()) { + return 0; + } + + return dsp_address + (address - cpu_address); +} + +void MemoryPoolInfo::SetUsed(const bool used) { + in_use = used; +} + +bool MemoryPoolInfo::IsUsed() const { + return in_use; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/memory/memory_pool_info.h b/src/audio_core/renderer/memory/memory_pool_info.h new file mode 100755 index 000000000..537a466ec --- /dev/null +++ b/src/audio_core/renderer/memory/memory_pool_info.h @@ -0,0 +1,170 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * CPU pools are mapped in user memory with the supplied process_handle (see PoolMapper). + */ +class MemoryPoolInfo { +public: + /** + * The location of this pool. + * CPU pools are mapped in user memory with the supplied process_handle (see PoolMapper). + * DSP pools are mapped in the current process sysmodule. + */ + enum class Location { + CPU = 1, + DSP = 2, + }; + + /** + * Current state of the pool + */ + enum class State { + Invalid, + Aquired, + RequestDetach, + Detached, + RequestAttach, + Attached, + Released, + }; + + /** + * Result code for updating the pool (See InfoUpdater::Update) + */ + enum class ResultState { + Success, + BadParam, + MapFailed, + InUse, + }; + + /** + * Input parameters coming from the game which are used to update current pools + * (See InfoUpdater::Update) + */ + struct InParameter { + /* 0x00 */ u64 address; + /* 0x08 */ u64 size; + /* 0x10 */ State state; + /* 0x14 */ bool in_use; + /* 0x18 */ char unk18[0x8]; + }; + static_assert(sizeof(InParameter) == 0x20, "MemoryPoolInfo::InParameter has the wrong size!"); + + /** + * Output status sent back to the game on update (See InfoUpdater::Update) + */ + struct OutStatus { + /* 0x00 */ State state; + /* 0x04 */ char unk04[0xC]; + }; + static_assert(sizeof(OutStatus) == 0x10, "MemoryPoolInfo::OutStatus has the wrong size!"); + + MemoryPoolInfo() = default; + MemoryPoolInfo(Location location_) : location{location_} {} + + /** + * Get the CPU address for this pool. + * + * @return The CPU address of this pool. + */ + CpuAddr GetCpuAddress() const; + + /** + * Get the DSP address for this pool. + * + * @return The DSP address of this pool. + */ + CpuAddr GetDspAddress() const; + + /** + * Get the size of this pool. + * + * @return The size of this pool. + */ + u64 GetSize() const; + + /** + * Get the location of this pool. + * + * @return The location for the pool (see MemoryPoolInfo::Location). + */ + Location GetLocation() const; + + /** + * Set the CPU address for this pool. + * + * @param address - The new CPU address for this pool. + * @param size - The new size for this pool. + */ + void SetCpuAddress(CpuAddr address, u64 size); + + /** + * Set the DSP address for this pool. + * + * @param address - The new DSP address for this pool. + */ + void SetDspAddress(CpuAddr address); + + /** + * Check whether the pool contains a given range. + * + * @param address - The buffer address to look for. + * @param size - The size of the given buffer. + * @return True if the range is within this pool, otherwise false. + */ + bool Contains(CpuAddr address, u64 size) const; + + /** + * Check whether this pool is mapped, which is when the dsp address is set. + * + * @return True if the pool is mapped, otherwise false. + */ + bool IsMapped() const; + + /** + * Translates a given CPU range into a relative offset for the DSP. + * + * @param address - The buffer address to look for. + * @param size - The size of the given buffer. + * @return Pointer to the DSP-mapped memory. + */ + CpuAddr Translate(CpuAddr address, u64 size) const; + + /** + * Set or unset whether this memory pool is in use. + * + * @param used - Use state for this pool. + */ + void SetUsed(bool used); + + /** + * Get whether this pool is in use. + * + * @return True if in use, otherwise false. + */ + bool IsUsed() const; + +private: + /// Base address for the CPU-side memory + CpuAddr cpu_address{}; + /// Base address for the DSP-side memory + CpuAddr dsp_address{}; + /// Size of this pool + u64 size{}; + /// Location of this pool, either CPU or DSP + Location location{Location::DSP}; + /// If this pool is in use + bool in_use{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/memory/pool_mapper.cpp b/src/audio_core/renderer/memory/pool_mapper.cpp new file mode 100755 index 000000000..2baf2ce08 --- /dev/null +++ b/src/audio_core/renderer/memory/pool_mapper.cpp @@ -0,0 +1,243 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/memory/address_info.h" +#include "audio_core/renderer/memory/pool_mapper.h" +#include "core/hle/kernel/k_process.h" +#include "core/hle/kernel/svc.h" + +namespace AudioCore::AudioRenderer { + +PoolMapper::PoolMapper(u32 process_handle_, bool force_map_) + : process_handle{process_handle_}, force_map{force_map_} {} + +PoolMapper::PoolMapper(u32 process_handle_, std::span pool_infos_, u32 pool_count_, + bool force_map_) + : process_handle{process_handle_}, pool_infos{pool_infos_.data()}, + pool_count{pool_count_}, force_map{force_map_} {} + +void PoolMapper::ClearUseState(std::span pools, const u32 count) { + for (u32 i = 0; i < count; i++) { + pools[i].SetUsed(false); + } +} + +MemoryPoolInfo* PoolMapper::FindMemoryPool(MemoryPoolInfo* pools, const u64 count, + const CpuAddr address, const u64 size) const { + auto pool{pools}; + for (u64 i = 0; i < count; i++, pool++) { + if (pool->Contains(address, size)) { + return pool; + } + } + return nullptr; +} + +MemoryPoolInfo* PoolMapper::FindMemoryPool(const CpuAddr address, const u64 size) const { + auto pool{pool_infos}; + for (u64 i = 0; i < pool_count; i++, pool++) { + if (pool->Contains(address, size)) { + return pool; + } + } + return nullptr; +} + +bool PoolMapper::FillDspAddr(AddressInfo& address_info, MemoryPoolInfo* pools, + const u32 count) const { + if (address_info.GetCpuAddr() == 0) { + address_info.SetPool(nullptr); + return false; + } + + auto found_pool{ + FindMemoryPool(pools, count, address_info.GetCpuAddr(), address_info.GetSize())}; + if (found_pool != nullptr) { + address_info.SetPool(found_pool); + return true; + } + + if (force_map) { + address_info.SetForceMappedDspAddr(address_info.GetCpuAddr()); + } else { + address_info.SetPool(nullptr); + } + + return false; +} + +bool PoolMapper::FillDspAddr(AddressInfo& address_info) const { + if (address_info.GetCpuAddr() == 0) { + address_info.SetPool(nullptr); + return false; + } + + auto found_pool{FindMemoryPool(address_info.GetCpuAddr(), address_info.GetSize())}; + if (found_pool != nullptr) { + address_info.SetPool(found_pool); + return true; + } + + if (force_map) { + address_info.SetForceMappedDspAddr(address_info.GetCpuAddr()); + } else { + address_info.SetPool(nullptr); + } + + return false; +} + +bool PoolMapper::TryAttachBuffer(BehaviorInfo::ErrorInfo& error_info, AddressInfo& address_info, + const CpuAddr address, const u64 size) const { + address_info.Setup(address, size); + + if (!FillDspAddr(address_info)) { + error_info.error_code = Service::Audio::ERR_POOL_MAPPING_FAILED; + error_info.address = address; + return force_map; + } + + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + return true; +} + +bool PoolMapper::IsForceMapEnabled() const { + return force_map; +} + +u32 PoolMapper::GetProcessHandle(const MemoryPoolInfo* pool) const { + switch (pool->GetLocation()) { + case MemoryPoolInfo::Location::CPU: + return process_handle; + case MemoryPoolInfo::Location::DSP: + return Kernel::Svc::CurrentProcess; + } + LOG_WARNING(Service_Audio, "Invalid MemoryPoolInfo location!"); + return Kernel::Svc::CurrentProcess; +} + +bool PoolMapper::Map([[maybe_unused]] const u32 handle, [[maybe_unused]] const CpuAddr cpu_addr, + [[maybe_unused]] const u64 size) const { + // nn::audio::dsp::MapUserPointer(handle, cpu_addr, size); + return true; +} + +bool PoolMapper::Map(MemoryPoolInfo& pool) const { + switch (pool.GetLocation()) { + case MemoryPoolInfo::Location::CPU: + // Map with process_handle + pool.SetDspAddress(pool.GetCpuAddress()); + return true; + case MemoryPoolInfo::Location::DSP: + // Map with Kernel::Svc::CurrentProcess + pool.SetDspAddress(pool.GetCpuAddress()); + return true; + default: + LOG_WARNING(Service_Audio, "Invalid MemoryPoolInfo location={}!", + static_cast(pool.GetLocation())); + return false; + } +} + +bool PoolMapper::Unmap([[maybe_unused]] const u32 handle, [[maybe_unused]] const CpuAddr cpu_addr, + [[maybe_unused]] const u64 size) const { + // nn::audio::dsp::UnmapUserPointer(handle, cpu_addr, size); + return true; +} + +bool PoolMapper::Unmap(MemoryPoolInfo& pool) const { + [[maybe_unused]] u32 handle{0}; + + switch (pool.GetLocation()) { + case MemoryPoolInfo::Location::CPU: + handle = process_handle; + break; + case MemoryPoolInfo::Location::DSP: + handle = Kernel::Svc::CurrentProcess; + break; + } + // nn::audio::dsp::UnmapUserPointer(handle, pool->cpu_address, pool->size); + pool.SetCpuAddress(0, 0); + pool.SetDspAddress(0); + return true; +} + +void PoolMapper::ForceUnmapPointer(const AddressInfo& address_info) const { + if (force_map) { + [[maybe_unused]] auto found_pool{ + FindMemoryPool(address_info.GetCpuAddr(), address_info.GetSize())}; + // nn::audio::dsp::UnmapUserPointer(this->processHandle, address_info.GetCpuAddr(), 0); + } +} + +MemoryPoolInfo::ResultState PoolMapper::Update(MemoryPoolInfo& pool, + const MemoryPoolInfo::InParameter& in_params, + MemoryPoolInfo::OutStatus& out_params) const { + if (in_params.state != MemoryPoolInfo::State::RequestAttach && + in_params.state != MemoryPoolInfo::State::RequestDetach) { + return MemoryPoolInfo::ResultState::Success; + } + + if (in_params.address == 0 || in_params.size == 0 || !Common::Is4KBAligned(in_params.address) || + !Common::Is4KBAligned(in_params.size)) { + return MemoryPoolInfo::ResultState::BadParam; + } + + switch (in_params.state) { + case MemoryPoolInfo::State::RequestAttach: + pool.SetCpuAddress(in_params.address, in_params.size); + + Map(pool); + + if (pool.IsMapped()) { + out_params.state = MemoryPoolInfo::State::Attached; + return MemoryPoolInfo::ResultState::Success; + } + pool.SetCpuAddress(0, 0); + return MemoryPoolInfo::ResultState::MapFailed; + + case MemoryPoolInfo::State::RequestDetach: + if (pool.GetCpuAddress() != in_params.address || pool.GetSize() != in_params.size) { + return MemoryPoolInfo::ResultState::BadParam; + } + + if (pool.IsUsed()) { + return MemoryPoolInfo::ResultState::InUse; + } + + Unmap(pool); + + pool.SetCpuAddress(0, 0); + pool.SetDspAddress(0); + out_params.state = MemoryPoolInfo::State::Detached; + return MemoryPoolInfo::ResultState::Success; + + default: + LOG_ERROR(Service_Audio, "Invalid MemoryPoolInfo::State!"); + break; + } + + return MemoryPoolInfo::ResultState::Success; +} + +bool PoolMapper::InitializeSystemPool(MemoryPoolInfo& pool, const u8* memory, + const u64 size_) const { + switch (pool.GetLocation()) { + case MemoryPoolInfo::Location::CPU: + return false; + case MemoryPoolInfo::Location::DSP: + pool.SetCpuAddress(reinterpret_cast(memory), size_); + if (Map(Kernel::Svc::CurrentProcess, reinterpret_cast(memory), size_)) { + pool.SetDspAddress(pool.GetCpuAddress()); + return true; + } + return false; + default: + LOG_WARNING(Service_Audio, "Invalid MemoryPoolInfo location={}!", + static_cast(pool.GetLocation())); + return false; + } +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/memory/pool_mapper.h b/src/audio_core/renderer/memory/pool_mapper.h new file mode 100755 index 000000000..9a691da7a --- /dev/null +++ b/src/audio_core/renderer/memory/pool_mapper.h @@ -0,0 +1,179 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/memory/memory_pool_info.h" +#include "common/common_types.h" +#include "core/hle/service/audio/errors.h" + +namespace AudioCore::AudioRenderer { +class AddressInfo; + +/** + * Utility functions for managing MemoryPoolInfos + */ +class PoolMapper { +public: + explicit PoolMapper(u32 process_handle, bool force_map); + explicit PoolMapper(u32 process_handle, std::span pool_infos, u32 pool_count, + bool force_map); + + /** + * Clear the usage state for all given pools. + * + * @param pools - The memory pools to clear. + * @param count - The number of pools. + */ + static void ClearUseState(std::span pools, u32 count); + + /** + * Find the memory pool containing the given address and size from a given list of pools. + * + * @param pools - The memory pools to search within. + * @param count - The number of pools. + * @param address - The address of the region to find. + * @param size - The size of the region to find. + * @return Pointer to the memory pool if found, otherwise nullptr. + */ + MemoryPoolInfo* FindMemoryPool(MemoryPoolInfo* pools, u64 count, CpuAddr address, + u64 size) const; + + /** + * Find the memory pool containing the given address and size from the PoolMapper's memory pool. + * + * @param address - The address of the region to find. + * @param size - The size of the region to find. + * @return Pointer to the memory pool if found, otherwise nullptr. + */ + MemoryPoolInfo* FindMemoryPool(CpuAddr address, u64 size) const; + + /** + * Set the PoolMapper's memory pool to one in the given list of pools, which contains + * address_info. + * + * @param address_info - The expected region to find within pools. + * @param pools - The list of pools to search within. + * @param count - The number of pools given. + * @return True if successfully mapped, otherwise false. + */ + bool FillDspAddr(AddressInfo& address_info, MemoryPoolInfo* pools, u32 count) const; + + /** + * Set the PoolMapper's memory pool to the one containing address_info. + * + * @param address_info - The address to find the memory pool for. + * @return True if successfully mapped, otherwise false. + */ + bool FillDspAddr(AddressInfo& address_info) const; + + /** + * Try to attach a {address, size} region to the given address_info, and map it. Fills in the + * given error_info and address_info. + * + * @param error_info - Output error info. + * @param address_info - Output address info, initialized with the given {address, size} and + * attempted to map. + * @param address - Address of the region to map. + * @param size - Size of the region to map. + * @return True if successfully attached, otherwise false. + */ + bool TryAttachBuffer(BehaviorInfo::ErrorInfo& error_info, AddressInfo& address_info, + CpuAddr address, u64 size) const; + + /** + * Return whether force mapping is enabled. + * + * @return True if force mapping is enabled, otherwise false. + */ + bool IsForceMapEnabled() const; + + /** + * Get the process handle, depending on location. + * + * @param pool - The pool to check the location of. + * @return CurrentProcessHandle if location == DSP, + * the PoolMapper's process_handle if location == CPU + */ + u32 GetProcessHandle(const MemoryPoolInfo* pool) const; + + /** + * Map the given region with the given handle. This is a no-op. + * + * @param handle - The process handle to map to. + * @param cpu_addr - Address to map. + * @param size - Size to map. + * @return True if successfully mapped, otherwise false. + */ + bool Map(u32 handle, CpuAddr cpu_addr, u64 size) const; + + /** + * Map the given memory pool. + * + * @param pool - The pool to map. + * @return True if successfully mapped, otherwise false. + */ + bool Map(MemoryPoolInfo& pool) const; + + /** + * Unmap the given region with the given handle. + * + * @param handle - The process handle to unmap to. + * @param cpu_addr - Address to unmap. + * @param size - Size to unmap. + * @return True if successfully unmapped, otherwise false. + */ + bool Unmap(u32 handle, CpuAddr cpu_addr, u64 size) const; + + /** + * Unmap the given memory pool. + * + * @param pool - The pool to unmap. + * @return True if successfully unmapped, otherwise false. + */ + bool Unmap(MemoryPoolInfo& pool) const; + + /** + * Forcibly unmap the given region. + * + * @param address_info - The region to unmap. + */ + void ForceUnmapPointer(const AddressInfo& address_info) const; + + /** + * Update the given memory pool. + * + * @param pool - Pool to update. + * @param in_params - Input parameters for the update. + * @param out_params - Output parameters for the update. + * @return The result of the update. See MemoryPoolInfo::ResultState + */ + MemoryPoolInfo::ResultState Update(MemoryPoolInfo& pool, + const MemoryPoolInfo::InParameter& in_params, + MemoryPoolInfo::OutStatus& out_params) const; + + /** + * Initialize the PoolMapper's memory pool. + * + * @param pool - Input pool to initialize. + * @param memory - Pointer to the memory region for the pool. + * @param size - Size of the memory region for the pool. + * @return True if initialized successfully, otherwise false. + */ + bool InitializeSystemPool(MemoryPoolInfo& pool, const u8* memory, u64 size) const; + +private: + /// Process handle for this mapper, used when location == CPU + u32 process_handle; + /// List of memory pools assigned to this mapper + MemoryPoolInfo* pool_infos{}; + /// The number of pools + u64 pool_count{}; + /// Is forced mapping enabled + bool force_map; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/mix/mix_context.cpp b/src/audio_core/renderer/mix/mix_context.cpp new file mode 100755 index 000000000..2427c83ed --- /dev/null +++ b/src/audio_core/renderer/mix/mix_context.cpp @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/renderer/mix/mix_context.h" +#include "audio_core/renderer/splitter/splitter_context.h" + +namespace AudioCore::AudioRenderer { + +void MixContext::Initialize(std::span sorted_mix_infos_, std::span mix_infos_, + const u32 count_, std::span effect_process_order_buffer_, + const u32 effect_count_, std::span node_states_workbuffer, + const u64 node_buffer_size, std::span edge_matrix_workbuffer, + const u64 edge_matrix_size) { + count = count_; + sorted_mix_infos = sorted_mix_infos_; + mix_infos = mix_infos_; + effect_process_order_buffer = effect_process_order_buffer_; + effect_count = effect_count_; + + if (node_states_workbuffer.size() > 0 && edge_matrix_workbuffer.size() > 0) { + node_states.Initialize(node_states_workbuffer, node_buffer_size, count); + edge_matrix.Initialize(edge_matrix_workbuffer, edge_matrix_size, count); + } + + for (s32 i = 0; i < count; i++) { + sorted_mix_infos[i] = &mix_infos[i]; + } +} + +MixInfo* MixContext::GetSortedInfo(const s32 index) { + return sorted_mix_infos[index]; +} + +void MixContext::SetSortedInfo(const s32 index, MixInfo& mix_info) { + sorted_mix_infos[index] = &mix_info; +} + +MixInfo* MixContext::GetInfo(const s32 index) { + return &mix_infos[index]; +} + +MixInfo* MixContext::GetFinalMixInfo() { + return &mix_infos[0]; +} + +s32 MixContext::GetCount() const { + return count; +} + +void MixContext::UpdateDistancesFromFinalMix() { + for (s32 i = 0; i < count; i++) { + mix_infos[i].distance_from_final_mix = InvalidDistanceFromFinalMix; + } + + for (s32 i = 0; i < count; i++) { + auto& mix_info{mix_infos[i]}; + sorted_mix_infos[i] = &mix_info; + + if (!mix_info.in_use) { + continue; + } + + auto mix_id{mix_info.mix_id}; + auto distance_to_final_mix{FinalMixId}; + + while (distance_to_final_mix < count) { + if (mix_id == FinalMixId) { + break; + } + + if (mix_id == UnusedMixId) { + distance_to_final_mix = InvalidDistanceFromFinalMix; + break; + } + + auto distance_from_final_mix{mix_infos[mix_id].distance_from_final_mix}; + if (distance_from_final_mix != InvalidDistanceFromFinalMix) { + distance_to_final_mix = distance_from_final_mix + 1; + break; + } + + distance_to_final_mix++; + mix_id = mix_infos[mix_id].dst_mix_id; + } + + if (distance_to_final_mix >= count) { + distance_to_final_mix = InvalidDistanceFromFinalMix; + } + mix_info.distance_from_final_mix = distance_to_final_mix; + } +} + +void MixContext::SortInfo() { + UpdateDistancesFromFinalMix(); + + std::ranges::sort(sorted_mix_infos, [](const MixInfo* lhs, const MixInfo* rhs) { + return lhs->distance_from_final_mix > rhs->distance_from_final_mix; + }); + + CalcMixBufferOffset(); +} + +void MixContext::CalcMixBufferOffset() { + s16 offset{0}; + for (s32 i = 0; i < count; i++) { + auto mix_info{sorted_mix_infos[i]}; + if (mix_info->in_use) { + const auto buffer_count{mix_info->buffer_count}; + mix_info->buffer_offset = offset; + offset += buffer_count; + } + } +} + +bool MixContext::TSortInfo(const SplitterContext& splitter_context) { + if (!splitter_context.UsingSplitter()) { + CalcMixBufferOffset(); + return true; + } + + if (!node_states.Tsort(edge_matrix)) { + return false; + } + + std::vector sorted_results{node_states.GetSortedResuls()}; + const auto result_size{std::min(count, static_cast(sorted_results.size()))}; + for (s32 i = 0; i < result_size; i++) { + sorted_mix_infos[i] = &mix_infos[sorted_results[i]]; + } + + CalcMixBufferOffset(); + return true; +} + +EdgeMatrix& MixContext::GetEdgeMatrix() { + return edge_matrix; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/mix/mix_context.h b/src/audio_core/renderer/mix/mix_context.h new file mode 100755 index 000000000..da3aa2829 --- /dev/null +++ b/src/audio_core/renderer/mix/mix_context.h @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/mix/mix_info.h" +#include "audio_core/renderer/nodes/edge_matrix.h" +#include "audio_core/renderer/nodes/node_states.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +class SplitterContext; + +/* + * Manages mixing states, sorting and building a node graph to describe a mix order. + */ +class MixContext { +public: + /** + * Initialize the mix context. + * + * @param sorted_mix_infos - Buffer for the sorted mix infos. + * @param mix_infos - Buffer for the mix infos. + * @param effect_process_order_buffer - Buffer for the effect process orders. + * @param effect_count - Number of effects in the buffer. + * @param node_states_workbuffer - Buffer for node states. + * @param node_buffer_size - Size of the node states buffer. + * @param edge_matrix_workbuffer - Buffer for edge matrix. + * @param edge_matrix_size - Size of the edge matrix buffer. + */ + void Initialize(std::span sorted_mix_infos, std::span mix_infos, u32 count_, + std::span effect_process_order_buffer, u32 effect_count, + std::span node_states_workbuffer, u64 node_buffer_size, + std::span edge_matrix_workbuffer, u64 edge_matrix_size); + + /** + * Get a sorted mix at the given index. + * + * @param index - Index of sorted mix. + * @return The sorted mix. + */ + MixInfo* GetSortedInfo(s32 index); + + /** + * Set the sorted info at the given index. + * + * @param index - Index of sorted mix. + * @param mix_info - The new mix for this index. + */ + void SetSortedInfo(s32 index, MixInfo& mix_info); + + /** + * Get a mix at the given index. + * + * @param index - Index of mix. + * @return The mix. + */ + MixInfo* GetInfo(s32 index); + + /** + * Get the final mix. + * + * @return The final mix. + */ + MixInfo* GetFinalMixInfo(); + + /** + * Get the current number of mixes. + * + * @return The number of active mixes. + */ + s32 GetCount() const; + + /** + * Update all of the mixes' distance from the final mix. + * Needs to be called after altering the mix graph. + */ + void UpdateDistancesFromFinalMix(); + + /** + * Non-splitter sort, sorts the sorted mixes based on their distance from the final mix. + */ + void SortInfo(); + + /** + * Re-calculate the mix buffer offsets for each mix after altering the mix. + */ + void CalcMixBufferOffset(); + + /** + * Splitter sort, traverse the splitter node graph and sort the sorted mixes from results. + * + * @param splitter_context - Splitter context for the sort. + * @return True if the sort was successful, othewise false. + */ + bool TSortInfo(const SplitterContext& splitter_context); + + /** + * Get the edge matrix used for the mix graph. + * + * @return The edge matrix used. + */ + EdgeMatrix& GetEdgeMatrix(); + +private: + /// Array of sorted mixes + std::span sorted_mix_infos{}; + /// Array of mixes + std::span mix_infos{}; + /// Number of active mixes + s32 count{}; + /// Array of effect process orderings + std::span effect_process_order_buffer{}; + /// Number of effects in the process ordering buffer + u64 effect_count{}; + /// Node states used in splitter sort + NodeStates node_states{}; + /// Edge matrix for connected nodes used in splitter sort + EdgeMatrix edge_matrix{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/mix/mix_info.cpp b/src/audio_core/renderer/mix/mix_info.cpp new file mode 100755 index 000000000..cc18e57ee --- /dev/null +++ b/src/audio_core/renderer/mix/mix_info.cpp @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/effect/effect_context.h" +#include "audio_core/renderer/mix/mix_info.h" +#include "audio_core/renderer/nodes/edge_matrix.h" +#include "audio_core/renderer/splitter/splitter_context.h" + +namespace AudioCore::AudioRenderer { + +MixInfo::MixInfo(std::span effect_order_buffer_, s32 effect_count_, BehaviorInfo& behavior) + : effect_order_buffer{effect_order_buffer_}, effect_count{effect_count_}, + long_size_pre_delay_supported{behavior.IsLongSizePreDelaySupported()} { + ClearEffectProcessingOrder(); +} + +void MixInfo::Cleanup() { + mix_id = UnusedMixId; + dst_mix_id = UnusedMixId; + dst_splitter_id = UnusedSplitterId; +} + +void MixInfo::ClearEffectProcessingOrder() { + for (s32 i = 0; i < effect_count; i++) { + effect_order_buffer[i] = -1; + } +} + +bool MixInfo::Update(EdgeMatrix& edge_matrix, const InParameter& in_params, + EffectContext& effect_context, SplitterContext& splitter_context, + const BehaviorInfo& behavior) { + volume = in_params.volume; + sample_rate = in_params.sample_rate; + buffer_count = static_cast(in_params.buffer_count); + in_use = in_params.in_use; + mix_id = in_params.mix_id; + node_id = in_params.node_id; + mix_volumes = in_params.mix_volumes; + + bool sort_required{false}; + if (behavior.IsSplitterSupported()) { + sort_required = UpdateConnection(edge_matrix, in_params, splitter_context); + } else { + if (dst_mix_id != in_params.dest_mix_id) { + dst_mix_id = in_params.dest_mix_id; + sort_required = true; + } + dst_splitter_id = UnusedSplitterId; + } + + ClearEffectProcessingOrder(); + + // Check all effects, and set their order if they belong to this mix. + const auto count{effect_context.GetCount()}; + for (u32 i = 0; i < count; i++) { + const auto& info{effect_context.GetInfo(i)}; + if (mix_id == info.GetMixId()) { + const auto processing_order{info.GetProcessingOrder()}; + if (processing_order > effect_count) { + break; + } + effect_order_buffer[processing_order] = i; + } + } + + return sort_required; +} + +bool MixInfo::UpdateConnection(EdgeMatrix& edge_matrix, const InParameter& in_params, + SplitterContext& splitter_context) { + auto has_new_connection{false}; + if (dst_splitter_id != UnusedSplitterId) { + auto& splitter_info{splitter_context.GetInfo(dst_splitter_id)}; + has_new_connection = splitter_info.HasNewConnection(); + } + + // Check if this mix matches the input parameters. + // If everything is the same, don't bother updating. + if (dst_mix_id == in_params.dest_mix_id && dst_splitter_id == in_params.dest_splitter_id && + !has_new_connection) { + return false; + } + + // Reset the mix in the graph, as we're about to update it. + edge_matrix.RemoveEdges(mix_id); + + if (in_params.dest_mix_id == UnusedMixId) { + if (in_params.dest_splitter_id != UnusedSplitterId) { + // If the splitter is used, connect this mix to each active destination. + auto& splitter_info{splitter_context.GetInfo(in_params.dest_splitter_id)}; + auto const destination_count{splitter_info.GetDestinationCount()}; + + for (u32 i = 0; i < destination_count; i++) { + auto destination{ + splitter_context.GetDesintationData(in_params.dest_splitter_id, i)}; + + if (destination) { + const auto destination_id{destination->GetMixId()}; + if (destination_id != UnusedMixId) { + edge_matrix.Connect(mix_id, destination_id); + } + } + } + } + } else { + // If the splitter is not used, only connect this mix to its destination. + edge_matrix.Connect(mix_id, in_params.dest_mix_id); + } + + dst_mix_id = in_params.dest_mix_id; + dst_splitter_id = in_params.dest_splitter_id; + return true; +} + +bool MixInfo::HasAnyConnection() const { + return dst_mix_id != UnusedMixId || dst_splitter_id != UnusedSplitterId; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/mix/mix_info.h b/src/audio_core/renderer/mix/mix_info.h new file mode 100755 index 000000000..b5fa4c0c7 --- /dev/null +++ b/src/audio_core/renderer/mix/mix_info.h @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/common/common.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +class EdgeMatrix; +class SplitterContext; +class EffectContext; +class BehaviorInfo; + +/** + * A single mix, which may feed through other mixes in a chain until reaching the final output mix. + */ +class MixInfo { +public: + struct InParameter { + /* 0x000 */ f32 volume; + /* 0x004 */ u32 sample_rate; + /* 0x008 */ u32 buffer_count; + /* 0x00C */ bool in_use; + /* 0x00D */ bool is_dirty; + /* 0x010 */ s32 mix_id; + /* 0x014 */ u32 effect_count; + /* 0x018 */ s32 node_id; + /* 0x01C */ char unk01C[0x8]; + /* 0x024 */ std::array, MaxMixBuffers> mix_volumes; + /* 0x924 */ s32 dest_mix_id; + /* 0x928 */ s32 dest_splitter_id; + /* 0x92C */ char unk92C[0x4]; + }; + static_assert(sizeof(InParameter) == 0x930, "MixInfo::InParameter has the wrong size!"); + + struct InDirtyParameter { + /* 0x00 */ u32 magic; + /* 0x04 */ s32 count; + /* 0x08 */ char unk08[0x18]; + }; + static_assert(sizeof(InDirtyParameter) == 0x20, + "MixInfo::InDirtyParameter has the wrong size!"); + + MixInfo(std::span effect_order_buffer, s32 effect_count, BehaviorInfo& behavior); + + /** + * Clean up the mix, resetting it to a default state. + */ + void Cleanup(); + + /** + * Clear the effect process order for all effects in this mix. + */ + void ClearEffectProcessingOrder(); + + /** + * Update the mix according to the given parameters. + * + * @param edge_matrix - Updated with new splitter node connections, if supported. + * @param in_params - Input parameters. + * @param effect_context - Used to update the effect orderings. + * @param splitter_context - Used to update the mix graph if supported. + * @param behavior - Used for checking which features are supported. + * @return True if the mix was updated and a sort is required, otherwise false. + */ + bool Update(EdgeMatrix& edge_matrix, const InParameter& in_params, + EffectContext& effect_context, SplitterContext& splitter_context, + const BehaviorInfo& behavior); + + /** + * Update the mix's connection in the node graph according to the given parameters. + * + * @param edge_matrix - Updated with new splitter node connections, if supported. + * @param in_params - Input parameters. + * @param splitter_context - Used to update the mix graph if supported. + * @return True if the mix was updated and a sort is required, otherwise false. + */ + bool UpdateConnection(EdgeMatrix& edge_matrix, const InParameter& in_params, + SplitterContext& splitter_context); + + /** + * Check if this mix is connected to any other. + * + * @return True if the mix has a connection, otherwise false. + */ + bool HasAnyConnection() const; + + /// Volume of this mix + f32 volume{}; + /// Sample rate of this mix + u32 sample_rate{}; + /// Number of buffers in this mix + s16 buffer_count{}; + /// Is this mix in use? + bool in_use{}; + /// Is this mix enabled? + bool enabled{}; + /// Id of this mix + s32 mix_id{UnusedMixId}; + /// Node id of this mix + s32 node_id{}; + /// Buffer offset for this mix + s16 buffer_offset{}; + /// Distance to the final mix + s32 distance_from_final_mix{InvalidDistanceFromFinalMix}; + /// Array of effect orderings of all effects in this mix + std::span effect_order_buffer; + /// Number of effects in this mix + const s32 effect_count; + /// Id for next mix in the chain + s32 dst_mix_id{UnusedMixId}; + /// Mixing volumes for this mix used when this mix is chained with another + std::array, MaxMixBuffers> mix_volumes{}; + /// Id for next mix in the graph when splitter is used + s32 dst_splitter_id{UnusedSplitterId}; + /// Is a longer pre-delay time supported for the reverb effect? + const bool long_size_pre_delay_supported; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/nodes/bit_array.h b/src/audio_core/renderer/nodes/bit_array.h new file mode 100755 index 000000000..b0d53cd51 --- /dev/null +++ b/src/audio_core/renderer/nodes/bit_array.h @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Represents an array of bits used for nodes and edges for the mixing graph. + */ +struct BitArray { + void reset() { + buffer.assign(buffer.size(), false); + } + + /// Bits + std::vector buffer{}; + /// Size of the buffer + u32 size{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/nodes/edge_matrix.cpp b/src/audio_core/renderer/nodes/edge_matrix.cpp new file mode 100755 index 000000000..5573f33b9 --- /dev/null +++ b/src/audio_core/renderer/nodes/edge_matrix.cpp @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/nodes/edge_matrix.h" + +namespace AudioCore::AudioRenderer { + +void EdgeMatrix::Initialize([[maybe_unused]] std::span buffer, + [[maybe_unused]] const u64 node_buffer_size, const u32 count_) { + count = count_; + edges.buffer.resize(count_ * count_); + edges.size = count_ * count_; + edges.reset(); +} + +bool EdgeMatrix::Connected(const u32 id, const u32 destination_id) const { + return edges.buffer[count * id + destination_id]; +} + +void EdgeMatrix::Connect(const u32 id, const u32 destination_id) { + edges.buffer[count * id + destination_id] = true; +} + +void EdgeMatrix::Disconnect(const u32 id, const u32 destination_id) { + edges.buffer[count * id + destination_id] = false; +} + +void EdgeMatrix::RemoveEdges(const u32 id) { + for (u32 dest = 0; dest < count; dest++) { + Disconnect(id, dest); + } +} + +u32 EdgeMatrix::GetNodeCount() const { + return count; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/nodes/edge_matrix.h b/src/audio_core/renderer/nodes/edge_matrix.h new file mode 100755 index 000000000..27a20e43e --- /dev/null +++ b/src/audio_core/renderer/nodes/edge_matrix.h @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/nodes/bit_array.h" +#include "common/alignment.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * An edge matrix, holding the connections for each node to every other node in the graph. + */ +class EdgeMatrix { +public: + /** + * Calculate the size required for its workbuffer. + * + * @param count - The number of nodes in the graph. + * @return The required workbuffer size. + */ + static u64 GetWorkBufferSize(u32 count) { + return Common::AlignUp(count * count, 0x40) / sizeof(u64); + } + + /** + * Initialize this edge matrix. + * + * @param buffer - The workbuffer to use. Unused. + * @param node_buffer_size - The size of the workbuffer. Unused. + * @param count - The number of nodes in the graph. + */ + void Initialize(std::span buffer, u64 node_buffer_size, u32 count); + + /** + * Check if a node is connected to another. + * + * @param id - The node id to check. + * @param destination_id - Node id to check connection with. + */ + bool Connected(u32 id, u32 destination_id) const; + + /** + * Connect a node to another. + * + * @param id - The node id to connect. + * @param destination_id - Destination to connect it to. + */ + void Connect(u32 id, u32 destination_id); + + /** + * Disconnect a node from another. + * + * @param id - The node id to disconnect. + * @param destination_id - Destination to disconnect it from. + */ + void Disconnect(u32 id, u32 destination_id); + + /** + * Remove all connections for a given node. + * + * @param id - The node id to disconnect. + */ + void RemoveEdges(u32 id); + + /** + * Get the number of nodes in the graph. + * + * @return Number of nodes. + */ + u32 GetNodeCount() const; + +private: + /// Edges for the current graph + BitArray edges; + /// Number of nodes (not edges) in the graph + u32 count; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/nodes/node_states.cpp b/src/audio_core/renderer/nodes/node_states.cpp new file mode 100755 index 000000000..1821a51e6 --- /dev/null +++ b/src/audio_core/renderer/nodes/node_states.cpp @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/nodes/node_states.h" +#include "common/logging/log.h" + +namespace AudioCore::AudioRenderer { + +void NodeStates::Initialize(std::span buffer_, [[maybe_unused]] const u64 node_buffer_size, + const u32 count) { + u64 num_blocks{Common::AlignUp(count, 0x40) / sizeof(u64)}; + u64 offset{0}; + + node_count = count; + + nodes_found.buffer.resize(count); + nodes_found.size = count; + nodes_found.reset(); + + offset += num_blocks; + + nodes_complete.buffer.resize(count); + nodes_complete.size = count; + nodes_complete.reset(); + + offset += num_blocks; + + results = {reinterpret_cast(&buffer_[offset]), count}; + + offset += count * sizeof(u32); + + stack.stack = {reinterpret_cast(&buffer_[offset]), count * count}; + stack.size = count * count; + stack.unk_10 = count * count; + + offset += count * count * sizeof(u32); +} + +bool NodeStates::Tsort(const EdgeMatrix& edge_matrix) { + return DepthFirstSearch(edge_matrix, stack); +} + +bool NodeStates::DepthFirstSearch(const EdgeMatrix& edge_matrix, Stack& stack_) { + ResetState(); + + for (u32 node_id = 0; node_id < node_count; node_id++) { + if (GetState(node_id) == SearchState::Unknown) { + stack_.push(node_id); + } + + while (stack_.Count() > 0) { + auto current_node{stack_.top()}; + switch (GetState(current_node)) { + case SearchState::Unknown: + SetState(current_node, SearchState::Found); + break; + case SearchState::Found: + SetState(current_node, SearchState::Complete); + PushTsortResult(current_node); + stack_.pop(); + continue; + case SearchState::Complete: + stack_.pop(); + continue; + } + + const auto edge_count{edge_matrix.GetNodeCount()}; + for (u32 edge_id = 0; edge_id < edge_count; edge_id++) { + if (!edge_matrix.Connected(current_node, edge_id)) { + continue; + } + + switch (GetState(edge_id)) { + case SearchState::Unknown: + stack_.push(edge_id); + break; + case SearchState::Found: + LOG_ERROR(Service_Audio, + "Cycle detected in the node graph, graph is not a DAG! " + "Bailing to avoid an infinite loop"); + ResetState(); + return false; + case SearchState::Complete: + break; + } + } + } + } + + return true; +} + +NodeStates::SearchState NodeStates::GetState(const u32 id) const { + if (nodes_found.buffer[id]) { + return SearchState::Found; + } else if (nodes_complete.buffer[id]) { + return SearchState::Complete; + } + return SearchState::Unknown; +} + +void NodeStates::PushTsortResult(const u32 id) { + results[result_pos++] = id; +} + +void NodeStates::SetState(const u32 id, const SearchState state) { + switch (state) { + case SearchState::Complete: + nodes_found.buffer[id] = false; + nodes_complete.buffer[id] = true; + break; + case SearchState::Found: + nodes_found.buffer[id] = true; + nodes_complete.buffer[id] = false; + break; + case SearchState::Unknown: + nodes_found.buffer[id] = false; + nodes_complete.buffer[id] = false; + break; + default: + LOG_ERROR(Service_Audio, "Unknown node SearchState {}", static_cast(state)); + break; + } +} + +void NodeStates::ResetState() { + nodes_found.reset(); + nodes_complete.reset(); + std::fill(results.begin(), results.end(), -1); + result_pos = 0; +} + +u32 NodeStates::GetNodeCount() const { + return node_count; +} + +std::vector NodeStates::GetSortedResuls() const { + return {results.rbegin(), results.rbegin() + result_pos}; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/nodes/node_states.h b/src/audio_core/renderer/nodes/node_states.h new file mode 100755 index 000000000..a1e0958a2 --- /dev/null +++ b/src/audio_core/renderer/nodes/node_states.h @@ -0,0 +1,195 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/nodes/edge_matrix.h" +#include "common/alignment.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Graph utility functions for sorting and getting results from the DAG. + */ +class NodeStates { + /** + * State of a node in the depth first search. + */ + enum class SearchState { + Unknown, + Found, + Complete, + }; + + /** + * Stack used for a depth first search. + */ + struct Stack { + /** + * Calculate the workbuffer size required for this stack. + * + * @param count - Maximum number of nodes for the stack. + * @return Required buffer size. + */ + static u32 CalcBufferSize(u32 count) { + return count * sizeof(u32); + } + + /** + * Reset the stack back to default. + * + * @param buffer_ - The new buffer to use. + * @param size_ - The size of the new buffer. + */ + void Reset(u32* buffer_, u32 size_) { + stack = {buffer_, size_}; + size = size_; + pos = 0; + unk_10 = size_; + } + + /** + * Get the current stack position. + * + * @return The current stack position. + */ + u32 Count() { + return pos; + } + + /** + * Push a new node to the stack. + * + * @param data - The node to push. + */ + void push(u32 data) { + stack[pos++] = data; + } + + /** + * Pop a node from the stack. + * + * @return The node on the top of the stack. + */ + u32 pop() { + return stack[--pos]; + } + + /** + * Get the top of the stack without popping. + * + * @return The node on the top of the stack. + */ + u32 top() { + return stack[pos - 1]; + } + + /// Buffer for the stack + std::span stack{}; + /// Size of the stack buffer + u32 size{}; + /// Current stack position + u32 pos{}; + /// Unknown + u32 unk_10{}; + }; + +public: + /** + * Calculate the workbuffer size required for the node states. + * + * @param count - The number of nodes. + * @return The required workbuffer size. + */ + static u64 GetWorkBufferSize(u32 count) { + return (Common::AlignUp(count, 0x40) / sizeof(u64)) * 2 + count * sizeof(BitArray) + + count * Stack::CalcBufferSize(count); + } + + /** + * Initialize the node states. + * + * @param buffer - The workbuffer to use. Unused. + * @param node_buffer_size - The size of the workbuffer. Unused. + * @param count - The number of nodes in the graph. + */ + void Initialize(std::span nodes, u64 node_buffer_size, u32 count); + + /** + * Sort the graph. Only calls DepthFirstSearch. + * + * @param edge_matrix - The edge matrix used to hold the connections between nodes. + * @return True if the sort was successful, otherwise false. + */ + bool Tsort(const EdgeMatrix& edge_matrix); + + /** + * Sort the graph via depth first search. + * + * @param edge_matrix - The edge matrix used to hold the connections between nodes. + * @param stack - The stack used for pushing and popping nodes. + * @return True if the sort was successful, otherwise false. + */ + bool DepthFirstSearch(const EdgeMatrix& edge_matrix, Stack& stack); + + /** + * Get the search state of a given node. + * + * @param id - The node id to check. + * @return The node's search state. See SearchState + */ + SearchState GetState(u32 id) const; + + /** + * Push a node id to the results buffer when found in the DFS. + * + * @param id - The node id to push. + */ + void PushTsortResult(u32 id); + + /** + * Set the state of a node. + * + * @param id - The node id to alter. + * @param state - The new search state. + */ + void SetState(u32 id, SearchState state); + + /** + * Reset the nodes found, complete and the results. + */ + void ResetState(); + + /** + * Get the number of nodes in the graph. + * + * @return The number of nodes. + */ + u32 GetNodeCount() const; + + /** + * Get the sorted results from the DFS. + * + * @return Vector of nodes in reverse order. + */ + std::vector GetSortedResuls() const; + +private: + /// Number of nodes in the graph + u32 node_count{}; + /// Position in results buffer + u32 result_pos{}; + /// List of nodes found + BitArray nodes_found{}; + /// List of nodes completed + BitArray nodes_complete{}; + /// List of results from the depth first search + std::span results{}; + /// Stack used during the depth first search + Stack stack{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/performance/detail_aspect.cpp b/src/audio_core/renderer/performance/detail_aspect.cpp new file mode 100755 index 000000000..f6405937f --- /dev/null +++ b/src/audio_core/renderer/performance/detail_aspect.cpp @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/command/command_buffer.h" +#include "audio_core/renderer/command/command_generator.h" +#include "audio_core/renderer/performance/detail_aspect.h" + +namespace AudioCore::AudioRenderer { + +DetailAspect::DetailAspect(CommandGenerator& command_generator_, + const PerformanceEntryType entry_type, const s32 node_id_, + const PerformanceDetailType detail_type) + : command_generator{command_generator_}, node_id{node_id_} { + auto perf_manager{command_generator.GetPerformanceManager()}; + if (perf_manager != nullptr && perf_manager->IsInitialized() && + perf_manager->IsDetailTarget(node_id) && + perf_manager->GetNextEntry(performance_entry_address, detail_type, entry_type, node_id)) { + command_generator.GeneratePerformanceCommand(node_id, PerformanceState::Start, + performance_entry_address); + + initialized = true; + } +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/performance/detail_aspect.h b/src/audio_core/renderer/performance/detail_aspect.h new file mode 100755 index 000000000..ee4ac2f76 --- /dev/null +++ b/src/audio_core/renderer/performance/detail_aspect.h @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/renderer/performance/performance_entry_addresses.h" +#include "audio_core/renderer/performance/performance_manager.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +class CommandGenerator; + +/** + * Holds detailed information about performance metrics, filled in by the AudioRenderer during + * Performance commands. + */ +class DetailAspect { +public: + DetailAspect() = default; + DetailAspect(CommandGenerator& command_generator, PerformanceEntryType entry_type, s32 node_id, + PerformanceDetailType detail_type); + + /// Command generator the command will be generated into + CommandGenerator& command_generator; + /// Addresses to be filled by the AudioRenderer + PerformanceEntryAddresses performance_entry_address{}; + /// Is this detail aspect initialized? + bool initialized{}; + /// Node id of this aspect + s32 node_id; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/performance/entry_aspect.cpp b/src/audio_core/renderer/performance/entry_aspect.cpp new file mode 100755 index 000000000..dd4165803 --- /dev/null +++ b/src/audio_core/renderer/performance/entry_aspect.cpp @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/command/command_buffer.h" +#include "audio_core/renderer/command/command_generator.h" +#include "audio_core/renderer/performance/entry_aspect.h" + +namespace AudioCore::AudioRenderer { + +EntryAspect::EntryAspect(CommandGenerator& command_generator_, const PerformanceEntryType type, + const s32 node_id_) + : command_generator{command_generator_}, node_id{node_id_} { + auto perf_manager{command_generator.GetPerformanceManager()}; + if (perf_manager != nullptr && perf_manager->IsInitialized() && + perf_manager->GetNextEntry(performance_entry_address, type, node_id)) { + command_generator.GeneratePerformanceCommand(node_id, PerformanceState::Start, + performance_entry_address); + + initialized = true; + } +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/performance/entry_aspect.h b/src/audio_core/renderer/performance/entry_aspect.h new file mode 100755 index 000000000..01c1eb3f1 --- /dev/null +++ b/src/audio_core/renderer/performance/entry_aspect.h @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/renderer/performance/performance_entry_addresses.h" +#include "audio_core/renderer/performance/performance_manager.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +class CommandGenerator; + +/** + * Holds entry information about performance metrics, filled in by the AudioRenderer during + * Performance commands. + */ +class EntryAspect { +public: + EntryAspect() = default; + EntryAspect(CommandGenerator& command_generator, PerformanceEntryType type, s32 node_id); + + /// Command generator the command will be generated into + CommandGenerator& command_generator; + /// Addresses to be filled by the AudioRenderer + PerformanceEntryAddresses performance_entry_address{}; + /// Is this detail aspect initialized? + bool initialized{}; + /// Node id of this aspect + s32 node_id; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/performance/performance_detail.h b/src/audio_core/renderer/performance/performance_detail.h new file mode 100755 index 000000000..d9069ff78 --- /dev/null +++ b/src/audio_core/renderer/performance/performance_detail.h @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/renderer/performance/performance_entry.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { + +enum class PerformanceDetailType : u8 { + /* 0 */ Invalid, + /* 1 */ Unk1, + /* 2 */ Unk2, + /* 3 */ Unk3, + /* 4 */ Unk4, + /* 5 */ Unk5, + /* 6 */ Unk6, + /* 7 */ Unk7, + /* 8 */ Unk8, + /* 9 */ Unk9, + /* 10 */ Unk10, + /* 11 */ Unk11, + /* 12 */ Unk12, +}; + +struct PerformanceDetailVersion1 { + /* 0x00 */ u32 node_id; + /* 0x04 */ u32 start_time; + /* 0x08 */ u32 processed_time; + /* 0x0C */ PerformanceDetailType detail_type; + /* 0x0D */ PerformanceEntryType entry_type; +}; +static_assert(sizeof(PerformanceDetailVersion1) == 0x10, + "PerformanceDetailVersion1 has the worng size!"); + +struct PerformanceDetailVersion2 { + /* 0x00 */ u32 node_id; + /* 0x04 */ u32 start_time; + /* 0x08 */ u32 processed_time; + /* 0x0C */ PerformanceDetailType detail_type; + /* 0x0D */ PerformanceEntryType entry_type; + /* 0x10 */ u32 unk_10; + /* 0x14 */ char unk14[0x4]; +}; +static_assert(sizeof(PerformanceDetailVersion2) == 0x18, + "PerformanceDetailVersion2 has the worng size!"); + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/performance/performance_entry.h b/src/audio_core/renderer/performance/performance_entry.h new file mode 100755 index 000000000..d1b21406b --- /dev/null +++ b/src/audio_core/renderer/performance/performance_entry.h @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { + +enum class PerformanceEntryType : u8 { + Invalid, + Voice, + SubMix, + FinalMix, + Sink, +}; + +struct PerformanceEntryVersion1 { + /* 0x00 */ u32 node_id; + /* 0x04 */ u32 start_time; + /* 0x08 */ u32 processed_time; + /* 0x0C */ PerformanceEntryType entry_type; +}; +static_assert(sizeof(PerformanceEntryVersion1) == 0x10, + "PerformanceEntryVersion1 has the worng size!"); + +struct PerformanceEntryVersion2 { + /* 0x00 */ u32 node_id; + /* 0x04 */ u32 start_time; + /* 0x08 */ u32 processed_time; + /* 0x0C */ PerformanceEntryType entry_type; + /* 0x0D */ char unk0D[0xB]; +}; +static_assert(sizeof(PerformanceEntryVersion2) == 0x18, + "PerformanceEntryVersion2 has the worng size!"); + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/performance/performance_entry_addresses.h b/src/audio_core/renderer/performance/performance_entry_addresses.h new file mode 100755 index 000000000..e381d765c --- /dev/null +++ b/src/audio_core/renderer/performance/performance_entry_addresses.h @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/common/common.h" + +namespace AudioCore::AudioRenderer { + +struct PerformanceEntryAddresses { + CpuAddr translated_address; + CpuAddr entry_start_time_offset; + CpuAddr header_entry_count_offset; + CpuAddr entry_processed_time_offset; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/performance/performance_frame_header.h b/src/audio_core/renderer/performance/performance_frame_header.h new file mode 100755 index 000000000..707cc0afb --- /dev/null +++ b/src/audio_core/renderer/performance/performance_frame_header.h @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { + +struct PerformanceFrameHeaderVersion1 { + /* 0x00 */ u32 magic; // "PERF" + /* 0x04 */ u32 entry_count; + /* 0x08 */ u32 detail_count; + /* 0x0C */ u32 next_offset; + /* 0x10 */ u32 total_processing_time; + /* 0x14 */ u32 frame_index; +}; +static_assert(sizeof(PerformanceFrameHeaderVersion1) == 0x18, + "PerformanceFrameHeaderVersion1 has the worng size!"); + +struct PerformanceFrameHeaderVersion2 { + /* 0x00 */ u32 magic; // "PERF" + /* 0x04 */ u32 entry_count; + /* 0x08 */ u32 detail_count; + /* 0x0C */ u32 next_offset; + /* 0x10 */ u32 total_processing_time; + /* 0x14 */ u32 voices_dropped; + /* 0x18 */ u64 start_time; + /* 0x20 */ u32 frame_index; + /* 0x24 */ bool render_time_exceeded; + /* 0x25 */ char unk25[0xB]; +}; +static_assert(sizeof(PerformanceFrameHeaderVersion2) == 0x30, + "PerformanceFrameHeaderVersion2 has the worng size!"); + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/performance/performance_manager.cpp b/src/audio_core/renderer/performance/performance_manager.cpp new file mode 100755 index 000000000..fd5873e1e --- /dev/null +++ b/src/audio_core/renderer/performance/performance_manager.cpp @@ -0,0 +1,645 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/memory/memory_pool_info.h" +#include "audio_core/renderer/performance/performance_manager.h" +#include "common/common_funcs.h" + +namespace AudioCore::AudioRenderer { + +void PerformanceManager::CreateImpl(const size_t version) { + switch (version) { + case 1: + impl = std::make_unique< + PerformanceManagerImpl>(); + break; + case 2: + impl = std::make_unique< + PerformanceManagerImpl>(); + break; + default: + LOG_WARNING(Service_Audio, "Invalid PerformanceMetricsDataFormat {}, creating version 1", + static_cast(version)); + impl = std::make_unique< + PerformanceManagerImpl>(); + } +} + +void PerformanceManager::Initialize(std::span workbuffer, const u64 workbuffer_size, + const AudioRendererParameterInternal& params, + const BehaviorInfo& behavior, + const MemoryPoolInfo& memory_pool) { + CreateImpl(behavior.GetPerformanceMetricsDataFormat()); + impl->Initialize(workbuffer, workbuffer_size, params, behavior, memory_pool); +} + +bool PerformanceManager::IsInitialized() const { + if (impl) { + return impl->IsInitialized(); + } + return false; +} + +u32 PerformanceManager::CopyHistories(u8* out_buffer, u64 out_size) { + if (impl) { + return impl->CopyHistories(out_buffer, out_size); + } + return 0; +} + +bool PerformanceManager::GetNextEntry(PerformanceEntryAddresses& addresses, u32** unk, + const PerformanceSysDetailType sys_detail_type, + const s32 node_id) { + if (impl) { + return impl->GetNextEntry(addresses, unk, sys_detail_type, node_id); + } + return false; +} + +bool PerformanceManager::GetNextEntry(PerformanceEntryAddresses& addresses, + const PerformanceEntryType entry_type, const s32 node_id) { + if (impl) { + return impl->GetNextEntry(addresses, entry_type, node_id); + } + return false; +} + +bool PerformanceManager::GetNextEntry(PerformanceEntryAddresses& addresses, + const PerformanceDetailType detail_type, + const PerformanceEntryType entry_type, const s32 node_id) { + if (impl) { + return impl->GetNextEntry(addresses, detail_type, entry_type, node_id); + } + return false; +} + +void PerformanceManager::TapFrame(const bool dsp_behind, const u32 voices_dropped, + const u64 rendering_start_tick) { + if (impl) { + impl->TapFrame(dsp_behind, voices_dropped, rendering_start_tick); + } +} + +bool PerformanceManager::IsDetailTarget(const u32 target_node_id) const { + if (impl) { + return impl->IsDetailTarget(target_node_id); + } + return false; +} + +void PerformanceManager::SetDetailTarget(const u32 target_node_id) { + if (impl) { + impl->SetDetailTarget(target_node_id); + } +} + +template <> +void PerformanceManagerImpl< + PerformanceVersion::Version1, PerformanceFrameHeaderVersion1, PerformanceEntryVersion1, + PerformanceDetailVersion1>::Initialize(std::span workbuffer_, const u64 workbuffer_size, + const AudioRendererParameterInternal& params, + const BehaviorInfo& behavior, + const MemoryPoolInfo& memory_pool) { + workbuffer = workbuffer_; + entries_per_frame = params.voices + params.effects + params.sinks + params.sub_mixes + 1; + max_detail_count = MaxDetailEntries; + frame_size = GetRequiredBufferSizeForPerformanceMetricsPerFrame(behavior, params); + const auto frame_count{static_cast(workbuffer_size / frame_size)}; + max_frames = frame_count - 1; + translated_buffer = memory_pool.Translate(CpuAddr(workbuffer.data()), workbuffer_size); + + // The first frame is the "current" frame we're writing to. + auto buffer_offset{workbuffer.data()}; + frame_header = reinterpret_cast(buffer_offset); + buffer_offset += sizeof(PerformanceFrameHeaderVersion1); + entry_buffer = {reinterpret_cast(buffer_offset), entries_per_frame}; + buffer_offset += entries_per_frame * sizeof(PerformanceEntryVersion1); + detail_buffer = {reinterpret_cast(buffer_offset), max_detail_count}; + + // After the current, is a ringbuffer of history frames, the current frame will be copied here + // before a new frame is written. + frame_history = std::span(workbuffer.data() + frame_size, workbuffer_size - frame_size); + + // If there's room for any history frames. + if (frame_count >= 2) { + buffer_offset = frame_history.data(); + frame_history_header = reinterpret_cast(buffer_offset); + buffer_offset += sizeof(PerformanceFrameHeaderVersion1); + frame_history_entries = {reinterpret_cast(buffer_offset), + entries_per_frame}; + buffer_offset += entries_per_frame * sizeof(PerformanceEntryVersion1); + frame_history_details = {reinterpret_cast(buffer_offset), + max_detail_count}; + } else { + frame_history_header = {}; + frame_history_entries = {}; + frame_history_details = {}; + } + + target_node_id = 0; + version = PerformanceVersion(behavior.GetPerformanceMetricsDataFormat()); + entry_count = 0; + detail_count = 0; + frame_header->entry_count = 0; + frame_header->detail_count = 0; + output_frame_index = 0; + last_output_frame_index = 0; + is_initialized = true; +} + +template <> +bool PerformanceManagerImpl::IsInitialized() + const { + return is_initialized; +} + +template <> +u32 PerformanceManagerImpl::CopyHistories(u8* out_buffer, u64 out_size) { + if (out_buffer == nullptr || out_size == 0 || !is_initialized) { + return 0; + } + + // Are there any new frames waiting to be output? + if (last_output_frame_index == output_frame_index) { + return 0; + } + + PerformanceFrameHeaderVersion1* out_header{nullptr}; + u32 out_history_size{0}; + + while (last_output_frame_index != output_frame_index) { + PerformanceFrameHeaderVersion1* history_header{nullptr}; + std::span history_entries{}; + std::span history_details{}; + + if (max_frames > 0) { + auto frame_offset{&frame_history[last_output_frame_index * frame_size]}; + history_header = reinterpret_cast(frame_offset); + frame_offset += sizeof(PerformanceFrameHeaderVersion1); + history_entries = {reinterpret_cast(frame_offset), + history_header->entry_count}; + frame_offset += entries_per_frame * sizeof(PerformanceFrameHeaderVersion1); + history_details = {reinterpret_cast(frame_offset), + history_header->detail_count}; + } else { + // Original code does not break here, but will crash when trying to dereference the + // header in the next if, so let's just skip this frame and continue... + // Hopefully this will not happen. + LOG_WARNING(Service_Audio, + "max_frames should not be 0! Skipping frame to avoid a crash"); + last_output_frame_index++; + continue; + } + + if (out_size < history_header->entry_count * sizeof(PerformanceEntryVersion1) + + history_header->detail_count * sizeof(PerformanceDetailVersion1) + + 2 * sizeof(PerformanceFrameHeaderVersion1)) { + break; + } + + u32 out_offset{sizeof(PerformanceFrameHeaderVersion1)}; + auto out_entries{std::span( + reinterpret_cast(out_buffer + out_offset), + history_header->entry_count)}; + u32 out_entry_count{0}; + u32 total_processing_time{0}; + for (auto& history_entry : history_entries) { + if (history_entry.processed_time > 0 || history_entry.start_time > 0) { + out_entries[out_entry_count++] = history_entry; + total_processing_time += history_entry.processed_time; + } + } + + out_offset += static_cast(out_entry_count * sizeof(PerformanceEntryVersion1)); + auto out_details{std::span( + reinterpret_cast(out_buffer + out_offset), + history_header->detail_count)}; + u32 out_detail_count{0}; + for (auto& history_detail : history_details) { + if (history_detail.processed_time > 0 || history_detail.start_time > 0) { + out_details[out_detail_count++] = history_detail; + } + } + + out_offset += static_cast(out_detail_count * sizeof(PerformanceDetailVersion1)); + out_header = reinterpret_cast(out_buffer); + out_header->magic = Common::MakeMagic('P', 'E', 'R', 'F'); + out_header->entry_count = out_entry_count; + out_header->detail_count = out_detail_count; + out_header->next_offset = out_offset; + out_header->total_processing_time = total_processing_time; + out_header->frame_index = history_header->frame_index; + + out_history_size += out_offset; + + out_buffer += out_offset; + out_size -= out_offset; + last_output_frame_index = (last_output_frame_index + 1) % max_frames; + } + + // We're out of frames to output, so if there's enough left in the output buffer for another + // header, and we output at least 1 frame, set the next header to null. + if (out_size > sizeof(PerformanceFrameHeaderVersion1) && out_header != nullptr) { + std::memset(out_buffer, 0, sizeof(PerformanceFrameHeaderVersion1)); + } + + return out_history_size; +} + +template <> +bool PerformanceManagerImpl:: + GetNextEntry([[maybe_unused]] PerformanceEntryAddresses& addresses, [[maybe_unused]] u32** unk, + [[maybe_unused]] PerformanceSysDetailType sys_detail_type, + [[maybe_unused]] s32 node_id) { + return false; +} + +template <> +bool PerformanceManagerImpl< + PerformanceVersion::Version1, PerformanceFrameHeaderVersion1, PerformanceEntryVersion1, + PerformanceDetailVersion1>::GetNextEntry(PerformanceEntryAddresses& addresses, + const PerformanceEntryType entry_type, + const s32 node_id) { + if (!is_initialized) { + return false; + } + + addresses.translated_address = translated_buffer; + addresses.header_entry_count_offset = CpuAddr(frame_header) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceFrameHeaderVersion1, entry_count); + + auto entry{&entry_buffer[entry_count++]}; + addresses.entry_start_time_offset = CpuAddr(entry) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceEntryVersion1, start_time); + addresses.entry_processed_time_offset = CpuAddr(entry) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceEntryVersion1, processed_time); + + std::memset(entry, 0, sizeof(PerformanceEntryVersion1)); + entry->node_id = node_id; + entry->entry_type = entry_type; + return true; +} + +template <> +bool PerformanceManagerImpl< + PerformanceVersion::Version1, PerformanceFrameHeaderVersion1, PerformanceEntryVersion1, + PerformanceDetailVersion1>::GetNextEntry(PerformanceEntryAddresses& addresses, + const PerformanceDetailType detail_type, + const PerformanceEntryType entry_type, + const s32 node_id) { + if (!is_initialized || detail_count > MaxDetailEntries) { + return false; + } + + auto detail{&detail_buffer[detail_count++]}; + + addresses.translated_address = translated_buffer; + addresses.header_entry_count_offset = CpuAddr(frame_header) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceFrameHeaderVersion1, detail_count); + addresses.entry_start_time_offset = CpuAddr(detail) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceDetailVersion1, start_time); + addresses.entry_processed_time_offset = CpuAddr(detail) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceDetailVersion1, processed_time); + + std::memset(detail, 0, sizeof(PerformanceDetailVersion1)); + detail->node_id = node_id; + detail->entry_type = entry_type; + detail->detail_type = detail_type; + return true; +} + +template <> +void PerformanceManagerImpl< + PerformanceVersion::Version1, PerformanceFrameHeaderVersion1, PerformanceEntryVersion1, + PerformanceDetailVersion1>::TapFrame([[maybe_unused]] bool dsp_behind, + [[maybe_unused]] u32 voices_dropped, + [[maybe_unused]] u64 rendering_start_tick) { + if (!is_initialized) { + return; + } + + if (max_frames > 0) { + if (!frame_history.empty() && !workbuffer.empty()) { + auto history_frame = reinterpret_cast( + &frame_history[output_frame_index * frame_size]); + std::memcpy(history_frame, workbuffer.data(), frame_size); + history_frame->frame_index = history_frame_index++; + } + output_frame_index = (output_frame_index + 1) % max_frames; + } + + entry_count = 0; + detail_count = 0; + frame_header->entry_count = 0; + frame_header->detail_count = 0; +} + +template <> +bool PerformanceManagerImpl< + PerformanceVersion::Version1, PerformanceFrameHeaderVersion1, PerformanceEntryVersion1, + PerformanceDetailVersion1>::IsDetailTarget(const u32 target_node_id_) const { + return target_node_id == target_node_id_; +} + +template <> +void PerformanceManagerImpl::SetDetailTarget(const u32 target_node_id_) { + target_node_id = target_node_id_; +} + +template <> +void PerformanceManagerImpl< + PerformanceVersion::Version2, PerformanceFrameHeaderVersion2, PerformanceEntryVersion2, + PerformanceDetailVersion2>::Initialize(std::span workbuffer_, const u64 workbuffer_size, + const AudioRendererParameterInternal& params, + const BehaviorInfo& behavior, + const MemoryPoolInfo& memory_pool) { + workbuffer = workbuffer_; + entries_per_frame = params.voices + params.effects + params.sinks + params.sub_mixes + 1; + max_detail_count = MaxDetailEntries; + frame_size = GetRequiredBufferSizeForPerformanceMetricsPerFrame(behavior, params); + const auto frame_count{static_cast(workbuffer_size / frame_size)}; + max_frames = frame_count - 1; + translated_buffer = memory_pool.Translate(CpuAddr(workbuffer.data()), workbuffer_size); + + // The first frame is the "current" frame we're writing to. + auto buffer_offset{workbuffer.data()}; + frame_header = reinterpret_cast(buffer_offset); + buffer_offset += sizeof(PerformanceFrameHeaderVersion2); + entry_buffer = {reinterpret_cast(buffer_offset), entries_per_frame}; + buffer_offset += entries_per_frame * sizeof(PerformanceEntryVersion2); + detail_buffer = {reinterpret_cast(buffer_offset), max_detail_count}; + + // After the current, is a ringbuffer of history frames, the current frame will be copied here + // before a new frame is written. + frame_history = std::span(workbuffer.data() + frame_size, workbuffer_size - frame_size); + + // If there's room for any history frames. + if (frame_count >= 2) { + buffer_offset = frame_history.data(); + frame_history_header = reinterpret_cast(buffer_offset); + buffer_offset += sizeof(PerformanceFrameHeaderVersion2); + frame_history_entries = {reinterpret_cast(buffer_offset), + entries_per_frame}; + buffer_offset += entries_per_frame * sizeof(PerformanceEntryVersion2); + frame_history_details = {reinterpret_cast(buffer_offset), + max_detail_count}; + } else { + frame_history_header = {}; + frame_history_entries = {}; + frame_history_details = {}; + } + + target_node_id = 0; + version = PerformanceVersion(behavior.GetPerformanceMetricsDataFormat()); + entry_count = 0; + detail_count = 0; + frame_header->entry_count = 0; + frame_header->detail_count = 0; + output_frame_index = 0; + last_output_frame_index = 0; + is_initialized = true; +} + +template <> +bool PerformanceManagerImpl::IsInitialized() + const { + return is_initialized; +} + +template <> +u32 PerformanceManagerImpl::CopyHistories(u8* out_buffer, u64 out_size) { + if (out_buffer == nullptr || out_size == 0 || !is_initialized) { + return 0; + } + + // Are there any new frames waiting to be output? + if (last_output_frame_index == output_frame_index) { + return 0; + } + + PerformanceFrameHeaderVersion2* out_header{nullptr}; + u32 out_history_size{0}; + + while (last_output_frame_index != output_frame_index) { + PerformanceFrameHeaderVersion2* history_header{nullptr}; + std::span history_entries{}; + std::span history_details{}; + + if (max_frames > 0) { + auto frame_offset{&frame_history[last_output_frame_index * frame_size]}; + history_header = reinterpret_cast(frame_offset); + frame_offset += sizeof(PerformanceFrameHeaderVersion2); + history_entries = {reinterpret_cast(frame_offset), + history_header->entry_count}; + frame_offset += entries_per_frame * sizeof(PerformanceFrameHeaderVersion2); + history_details = {reinterpret_cast(frame_offset), + history_header->detail_count}; + } else { + // Original code does not break here, but will crash when trying to dereference the + // header in the next if, so let's just skip this frame and continue... + // Hopefully this will not happen. + LOG_WARNING(Service_Audio, + "max_frames should not be 0! Skipping frame to avoid a crash"); + last_output_frame_index++; + continue; + } + + if (out_size < history_header->entry_count * sizeof(PerformanceEntryVersion2) + + history_header->detail_count * sizeof(PerformanceDetailVersion2) + + 2 * sizeof(PerformanceFrameHeaderVersion2)) { + break; + } + + u32 out_offset{sizeof(PerformanceFrameHeaderVersion2)}; + auto out_entries{std::span( + reinterpret_cast(out_buffer + out_offset), + history_header->entry_count)}; + u32 out_entry_count{0}; + u32 total_processing_time{0}; + for (auto& history_entry : history_entries) { + if (history_entry.processed_time > 0 || history_entry.start_time > 0) { + out_entries[out_entry_count++] = history_entry; + total_processing_time += history_entry.processed_time; + } + } + + out_offset += static_cast(out_entry_count * sizeof(PerformanceEntryVersion2)); + auto out_details{std::span( + reinterpret_cast(out_buffer + out_offset), + history_header->detail_count)}; + u32 out_detail_count{0}; + for (auto& history_detail : history_details) { + if (history_detail.processed_time > 0 || history_detail.start_time > 0) { + out_details[out_detail_count++] = history_detail; + } + } + + out_offset += static_cast(out_detail_count * sizeof(PerformanceDetailVersion2)); + out_header = reinterpret_cast(out_buffer); + out_header->magic = Common::MakeMagic('P', 'E', 'R', 'F'); + out_header->entry_count = out_entry_count; + out_header->detail_count = out_detail_count; + out_header->next_offset = out_offset; + out_header->total_processing_time = total_processing_time; + out_header->voices_dropped = history_header->voices_dropped; + out_header->start_time = history_header->start_time; + out_header->frame_index = history_header->frame_index; + out_header->render_time_exceeded = history_header->render_time_exceeded; + + out_history_size += out_offset; + + out_buffer += out_offset; + out_size -= out_offset; + last_output_frame_index = (last_output_frame_index + 1) % max_frames; + } + + // We're out of frames to output, so if there's enough left in the output buffer for another + // header, and we output at least 1 frame, set the next header to null. + if (out_size > sizeof(PerformanceFrameHeaderVersion2) && out_header != nullptr) { + std::memset(out_buffer, 0, sizeof(PerformanceFrameHeaderVersion2)); + } + + return out_history_size; +} + +template <> +bool PerformanceManagerImpl< + PerformanceVersion::Version2, PerformanceFrameHeaderVersion2, PerformanceEntryVersion2, + PerformanceDetailVersion2>::GetNextEntry(PerformanceEntryAddresses& addresses, u32** unk, + const PerformanceSysDetailType sys_detail_type, + const s32 node_id) { + if (!is_initialized || detail_count > MaxDetailEntries) { + return false; + } + + auto detail{&detail_buffer[detail_count++]}; + + addresses.translated_address = translated_buffer; + addresses.header_entry_count_offset = CpuAddr(frame_header) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceFrameHeaderVersion2, detail_count); + addresses.entry_start_time_offset = CpuAddr(detail) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceDetailVersion2, start_time); + addresses.entry_processed_time_offset = CpuAddr(detail) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceDetailVersion2, processed_time); + + std::memset(detail, 0, sizeof(PerformanceDetailVersion2)); + detail->node_id = node_id; + detail->detail_type = static_cast(sys_detail_type); + + if (unk) { + *unk = &detail->unk_10; + } + return true; +} + +template <> +bool PerformanceManagerImpl< + PerformanceVersion::Version2, PerformanceFrameHeaderVersion2, PerformanceEntryVersion2, + PerformanceDetailVersion2>::GetNextEntry(PerformanceEntryAddresses& addresses, + const PerformanceEntryType entry_type, + const s32 node_id) { + if (!is_initialized) { + return false; + } + + auto entry{&entry_buffer[entry_count++]}; + + addresses.translated_address = translated_buffer; + addresses.header_entry_count_offset = CpuAddr(frame_header) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceFrameHeaderVersion2, entry_count); + addresses.entry_start_time_offset = CpuAddr(entry) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceEntryVersion2, start_time); + addresses.entry_processed_time_offset = CpuAddr(entry) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceEntryVersion2, processed_time); + + std::memset(entry, 0, sizeof(PerformanceEntryVersion2)); + entry->node_id = node_id; + entry->entry_type = entry_type; + return true; +} + +template <> +bool PerformanceManagerImpl< + PerformanceVersion::Version2, PerformanceFrameHeaderVersion2, PerformanceEntryVersion2, + PerformanceDetailVersion2>::GetNextEntry(PerformanceEntryAddresses& addresses, + const PerformanceDetailType detail_type, + const PerformanceEntryType entry_type, + const s32 node_id) { + if (!is_initialized || detail_count > MaxDetailEntries) { + return false; + } + + auto detail{&detail_buffer[detail_count++]}; + + addresses.translated_address = translated_buffer; + addresses.header_entry_count_offset = CpuAddr(frame_header) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceFrameHeaderVersion2, detail_count); + addresses.entry_start_time_offset = CpuAddr(detail) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceDetailVersion2, start_time); + addresses.entry_processed_time_offset = CpuAddr(detail) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceDetailVersion2, processed_time); + + std::memset(detail, 0, sizeof(PerformanceDetailVersion2)); + detail->node_id = node_id; + detail->entry_type = entry_type; + detail->detail_type = detail_type; + return true; +} + +template <> +void PerformanceManagerImpl::TapFrame(const bool dsp_behind, + const u32 voices_dropped, + const u64 rendering_start_tick) { + if (!is_initialized) { + return; + } + + if (max_frames > 0) { + if (!frame_history.empty() && !workbuffer.empty()) { + auto history_frame{reinterpret_cast( + &frame_history[output_frame_index * frame_size])}; + std::memcpy(history_frame, workbuffer.data(), frame_size); + history_frame->render_time_exceeded = dsp_behind; + history_frame->voices_dropped = voices_dropped; + history_frame->start_time = rendering_start_tick; + history_frame->frame_index = history_frame_index++; + } + output_frame_index = (output_frame_index + 1) % max_frames; + } + + entry_count = 0; + detail_count = 0; + frame_header->entry_count = 0; + frame_header->detail_count = 0; +} + +template <> +bool PerformanceManagerImpl< + PerformanceVersion::Version2, PerformanceFrameHeaderVersion2, PerformanceEntryVersion2, + PerformanceDetailVersion2>::IsDetailTarget(const u32 target_node_id_) const { + return target_node_id == target_node_id_; +} + +template <> +void PerformanceManagerImpl::SetDetailTarget(const u32 target_node_id_) { + target_node_id = target_node_id_; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/performance/performance_manager.h b/src/audio_core/renderer/performance/performance_manager.h new file mode 100755 index 000000000..b82176bef --- /dev/null +++ b/src/audio_core/renderer/performance/performance_manager.h @@ -0,0 +1,273 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "audio_core/common/audio_renderer_parameter.h" +#include "audio_core/renderer/performance/performance_detail.h" +#include "audio_core/renderer/performance/performance_entry.h" +#include "audio_core/renderer/performance/performance_entry_addresses.h" +#include "audio_core/renderer/performance/performance_frame_header.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +class BehaviorInfo; +class MemoryPoolInfo; + +enum class PerformanceVersion { + Version1, + Version2, +}; + +enum class PerformanceSysDetailType { + PcmInt16 = 15, + PcmFloat = 16, + Adpcm = 17, + LightLimiter = 37, +}; + +enum class PerformanceState { + Invalid, + Start, + Stop, +}; + +/** + * Manages performance information. + * + * The performance buffer is split into frames, each comprised of: + * Frame header - Information about the number of entries/details and some others + * Entries - Created when starting to generate types of commands, such as voice + * commands, mix commands, sink commands etc. Details - Created for specific commands + * within each group. Up to MaxDetailEntries per frame. + * + * A current frame is written to by the AudioRenderer, and before it processes the next command + * list, the current frame is copied to a ringbuffer of history frames. These frames are then + * output back to the game if it supplies a performance buffer to RequestUpdate. + * + * Two versions currently exist, version 2 adds a few extra fields to the header, and a new + * SysDetail type which is seemingly unused. + */ +class PerformanceManager { +public: + static constexpr size_t MaxDetailEntries = 100; + + struct InParameter { + /* 0x00 */ s32 target_node_id; + /* 0x04 */ char unk04[0xC]; + }; + static_assert(sizeof(InParameter) == 0x10, + "PerformanceManager::InParameter has the wrong size!"); + + struct OutStatus { + /* 0x00 */ s32 history_size; + /* 0x04 */ char unk04[0xC]; + }; + static_assert(sizeof(OutStatus) == 0x10, "PerformanceManager::OutStatus has the wrong size!"); + + /** + * Calculate the required size for the performance workbuffer. + * + * @param behavior - Check which version is supported. + * @param params - Input parameters. + * @return Required workbuffer size. + */ + static u64 GetRequiredBufferSizeForPerformanceMetricsPerFrame( + const BehaviorInfo& behavior, const AudioRendererParameterInternal& params) { + u64 entry_count{params.voices + params.effects + params.sub_mixes + params.sinks + 1}; + switch (behavior.GetPerformanceMetricsDataFormat()) { + case 1: + return sizeof(PerformanceFrameHeaderVersion1) + + PerformanceManager::MaxDetailEntries * sizeof(PerformanceDetailVersion1) + + entry_count * sizeof(PerformanceEntryVersion1); + case 2: + return sizeof(PerformanceFrameHeaderVersion2) + + PerformanceManager::MaxDetailEntries * sizeof(PerformanceDetailVersion2) + + entry_count * sizeof(PerformanceEntryVersion2); + } + + LOG_WARNING(Service_Audio, "Invalid PerformanceMetrics version, assuming version 1"); + return sizeof(PerformanceFrameHeaderVersion1) + + PerformanceManager::MaxDetailEntries * sizeof(PerformanceDetailVersion1) + + entry_count * sizeof(PerformanceEntryVersion1); + } + + virtual ~PerformanceManager() = default; + + /** + * Initialize the performance manager. + * + * @param workbuffer - Workbuffer to use for performance frames. + * @param workbuffer_size - Size of the workbuffer. + * @param params - Input parameters. + * @param behavior - Behaviour to check version and data format. + * @param memory_pool - Used to translate the workbuffer address for the DSP. + */ + virtual void Initialize(std::span workbuffer, u64 workbuffer_size, + const AudioRendererParameterInternal& params, + const BehaviorInfo& behavior, const MemoryPoolInfo& memory_pool); + + /** + * Check if the manager is initialized. + * + * @return True if initialized, otherwise false. + */ + virtual bool IsInitialized() const; + + /** + * Copy the waiting performance frames to the output buffer. + * + * @param out_buffer - Output buffer to store performance frames. + * @param out_size - Size of the output buffer. + * @return Size in bytes that were written to the buffer. + */ + virtual u32 CopyHistories(u8* out_buffer, u64 out_size); + + /** + * Setup a new sys detail in the current frame, filling in addresses with offsets to the + * current workbuffer, to be written by the AudioRenderer. Note: This version is + * unused/incomplete. + * + * @param addresses - Filled with pointers to the new entry, which should be passed to + * the AudioRenderer with Performance commands to be written. + * @param unk - Unknown. + * @param sys_detail_type - Sys detail type. + * @param node_id - Node id for this entry. + * @return True if a new entry was created and the offsets are valid, otherwise false. + */ + virtual bool GetNextEntry(PerformanceEntryAddresses& addresses, u32** unk, + PerformanceSysDetailType sys_detail_type, s32 node_id); + + /** + * Setup a new entry in the current frame, filling in addresses with offsets to the current + * workbuffer, to be written by the AudioRenderer. + * + * @param addresses - Filled with pointers to the new entry, which should be passed to + * the AudioRenderer with Performance commands to be written. + * @param entry_type - The type of this entry. See PerformanceEntryType + * @param node_id - Node id for this entry. + * @return True if a new entry was created and the offsets are valid, otherwise false. + */ + virtual bool GetNextEntry(PerformanceEntryAddresses& addresses, PerformanceEntryType entry_type, + s32 node_id); + + /** + * Setup a new detail in the current frame, filling in addresses with offsets to the current + * workbuffer, to be written by the AudioRenderer. + * + * @param addresses - Filled with pointers to the new detail, which should be passed + * to the AudioRenderer with Performance commands to be written. + * @param entry_type - The type of this detail. See PerformanceEntryType + * @param node_id - Node id for this detail. + * @return True if a new detail was created and the offsets are valid, otherwise false. + */ + virtual bool GetNextEntry(PerformanceEntryAddresses& addresses, + PerformanceDetailType detail_type, PerformanceEntryType entry_type, + s32 node_id); + + /** + * Save the current frame to the ring buffer. + * + * @param dsp_behind - Did the AudioRenderer fall behind and not + * finish processing the command list? + * @param voices_dropped - The number of voices that were dropped. + * @param rendering_start_tick - The tick rendering started. + */ + virtual void TapFrame(bool dsp_behind, u32 voices_dropped, u64 rendering_start_tick); + + /** + * Check if the node id is a detail type. + * + * @return True if the node is a detail type, otherwise false. + */ + virtual bool IsDetailTarget(u32 target_node_id) const; + + /** + * Set the given node to be a detail type. + * + * @param target_node_id - Node to set. + */ + virtual void SetDetailTarget(u32 target_node_id); + +private: + /** + * Create the performance manager. + * + * @param version - Performance version to create. + */ + void CreateImpl(size_t version); + + std::unique_ptr + /// Impl for the performance manager, may be version 1 or 2. + impl; +}; + +template +class PerformanceManagerImpl : public PerformanceManager { +public: + void Initialize(std::span workbuffer, u64 workbuffer_size, + const AudioRendererParameterInternal& params, const BehaviorInfo& behavior, + const MemoryPoolInfo& memory_pool) override; + bool IsInitialized() const override; + u32 CopyHistories(u8* out_buffer, u64 out_size) override; + bool GetNextEntry(PerformanceEntryAddresses& addresses, u32** unk, + PerformanceSysDetailType sys_detail_type, s32 node_id) override; + bool GetNextEntry(PerformanceEntryAddresses& addresses, PerformanceEntryType entry_type, + s32 node_id) override; + bool GetNextEntry(PerformanceEntryAddresses& addresses, PerformanceDetailType detail_type, + PerformanceEntryType entry_type, s32 node_id) override; + void TapFrame(bool dsp_behind, u32 voices_dropped, u64 rendering_start_tick) override; + bool IsDetailTarget(u32 target_node_id) const override; + void SetDetailTarget(u32 target_node_id) override; + +private: + /// Workbuffer used to store the current performance frame + std::span workbuffer{}; + /// DSP address of the workbuffer, used by the AudioRenderer + CpuAddr translated_buffer{}; + /// Current frame index + u32 history_frame_index{}; + /// Current frame header + FrameHeaderVersion* frame_header{}; + /// Current frame entry buffer + std::span entry_buffer{}; + /// Current frame detail buffer + std::span detail_buffer{}; + /// Current frame entry count + u32 entry_count{}; + /// Current frame detail count + u32 detail_count{}; + /// Ringbuffer of previous frames + std::span frame_history{}; + /// Current history frame header + FrameHeaderVersion* frame_history_header{}; + /// Current history entry buffer + std::span frame_history_entries{}; + /// Current history detail buffer + std::span frame_history_details{}; + /// Current history ringbuffer write index + u32 output_frame_index{}; + /// Last history frame index that was written back to the game + u32 last_output_frame_index{}; + /// Maximum number of history frames in the ringbuffer + u32 max_frames{}; + /// Number of entries per frame + u32 entries_per_frame{}; + /// Maximum number of details per frame + u32 max_detail_count{}; + /// Frame size in bytes + u64 frame_size{}; + /// Is the performance manager initialized? + bool is_initialized{}; + /// Target node id + u32 target_node_id{}; + /// Performance version in use + PerformanceVersion version{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/sink/circular_buffer_sink_info.cpp b/src/audio_core/renderer/sink/circular_buffer_sink_info.cpp new file mode 100755 index 000000000..d91f10402 --- /dev/null +++ b/src/audio_core/renderer/sink/circular_buffer_sink_info.cpp @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/memory/pool_mapper.h" +#include "audio_core/renderer/sink/circular_buffer_sink_info.h" +#include "audio_core/renderer/upsampler/upsampler_manager.h" + +namespace AudioCore::AudioRenderer { + +CircularBufferSinkInfo::CircularBufferSinkInfo() { + state.fill(0); + parameter.fill(0); + type = Type::CircularBufferSink; + + auto state_{reinterpret_cast(state.data())}; + state_->address_info.Setup(0, 0); +} + +void CircularBufferSinkInfo::CleanUp() { + auto state_{reinterpret_cast(state.data())}; + + if (state_->upsampler_info) { + state_->upsampler_info->manager->Free(state_->upsampler_info); + state_->upsampler_info = nullptr; + } + + parameter.fill(0); + type = Type::Invalid; +} + +void CircularBufferSinkInfo::Update(BehaviorInfo::ErrorInfo& error_info, OutStatus& out_status, + const InParameter& in_params, const PoolMapper& pool_mapper) { + const auto buffer_params{ + reinterpret_cast(&in_params.circular_buffer)}; + auto current_params{reinterpret_cast(parameter.data())}; + auto current_state{reinterpret_cast(state.data())}; + + if (in_use == buffer_params->in_use && !buffer_unmapped) { + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + out_status.writeOffset = current_state->last_pos2; + return; + } + + node_id = in_params.node_id; + in_use = in_params.in_use; + + if (in_use) { + buffer_unmapped = + !pool_mapper.TryAttachBuffer(error_info, current_state->address_info, + buffer_params->cpu_address, buffer_params->size); + *current_params = *buffer_params; + } else { + *current_params = *buffer_params; + } + out_status.writeOffset = current_state->last_pos2; +} + +void CircularBufferSinkInfo::UpdateForCommandGeneration() { + if (in_use) { + auto params{reinterpret_cast(parameter.data())}; + auto state_{reinterpret_cast(state.data())}; + + const auto pos{state_->current_pos}; + state_->last_pos2 = state_->last_pos; + state_->last_pos = pos; + + state_->current_pos += static_cast(params->input_count * params->sample_count * + GetSampleFormatByteSize(SampleFormat::PcmInt16)); + if (params->size > 0) { + state_->current_pos %= params->size; + } + } +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/sink/circular_buffer_sink_info.h b/src/audio_core/renderer/sink/circular_buffer_sink_info.h new file mode 100755 index 000000000..3356213ea --- /dev/null +++ b/src/audio_core/renderer/sink/circular_buffer_sink_info.h @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/renderer/sink/sink_info_base.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Info for a circular buffer sink. + */ +class CircularBufferSinkInfo : public SinkInfoBase { +public: + CircularBufferSinkInfo(); + + /** + * Clean up for info, resetting it to a default state. + */ + void CleanUp() override; + + /** + * Update the info according to parameters, and write the current state to out_status. + * + * @param error_info - Output error code. + * @param out_status - Output status. + * @param in_params - Input parameters. + * @param pool_mapper - Used to map the circular buffer. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, OutStatus& out_status, + const InParameter& in_params, const PoolMapper& pool_mapper) override; + + /** + * Update the circular buffer on command generation, incrementing its current offsets. + */ + void UpdateForCommandGeneration() override; +}; +static_assert(sizeof(CircularBufferSinkInfo) <= sizeof(SinkInfoBase), + "CircularBufferSinkInfo is too large!"); + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/sink/device_sink_info.cpp b/src/audio_core/renderer/sink/device_sink_info.cpp new file mode 100755 index 000000000..b7b3d6f1d --- /dev/null +++ b/src/audio_core/renderer/sink/device_sink_info.cpp @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/sink/device_sink_info.h" +#include "audio_core/renderer/upsampler/upsampler_manager.h" + +namespace AudioCore::AudioRenderer { + +DeviceSinkInfo::DeviceSinkInfo() { + state.fill(0); + parameter.fill(0); + type = Type::DeviceSink; +} + +void DeviceSinkInfo::CleanUp() { + auto state_{reinterpret_cast(state.data())}; + + if (state_->upsampler_info) { + state_->upsampler_info->manager->Free(state_->upsampler_info); + state_->upsampler_info = nullptr; + } + + parameter.fill(0); + type = Type::Invalid; +} + +void DeviceSinkInfo::Update(BehaviorInfo::ErrorInfo& error_info, OutStatus& out_status, + const InParameter& in_params, + [[maybe_unused]] const PoolMapper& pool_mapper) { + + const auto device_params{reinterpret_cast(&in_params.device)}; + auto current_params{reinterpret_cast(parameter.data())}; + + if (in_use == in_params.in_use) { + current_params->downmix_enabled = device_params->downmix_enabled; + current_params->downmix_coeff = device_params->downmix_coeff; + } else { + type = in_params.type; + in_use = in_params.in_use; + node_id = in_params.node_id; + *current_params = *device_params; + } + + auto current_state{reinterpret_cast(state.data())}; + + for (size_t i = 0; i < current_state->downmix_coeff.size(); i++) { + current_state->downmix_coeff[i] = current_params->downmix_coeff[i]; + } + + std::memset(&out_status, 0, sizeof(OutStatus)); + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void DeviceSinkInfo::UpdateForCommandGeneration() {} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/sink/device_sink_info.h b/src/audio_core/renderer/sink/device_sink_info.h new file mode 100755 index 000000000..a1c441454 --- /dev/null +++ b/src/audio_core/renderer/sink/device_sink_info.h @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/renderer/sink/sink_info_base.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Info for a device sink. + */ +class DeviceSinkInfo : public SinkInfoBase { +public: + DeviceSinkInfo(); + + /** + * Clean up for info, resetting it to a default state. + */ + void CleanUp() override; + + /** + * Update the info according to parameters, and write the current state to out_status. + * + * @param error_info - Output error code. + * @param out_status - Output status. + * @param in_params - Input parameters. + * @param pool_mapper - Unused. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, OutStatus& out_status, + const InParameter& in_params, const PoolMapper& pool_mapper) override; + + /** + * Update the device sink on command generation, unused. + */ + void UpdateForCommandGeneration() override; +}; +static_assert(sizeof(DeviceSinkInfo) <= sizeof(SinkInfoBase), "DeviceSinkInfo is too large!"); + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/sink/sink_context.cpp b/src/audio_core/renderer/sink/sink_context.cpp new file mode 100755 index 000000000..634bc1cf9 --- /dev/null +++ b/src/audio_core/renderer/sink/sink_context.cpp @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/sink/sink_context.h" + +namespace AudioCore::AudioRenderer { + +void SinkContext::Initialize(std::span sink_infos_, const u32 sink_count_) { + sink_infos = sink_infos_; + sink_count = sink_count_; +} + +SinkInfoBase* SinkContext::GetInfo(const u32 index) { + return &sink_infos[index]; +} + +u32 SinkContext::GetCount() const { + return sink_count; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/sink/sink_context.h b/src/audio_core/renderer/sink/sink_context.h new file mode 100755 index 000000000..185572e29 --- /dev/null +++ b/src/audio_core/renderer/sink/sink_context.h @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/sink/sink_info_base.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Manages output sinks. + */ +class SinkContext { +public: + /** + * Initialize the sink context. + * + * @param sink_infos - Workbuffer for the sinks. + * @param sink_count - Number of sinks in the buffer. + */ + void Initialize(std::span sink_infos, u32 sink_count); + + /** + * Get a given index's info. + * + * @param index - Sink index to get. + * @return The sink info base for the given index. + */ + SinkInfoBase* GetInfo(u32 index); + + /** + * Get the current number of sinks. + * + * @return The number of sinks. + */ + u32 GetCount() const; + +private: + /// Buffer of sink infos + std::span sink_infos{}; + /// Number of sinks in the buffer + u32 sink_count{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/sink/sink_info_base.cpp b/src/audio_core/renderer/sink/sink_info_base.cpp new file mode 100755 index 000000000..4279beaa0 --- /dev/null +++ b/src/audio_core/renderer/sink/sink_info_base.cpp @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/memory/pool_mapper.h" +#include "audio_core/renderer/sink/sink_info_base.h" + +namespace AudioCore::AudioRenderer { + +void SinkInfoBase::CleanUp() { + type = Type::Invalid; +} + +void SinkInfoBase::Update(BehaviorInfo::ErrorInfo& error_info, OutStatus& out_status, + [[maybe_unused]] const InParameter& in_params, + [[maybe_unused]] const PoolMapper& pool_mapper) { + std::memset(&out_status, 0, sizeof(OutStatus)); + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void SinkInfoBase::UpdateForCommandGeneration() {} + +SinkInfoBase::DeviceState* SinkInfoBase::GetDeviceState() { + return reinterpret_cast(state.data()); +} + +SinkInfoBase::Type SinkInfoBase::GetType() const { + return type; +} + +bool SinkInfoBase::IsUsed() const { + return in_use; +} + +bool SinkInfoBase::ShouldSkip() const { + return buffer_unmapped; +} + +u32 SinkInfoBase::GetNodeId() const { + return node_id; +} + +u8* SinkInfoBase::GetState() { + return state.data(); +} + +u8* SinkInfoBase::GetParameter() { + return parameter.data(); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/sink/sink_info_base.h b/src/audio_core/renderer/sink/sink_info_base.h new file mode 100755 index 000000000..a1b855f20 --- /dev/null +++ b/src/audio_core/renderer/sink/sink_info_base.h @@ -0,0 +1,177 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/memory/address_info.h" +#include "common/common_types.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { +struct UpsamplerInfo; +class PoolMapper; + +/** + * Base for the circular buffer and device sinks, holding their states for the AudioRenderer and + * their parametetrs for generating sink commands. + */ +class SinkInfoBase { +public: + enum class Type : u8 { + Invalid, + DeviceSink, + CircularBufferSink, + }; + + struct DeviceInParameter { + /* 0x000 */ char name[0x100]; + /* 0x100 */ u32 input_count; + /* 0x104 */ std::array inputs; + /* 0x10A */ char unk10A[0x1]; + /* 0x10B */ bool downmix_enabled; + /* 0x10C */ std::array downmix_coeff; + }; + static_assert(sizeof(DeviceInParameter) == 0x11C, "DeviceInParameter has the wrong size!"); + + struct DeviceState { + /* 0x00 */ UpsamplerInfo* upsampler_info; + /* 0x08 */ std::array, 4> downmix_coeff; + /* 0x18 */ char unk18[0x18]; + }; + static_assert(sizeof(DeviceState) == 0x30, "DeviceState has the wrong size!"); + + struct CircularBufferInParameter { + /* 0x00 */ u64 cpu_address; + /* 0x08 */ u32 size; + /* 0x0C */ u32 input_count; + /* 0x10 */ u32 sample_count; + /* 0x14 */ u32 previous_pos; + /* 0x18 */ SampleFormat format; + /* 0x1C */ std::array inputs; + /* 0x22 */ bool in_use; + /* 0x23 */ char unk23[0x5]; + }; + static_assert(sizeof(CircularBufferInParameter) == 0x28, + "CircularBufferInParameter has the wrong size!"); + + struct CircularBufferState { + /* 0x00 */ u32 last_pos2; + /* 0x04 */ s32 current_pos; + /* 0x08 */ u32 last_pos; + /* 0x0C */ char unk0C[0x4]; + /* 0x10 */ AddressInfo address_info; + }; + static_assert(sizeof(CircularBufferState) == 0x30, "CircularBufferState has the wrong size!"); + + struct InParameter { + /* 0x000 */ Type type; + /* 0x001 */ bool in_use; + /* 0x004 */ u32 node_id; + /* 0x008 */ char unk08[0x18]; + union { + /* 0x020 */ DeviceInParameter device; + /* 0x020 */ CircularBufferInParameter circular_buffer; + }; + }; + static_assert(sizeof(InParameter) == 0x140, "SinkInfoBase::InParameter has the wrong size!"); + + struct OutStatus { + /* 0x00 */ u32 writeOffset; + /* 0x04 */ char unk04[0x1C]; + }; // size == 0x20 + static_assert(sizeof(OutStatus) == 0x20, "SinkInfoBase::OutStatus has the wrong size!"); + + virtual ~SinkInfoBase() = default; + + /** + * Clean up for info, resetting it to a default state. + */ + virtual void CleanUp(); + + /** + * Update the info according to parameters, and write the current state to out_status. + * + * @param error_info - Output error code. + * @param out_status - Output status. + * @param in_params - Input parameters. + * @param pool_mapper - Used to map the circular buffer. + */ + virtual void Update(BehaviorInfo::ErrorInfo& error_info, OutStatus& out_status, + [[maybe_unused]] const InParameter& in_params, + [[maybe_unused]] const PoolMapper& pool_mapper); + + /** + * Update the circular buffer on command generation, incrementing its current offsets. + */ + virtual void UpdateForCommandGeneration(); + + /** + * Get the state as a device sink. + * + * @return Device state. + */ + DeviceState* GetDeviceState(); + + /** + * Get the type of this sink. + * + * @return Either Device, Circular, or Invalid. + */ + Type GetType() const; + + /** + * Check if this sink is in use. + * + * @return True if used, otherwise false. + */ + bool IsUsed() const; + + /** + * Check if this sink should be skipped for updates. + * + * @return True if it should be skipped, otherwise false. + */ + bool ShouldSkip() const; + + /** + * Get the node if of this sink. + * + * @return Node id for this sink. + */ + u32 GetNodeId() const; + + /** + * Get the state of this sink. + * + * @return Pointer to the state, must be cast to the correct type. + */ + u8* GetState(); + + /** + * Get the parameters of this sink. + * + * @return Pointer to the parameters, must be cast to the correct type. + */ + u8* GetParameter(); + +protected: + /// Type of this sink + Type type{Type::Invalid}; + /// Is this sink in use? + bool in_use{}; + /// Is this sink's buffer unmapped? Circular only + bool buffer_unmapped{}; + /// Node id for this sink + u32 node_id{}; + /// State buffer for this sink + std::array state{}; + /// Parameter buffer for this sink + std::array + parameter{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/splitter/splitter_context.cpp b/src/audio_core/renderer/splitter/splitter_context.cpp new file mode 100755 index 000000000..7a23ba43f --- /dev/null +++ b/src/audio_core/renderer/splitter/splitter_context.cpp @@ -0,0 +1,217 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/common/audio_renderer_parameter.h" +#include "audio_core/common/workbuffer_allocator.h" +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/splitter/splitter_context.h" +#include "common/alignment.h" + +namespace AudioCore::AudioRenderer { + +SplitterDestinationData* SplitterContext::GetDesintationData(const s32 splitter_id, + const s32 destination_id) { + return splitter_infos[splitter_id].GetData(destination_id); +} + +SplitterInfo& SplitterContext::GetInfo(const s32 splitter_id) { + return splitter_infos[splitter_id]; +} + +u32 SplitterContext::GetDataCount() const { + return destinations_count; +} + +u32 SplitterContext::GetInfoCount() const { + return info_count; +} + +SplitterDestinationData& SplitterContext::GetData(const u32 index) { + return splitter_destinations[index]; +} + +void SplitterContext::Setup(std::span splitter_infos_, const u32 splitter_info_count_, + SplitterDestinationData* splitter_destinations_, + const u32 destination_count_, const bool splitter_bug_fixed_) { + splitter_infos = splitter_infos_; + info_count = splitter_info_count_; + splitter_destinations = splitter_destinations_; + destinations_count = destination_count_; + splitter_bug_fixed = splitter_bug_fixed_; +} + +bool SplitterContext::UsingSplitter() const { + return splitter_infos.size() > 0 && info_count > 0 && splitter_destinations != nullptr && + destinations_count > 0; +} + +void SplitterContext::ClearAllNewConnectionFlag() { + for (s32 i = 0; i < info_count; i++) { + splitter_infos[i].SetNewConnectionFlag(); + } +} + +bool SplitterContext::Initialize(const BehaviorInfo& behavior, + const AudioRendererParameterInternal& params, + WorkbufferAllocator& allocator) { + if (behavior.IsSplitterSupported() && params.splitter_infos > 0 && + params.splitter_destinations > 0) { + splitter_infos = allocator.Allocate(params.splitter_infos, 0x10); + + for (u32 i = 0; i < params.splitter_infos; i++) { + std::construct_at(&splitter_infos[i], static_cast(i)); + } + + if (splitter_infos.size() == 0) { + splitter_infos = {}; + return false; + } + + splitter_destinations = + allocator.Allocate(params.splitter_destinations, 0x10).data(); + + for (s32 i = 0; i < params.splitter_destinations; i++) { + std::construct_at(&splitter_destinations[i], i); + } + + if (params.splitter_destinations <= 0) { + splitter_infos = {}; + splitter_destinations = nullptr; + return false; + } + + Setup(splitter_infos, params.splitter_infos, splitter_destinations, + params.splitter_destinations, behavior.IsSplitterBugFixed()); + } + return true; +} + +bool SplitterContext::Update(const u8* input, u32& consumed_size) { + auto in_params{reinterpret_cast(input)}; + + if (destinations_count == 0 || info_count == 0) { + consumed_size = 0; + return true; + } + + if (in_params->magic != GetSplitterInParamHeaderMagic()) { + consumed_size = 0; + return false; + } + + for (auto& splitter_info : splitter_infos) { + splitter_info.ClearNewConnectionFlag(); + } + + u32 offset{sizeof(InParameterHeader)}; + offset = UpdateInfo(input, offset, in_params->info_count); + offset = UpdateData(input, offset, in_params->destination_count); + + consumed_size = Common::AlignUp(offset, 0x10); + return true; +} + +u32 SplitterContext::UpdateInfo(const u8* input, u32 offset, const u32 splitter_count) { + for (u32 i = 0; i < splitter_count; i++) { + auto info_header{reinterpret_cast(input + offset)}; + + if (info_header->magic != GetSplitterInfoMagic()) { + continue; + } + + if (info_header->id < 0 || info_header->id > info_count) { + break; + } + + auto& info{splitter_infos[info_header->id]}; + RecomposeDestination(info, info_header); + + offset += info.Update(info_header); + } + + return offset; +} + +u32 SplitterContext::UpdateData(const u8* input, u32 offset, const u32 count) { + for (u32 i = 0; i < count; i++) { + auto data_header{ + reinterpret_cast(input + offset)}; + + if (data_header->magic != GetSplitterSendDataMagic()) { + continue; + } + + if (data_header->id < 0 || data_header->id > destinations_count) { + continue; + } + + splitter_destinations[data_header->id].Update(*data_header); + offset += sizeof(SplitterDestinationData::InParameter); + } + + return offset; +} + +void SplitterContext::UpdateInternalState() { + for (s32 i = 0; i < info_count; i++) { + splitter_infos[i].UpdateInternalState(); + } +} + +void SplitterContext::RecomposeDestination(SplitterInfo& out_info, + const SplitterInfo::InParameter* info_header) { + auto destination{out_info.GetData(0)}; + while (destination != nullptr) { + auto dest{destination->GetNext()}; + destination->SetNext(nullptr); + destination = dest; + } + out_info.SetDestinations(nullptr); + + auto dest_count{info_header->destination_count}; + if (!splitter_bug_fixed) { + dest_count = std::min(dest_count, GetDestCountPerInfoForCompat()); + } + + if (dest_count == 0) { + return; + } + + std::span destination_ids{reinterpret_cast(&info_header[1]), dest_count}; + + auto head{&splitter_destinations[destination_ids[0]]}; + auto current_destination{head}; + for (u32 i = 1; i < dest_count; i++) { + auto next_destination{&splitter_destinations[destination_ids[i]]}; + current_destination->SetNext(next_destination); + current_destination = next_destination; + } + + out_info.SetDestinations(head); + out_info.SetDestinationCount(dest_count); +} + +u32 SplitterContext::GetDestCountPerInfoForCompat() const { + if (info_count <= 0) { + return 0; + } + return static_cast(destinations_count / info_count); +} + +u64 SplitterContext::CalcWorkBufferSize(const BehaviorInfo& behavior, + const AudioRendererParameterInternal& params) { + u64 size{0}; + if (!behavior.IsSplitterSupported()) { + return size; + } + + size += params.splitter_destinations * sizeof(SplitterDestinationData) + + params.splitter_infos * sizeof(SplitterInfo); + + if (behavior.IsSplitterBugFixed()) { + size += Common::AlignUp(params.splitter_destinations * sizeof(u32), 0x10); + } + return size; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/splitter/splitter_context.h b/src/audio_core/renderer/splitter/splitter_context.h new file mode 100755 index 000000000..cfd092b4f --- /dev/null +++ b/src/audio_core/renderer/splitter/splitter_context.h @@ -0,0 +1,189 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/splitter/splitter_destinations_data.h" +#include "audio_core/renderer/splitter/splitter_info.h" +#include "common/common_types.h" + +namespace AudioCore { +struct AudioRendererParameterInternal; +class WorkbufferAllocator; + +namespace AudioRenderer { +class BehaviorInfo; + +/** + * The splitter allows much more control over how sound is mixed together. + * Previously, one mix can only connect to one other, and you may need + * more mixes (and duplicate processing) to achieve the same result. + * With the splitter, many-to-one and one-to-many mixing is possible. + * This was added in revision 2. + * Had a bug with incorrect numbers of destinations, fixed in revision 5. + */ +class SplitterContext { + struct InParameterHeader { + /* 0x00 */ u32 magic; // 'SNDH' + /* 0x04 */ s32 info_count; + /* 0x08 */ s32 destination_count; + /* 0x0C */ char unk0C[0x14]; + }; + static_assert(sizeof(InParameterHeader) == 0x20, + "SplitterContext::InParameterHeader has the wrong size!"); + +public: + /** + * Get a destination mix from the given splitter and destination index. + * + * @param splitter_id - Splitter index to get from. + * @param destination_id - Destination index within the splitter. + * @return Pointer to the found destination. May be nullptr. + */ + SplitterDestinationData* GetDesintationData(s32 splitter_id, s32 destination_id); + + /** + * Get a splitter from the given index. + * + * @param index - Index of the desired splitter. + * @return Splitter requested. + */ + SplitterInfo& GetInfo(s32 index); + + /** + * Get the total number of splitter destinations. + * + * @return Number of destiantions. + */ + u32 GetDataCount() const; + + /** + * Get the total number of splitters. + * + * @return Number of splitters. + */ + u32 GetInfoCount() const; + + /** + * Get a specific global destination. + * + * @param index - Index of the desired destination. + * @return The requested destination. + */ + SplitterDestinationData& GetData(u32 index); + + /** + * Check if the splitter is in use. + * + * @return True if any splitter or destination is in use, otherwise false. + */ + bool UsingSplitter() const; + + /** + * Mark all splitters as having new connections. + */ + void ClearAllNewConnectionFlag(); + + /** + * Initialize the context. + * + * @param behavior - Used to check for splitter support. + * @param params - Input parameters. + * @param allocator - Allocator used to allocate workbuffer memory. + */ + bool Initialize(const BehaviorInfo& behavior, const AudioRendererParameterInternal& params, + WorkbufferAllocator& allocator); + + /** + * Update the context. + * + * @param input - Input buffer with the new info, + * expected to point to a InParameterHeader. + * @param consumed_size - Output with the number of bytes consumed from input. + */ + bool Update(const u8* input, u32& consumed_size); + + /** + * Update the splitters. + * + * @param input - Input buffer with the new info. + * @param offset - Current offset within the input buffer, + * input + offset should point to a SplitterInfo::InParameter. + * @param splitter_count - Number of splitters in the input buffer. + * @return Number of bytes consumed in input. + */ + u32 UpdateInfo(const u8* input, u32 offset, u32 splitter_count); + + /** + * Update the splitters. + * + * @param input - Input buffer with the new info. + * @param offset - Current offset within the input buffer, + * input + offset should point to a + * SplitterDestinationData::InParameter. + * @param destination_count - Number of destinations in the input buffer. + * @return Number of bytes consumed in input. + */ + u32 UpdateData(const u8* input, u32 offset, u32 destination_count); + + /** + * Update the state of all destinations in all splitters. + */ + void UpdateInternalState(); + + /** + * Replace the given splitter's destinations with new ones. + * + * @param out_info - Splitter to recompose. + * @param info_header - Input parameters containing new destination ids. + */ + void RecomposeDestination(SplitterInfo& out_info, const SplitterInfo::InParameter* info_header); + + /** + * Old calculation for destinations, this is the thing the splitter bug fixes. + * Left for compatibility, and now min'd with the actual count to not bug. + * + * @return Number of splitter destinations. + */ + u32 GetDestCountPerInfoForCompat() const; + + /** + * Calculate the size of the required workbuffer for splitters and destinations. + * + * @param behavior - Used to check splitter features. + * @param params - Input parameters with splitter/destination counts. + * @return Required buffer size. + */ + static u64 CalcWorkBufferSize(const BehaviorInfo& behavior, + const AudioRendererParameterInternal& params); + +private: + /** + * Setup the context. + * + * @param splitter_infos - Workbuffer for splitters. + * @param splitter_info_count - Number of splitters in the workbuffer. + * @param splitter_destinations - Workbuffer for splitter destinations. + * @param destination_count - Number of destinations in the workbuffer. + * @param splitter_bug_fixed - Is the splitter bug fixed? + */ + void Setup(std::span splitter_infos, u32 splitter_info_count, + SplitterDestinationData* splitter_destinations, u32 destination_count, + bool splitter_bug_fixed); + + /// Workbuffer for splitters + std::span splitter_infos{}; + /// Number of splitters in buffer + s32 info_count{}; + /// Workbuffer for destinations + SplitterDestinationData* splitter_destinations{}; + /// Number of destinations in buffer + s32 destinations_count{}; + /// Is the splitter bug fixed? + bool splitter_bug_fixed{}; +}; + +} // namespace AudioRenderer +} // namespace AudioCore diff --git a/src/audio_core/renderer/splitter/splitter_destinations_data.cpp b/src/audio_core/renderer/splitter/splitter_destinations_data.cpp new file mode 100755 index 000000000..b27d44896 --- /dev/null +++ b/src/audio_core/renderer/splitter/splitter_destinations_data.cpp @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/splitter/splitter_destinations_data.h" + +namespace AudioCore::AudioRenderer { + +SplitterDestinationData::SplitterDestinationData(const s32 id_) : id{id_} {} + +void SplitterDestinationData::ClearMixVolume() { + mix_volumes.fill(0.0f); + prev_mix_volumes.fill(0.0f); +} + +s32 SplitterDestinationData::GetId() const { + return id; +} + +bool SplitterDestinationData::IsConfigured() const { + return in_use && destination_id != UnusedMixId; +} + +s32 SplitterDestinationData::GetMixId() const { + return destination_id; +} + +f32 SplitterDestinationData::GetMixVolume(const u32 index) const { + if (index >= mix_volumes.size()) { + LOG_ERROR(Service_Audio, "SplitterDestinationData::GetMixVolume Invalid index {}", index); + return 0.0f; + } + return mix_volumes[index]; +} + +std::span SplitterDestinationData::GetMixVolume() { + return mix_volumes; +} + +f32 SplitterDestinationData::GetMixVolumePrev(const u32 index) const { + if (index >= prev_mix_volumes.size()) { + LOG_ERROR(Service_Audio, "SplitterDestinationData::GetMixVolumePrev Invalid index {}", + index); + return 0.0f; + } + return prev_mix_volumes[index]; +} + +std::span SplitterDestinationData::GetMixVolumePrev() { + return prev_mix_volumes; +} + +void SplitterDestinationData::Update(const InParameter& params) { + if (params.id != id || params.magic != GetSplitterSendDataMagic()) { + return; + } + + destination_id = params.mix_id; + mix_volumes = params.mix_volumes; + + if (!in_use && params.in_use) { + prev_mix_volumes = mix_volumes; + need_update = false; + } + + in_use = params.in_use; +} + +void SplitterDestinationData::MarkAsNeedToUpdateInternalState() { + need_update = true; +} + +void SplitterDestinationData::UpdateInternalState() { + if (in_use && need_update) { + prev_mix_volumes = mix_volumes; + } + need_update = false; +} + +SplitterDestinationData* SplitterDestinationData::GetNext() const { + return next; +} + +void SplitterDestinationData::SetNext(SplitterDestinationData* next_) { + next = next_; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/splitter/splitter_destinations_data.h b/src/audio_core/renderer/splitter/splitter_destinations_data.h new file mode 100755 index 000000000..bd3d55748 --- /dev/null +++ b/src/audio_core/renderer/splitter/splitter_destinations_data.h @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/common/common.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Represents a mixing node, can be connected to a previous and next destination forming a chain + * that a certain mix buffer will pass through to output. + */ +class SplitterDestinationData { +public: + struct InParameter { + /* 0x00 */ u32 magic; // 'SNDD' + /* 0x04 */ s32 id; + /* 0x08 */ std::array mix_volumes; + /* 0x68 */ u32 mix_id; + /* 0x6C */ bool in_use; + }; + static_assert(sizeof(InParameter) == 0x70, + "SplitterDestinationData::InParameter has the wrong size!"); + + SplitterDestinationData(s32 id); + + /** + * Reset the mix volumes for this destination. + */ + void ClearMixVolume(); + + /** + * Get the id of this destination. + * + * @return Id for this destination. + */ + s32 GetId() const; + + /** + * Check if this destination is correctly configured. + * + * @return True if configured, otherwise false. + */ + bool IsConfigured() const; + + /** + * Get the mix id for this destination. + * + * @return Mix id for this destination. + */ + s32 GetMixId() const; + + /** + * Get the current mix volume of a given index in this destination. + * + * @param index - Mix buffer index to get the volume for. + * @return Current volume of the specified mix. + */ + f32 GetMixVolume(u32 index) const; + + /** + * Get the current mix volumes for all mix buffers in this destination. + * + * @return Span of current mix buffer volumes. + */ + std::span GetMixVolume(); + + /** + * Get the previous mix volume of a given index in this destination. + * + * @param index - Mix buffer index to get the volume for. + * @return Previous volume of the specified mix. + */ + f32 GetMixVolumePrev(u32 index) const; + + /** + * Get the previous mix volumes for all mix buffers in this destination. + * + * @return Span of previous mix buffer volumes. + */ + std::span GetMixVolumePrev(); + + /** + * Update this destination. + * + * @param params - Inpout parameters to update the destination. + */ + void Update(const InParameter& params); + + /** + * Mark this destination as needing its volumes updated. + */ + void MarkAsNeedToUpdateInternalState(); + + /** + * Copy current volumes to previous if an update is required. + */ + void UpdateInternalState(); + + /** + * Get the next destination in the mix chain. + * + * @return The next splitter destination, may be nullptr if this is the last in the chain. + */ + SplitterDestinationData* GetNext() const; + + /** + * Set the next destination in the mix chain. + * + * @param next - Destination this one is to be connected to. + */ + void SetNext(SplitterDestinationData* next); + +private: + /// Id of this destination + const s32 id; + /// Mix id this destination represents + s32 destination_id{UnusedMixId}; + /// Current mix volumes + std::array mix_volumes{0.0f}; + /// Previous mix volumes + std::array prev_mix_volumes{0.0f}; + /// Next destination in the mix chain + SplitterDestinationData* next{}; + /// Is this destiantion in use? + bool in_use{}; + /// Does this destiantion need its volumes updated? + bool need_update{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/splitter/splitter_info.cpp b/src/audio_core/renderer/splitter/splitter_info.cpp new file mode 100755 index 000000000..1aee6720b --- /dev/null +++ b/src/audio_core/renderer/splitter/splitter_info.cpp @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/splitter/splitter_info.h" + +namespace AudioCore::AudioRenderer { + +SplitterInfo::SplitterInfo(const s32 id_) : id{id_} {} + +void SplitterInfo::InitializeInfos(SplitterInfo* splitters, const u32 count) { + if (splitters == nullptr) { + return; + } + + for (u32 i = 0; i < count; i++) { + auto& splitter{splitters[i]}; + splitter.destinations = nullptr; + splitter.destination_count = 0; + splitter.has_new_connection = true; + } +} + +u32 SplitterInfo::Update(const InParameter* params) { + if (params->id != id) { + return 0; + } + sample_rate = params->sample_rate; + has_new_connection = true; + return static_cast((sizeof(InParameter) + 3 * sizeof(s32)) + + params->destination_count * sizeof(s32)); +} + +SplitterDestinationData* SplitterInfo::GetData(const u32 destination_id) { + auto out_destination{destinations}; + u32 i{0}; + while (i < destination_id) { + if (out_destination == nullptr) { + break; + } + out_destination = out_destination->GetNext(); + i++; + } + + return out_destination; +} + +u32 SplitterInfo::GetDestinationCount() const { + return destination_count; +} + +void SplitterInfo::SetDestinationCount(const u32 count) { + destination_count = count; +} + +bool SplitterInfo::HasNewConnection() const { + return has_new_connection; +} + +void SplitterInfo::ClearNewConnectionFlag() { + has_new_connection = false; +} + +void SplitterInfo::SetNewConnectionFlag() { + has_new_connection = true; +} + +void SplitterInfo::UpdateInternalState() { + auto destination{destinations}; + while (destination != nullptr) { + destination->UpdateInternalState(); + destination = destination->GetNext(); + } +} + +void SplitterInfo::SetDestinations(SplitterDestinationData* destinations_) { + destinations = destinations_; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/splitter/splitter_info.h b/src/audio_core/renderer/splitter/splitter_info.h new file mode 100755 index 000000000..d1d75064c --- /dev/null +++ b/src/audio_core/renderer/splitter/splitter_info.h @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/renderer/splitter/splitter_destinations_data.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Represents a splitter, wraps multiple output destinations to split an input mix into. + */ +class SplitterInfo { +public: + struct InParameter { + /* 0x00 */ u32 magic; // 'SNDI' + /* 0x04 */ s32 id; + /* 0x08 */ u32 sample_rate; + /* 0x0C */ u32 destination_count; + }; + static_assert(sizeof(InParameter) == 0x10, "SplitterInfo::InParameter has the wrong size!"); + + explicit SplitterInfo(s32 id); + + /** + * Initialize the given splitters. + * + * @param splitters - Splitters to initialize. + * @param count - Number of splitters given. + */ + static void InitializeInfos(SplitterInfo* splitters, u32 count); + + /** + * Update this splitter. + * + * @param params - Input parameters to update with. + * @return The size in bytes of this splitter. + */ + u32 Update(const InParameter* params); + + /** + * Get a destination in this splitter. + * + * @param id - Destination id to get. + * @return Pointer to the destination, may be nullptr. + */ + SplitterDestinationData* GetData(u32 id); + + /** + * Get the number of destinations in this splitter. + * + * @return The number of destiantions. + */ + u32 GetDestinationCount() const; + + /** + * Set the number of destinations in this splitter. + * + * @param count - The new number of destiantions. + */ + void SetDestinationCount(u32 count); + + /** + * Check if the splitter has a new connection. + * + * @return True if there is a new connection, otherwise false. + */ + bool HasNewConnection() const; + + /** + * Reset the new connection flag. + */ + void ClearNewConnectionFlag(); + + /** + * Mark as having a new connection. + */ + void SetNewConnectionFlag(); + + /** + * Update the state of all destinations. + */ + void UpdateInternalState(); + + /** + * Set this splitter's destinations. + * + * @param destinations - The new destination list for this splitter. + */ + void SetDestinations(SplitterDestinationData* destinations); + +private: + /// Id of this splitter + s32 id; + /// Sample rate of this splitter + u32 sample_rate{}; + /// Number of destinations in this splitter + u32 destination_count{}; + /// Does this splitter have a new connection? + bool has_new_connection{true}; + /// Pointer to the destinations of this splitter + SplitterDestinationData* destinations{}; + /// Number of channels this splitter manages + u32 channel_count{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/system.cpp b/src/audio_core/renderer/system.cpp new file mode 100755 index 000000000..5a4938270 --- /dev/null +++ b/src/audio_core/renderer/system.cpp @@ -0,0 +1,797 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "audio_core/audio_core.h" +#include "audio_core/common/audio_renderer_parameter.h" +#include "audio_core/common/common.h" +#include "audio_core/common/feature_support.h" +#include "audio_core/common/workbuffer_allocator.h" +#include "audio_core/renderer/adsp/adsp.h" +#include "audio_core/renderer/behavior/info_updater.h" +#include "audio_core/renderer/command/command_buffer.h" +#include "audio_core/renderer/command/command_generator.h" +#include "audio_core/renderer/command/command_list_header.h" +#include "audio_core/renderer/effect/effect_info_base.h" +#include "audio_core/renderer/effect/effect_result_state.h" +#include "audio_core/renderer/memory/memory_pool_info.h" +#include "audio_core/renderer/memory/pool_mapper.h" +#include "audio_core/renderer/mix/mix_info.h" +#include "audio_core/renderer/nodes/edge_matrix.h" +#include "audio_core/renderer/nodes/node_states.h" +#include "audio_core/renderer/sink/sink_info_base.h" +#include "audio_core/renderer/system.h" +#include "audio_core/renderer/upsampler/upsampler_info.h" +#include "audio_core/renderer/voice/voice_channel_resource.h" +#include "audio_core/renderer/voice/voice_info.h" +#include "audio_core/renderer/voice/voice_state.h" +#include "common/alignment.h" +#include "core/core.h" +#include "core/core_timing.h" +#include "core/hle/kernel/k_event.h" +#include "core/hle/kernel/k_transfer_memory.h" +#include "core/memory.h" + +namespace AudioCore::AudioRenderer { + +u64 System::GetWorkBufferSize(const AudioRendererParameterInternal& params) { + BehaviorInfo behavior; + behavior.SetUserLibRevision(params.revision); + + u64 size{0}; + + size += Common::AlignUp(params.mixes * sizeof(s32), 0x40); + size += params.sub_mixes * MaxEffects * sizeof(s32); + size += (params.sub_mixes + 1) * sizeof(MixInfo); + size += params.voices * (sizeof(VoiceInfo) + sizeof(VoiceChannelResource) + sizeof(VoiceState)); + size += Common::AlignUp((params.sub_mixes + 1) * sizeof(MixInfo*), 0x10); + size += Common::AlignUp(params.voices * sizeof(VoiceInfo*), 0x10); + size += Common::AlignUp(((params.sinks + params.sub_mixes) * TargetSampleCount * sizeof(s32) + + params.sample_count * sizeof(s32)) * + (params.mixes + MaxChannels), + 0x40); + + if (behavior.IsSplitterSupported()) { + const auto node_size{NodeStates::GetWorkBufferSize(params.sub_mixes + 1)}; + const auto edge_size{EdgeMatrix::GetWorkBufferSize(params.sub_mixes + 1)}; + size += Common::AlignUp(node_size + edge_size, 0x10); + } + + size += SplitterContext::CalcWorkBufferSize(behavior, params); + size += (params.effects + params.voices * MaxWaveBuffers) * sizeof(MemoryPoolInfo); + + if (behavior.IsEffectInfoVersion2Supported()) { + size += params.effects * sizeof(EffectResultState); + } + size += 0x50; + + size = Common::AlignUp(size, 0x40); + + size += (params.sinks + params.sub_mixes) * sizeof(UpsamplerInfo); + size += params.effects * sizeof(EffectInfoBase); + size += Common::AlignUp(params.voices * sizeof(VoiceState), 0x40); + size += params.sinks * sizeof(SinkInfoBase); + + if (behavior.IsEffectInfoVersion2Supported()) { + size += params.effects * sizeof(EffectResultState); + } + + if (params.perf_frames > 0) { + auto perf_size{PerformanceManager::GetRequiredBufferSizeForPerformanceMetricsPerFrame( + behavior, params)}; + size += Common::AlignUp(perf_size * (params.perf_frames + 1) + 0xC0, 0x100); + } + + if (behavior.IsVariadicCommandBufferSizeSupported()) { + size += CommandGenerator::CalculateCommandBufferSize(behavior, params) + (0x40 - 1) * 2; + } else { + size += 0x18000 + (0x40 - 1) * 2; + } + + size = Common::AlignUp(size, 0x1000); + return size; +} + +System::System(Core::System& core_, Kernel::KEvent* adsp_rendered_event_) + : core{core_}, adsp{core.AudioCore().GetADSP()}, adsp_rendered_event{adsp_rendered_event_} {} + +Result System::Initialize(const AudioRendererParameterInternal& params, + Kernel::KTransferMemory* transfer_memory, const u64 transfer_memory_size, + const u32 process_handle_, const u64 applet_resource_user_id_, + const s32 session_id_) { + if (!CheckValidRevision(params.revision)) { + return Service::Audio::ERR_INVALID_REVISION; + } + + if (GetWorkBufferSize(params) > transfer_memory_size) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + if (process_handle_ == 0) { + return Service::Audio::ERR_INVALID_PROCESS_HANDLE; + } + + behavior.SetUserLibRevision(params.revision); + + process_handle = process_handle_; + applet_resource_user_id = applet_resource_user_id_; + session_id = session_id_; + + sample_rate = params.sample_rate; + sample_count = params.sample_count; + mix_buffer_count = static_cast(params.mixes); + voice_channels = MaxChannels; + upsampler_count = params.sinks + params.sub_mixes; + memory_pool_count = params.effects + params.voices * MaxWaveBuffers; + render_device = params.rendering_device; + execution_mode = params.execution_mode; + + core.Memory().ZeroBlock(*core.Kernel().CurrentProcess(), transfer_memory->GetSourceAddress(), + transfer_memory_size); + + // Note: We're not actually using the transfer memory because it's a pain to code for. + // Allocate the memory normally instead and hope the game doesn't try to read anything back + workbuffer = std::make_unique(transfer_memory_size); + workbuffer_size = transfer_memory_size; + + PoolMapper pool_mapper(process_handle, false); + pool_mapper.InitializeSystemPool(memory_pool_info, workbuffer.get(), workbuffer_size); + + WorkbufferAllocator allocator({workbuffer.get(), workbuffer_size}, workbuffer_size); + + samples_workbuffer = + allocator.Allocate((voice_channels + mix_buffer_count) * sample_count, 0x10); + if (samples_workbuffer.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + auto upsampler_workbuffer{allocator.Allocate( + (voice_channels + mix_buffer_count) * TargetSampleCount * upsampler_count, 0x10)}; + if (upsampler_workbuffer.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + depop_buffer = + allocator.Allocate(Common::AlignUp(static_cast(mix_buffer_count), 0x40), 0x40); + if (depop_buffer.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + // invalidate samples_workbuffer DSP cache + + auto voice_infos{allocator.Allocate(params.voices, 0x10)}; + for (auto& voice_info : voice_infos) { + std::construct_at(&voice_info); + } + + if (voice_infos.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + auto sorted_voice_infos{allocator.Allocate(params.voices, 0x10)}; + if (sorted_voice_infos.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + std::memset(sorted_voice_infos.data(), 0, sorted_voice_infos.size_bytes()); + + auto voice_channel_resources{allocator.Allocate(params.voices, 0x10)}; + u32 i{0}; + for (auto& voice_channel_resource : voice_channel_resources) { + std::construct_at(&voice_channel_resource, i++); + } + + if (voice_channel_resources.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + auto voice_cpu_states{allocator.Allocate(params.voices, 0x10)}; + if (voice_cpu_states.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + for (auto& voice_state : voice_cpu_states) { + voice_state = {}; + } + + auto mix_infos{allocator.Allocate(params.sub_mixes + 1, 0x10)}; + + if (mix_infos.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + u32 effect_process_order_count{0}; + std::span effect_process_order_buffer{}; + + if (params.effects > 0) { + effect_process_order_count = params.effects * (params.sub_mixes + 1); + effect_process_order_buffer = allocator.Allocate(effect_process_order_count, 0x10); + if (effect_process_order_buffer.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + } + + i = 0; + for (auto& mix_info : mix_infos) { + std::construct_at( + &mix_info, effect_process_order_buffer.subspan(i * params.effects, params.effects), + params.effects, this->behavior); + i++; + } + + auto sorted_mix_infos{allocator.Allocate(params.sub_mixes + 1, 0x10)}; + if (sorted_mix_infos.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + std::memset(sorted_mix_infos.data(), 0, sorted_mix_infos.size_bytes()); + + if (behavior.IsSplitterSupported()) { + u64 node_state_size{NodeStates::GetWorkBufferSize(params.sub_mixes + 1)}; + u64 edge_matrix_size{EdgeMatrix::GetWorkBufferSize(params.sub_mixes + 1)}; + + auto node_states_workbuffer{allocator.Allocate(node_state_size, 1)}; + auto edge_matrix_workbuffer{allocator.Allocate(edge_matrix_size, 1)}; + + if (node_states_workbuffer.empty() || edge_matrix_workbuffer.size() == 0) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + mix_context.Initialize(sorted_mix_infos, mix_infos, params.sub_mixes + 1, + effect_process_order_buffer, effect_process_order_count, + node_states_workbuffer, node_state_size, edge_matrix_workbuffer, + edge_matrix_size); + } else { + mix_context.Initialize(sorted_mix_infos, mix_infos, params.sub_mixes + 1, + effect_process_order_buffer, effect_process_order_count, {}, 0, {}, + 0); + } + + upsampler_manager = allocator.Allocate(1, 0x10).data(); + if (upsampler_manager == nullptr) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + memory_pool_workbuffer = allocator.Allocate(memory_pool_count, 0x10); + for (auto& memory_pool : memory_pool_workbuffer) { + std::construct_at(&memory_pool, MemoryPoolInfo::Location::DSP); + } + + if (memory_pool_workbuffer.empty() && memory_pool_count > 0) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + if (!splitter_context.Initialize(behavior, params, allocator)) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + std::span effect_result_states_cpu{}; + if (behavior.IsEffectInfoVersion2Supported() && params.effects > 0) { + effect_result_states_cpu = allocator.Allocate(params.effects, 0x10); + if (effect_result_states_cpu.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + std::memset(effect_result_states_cpu.data(), 0, effect_result_states_cpu.size_bytes()); + } + + allocator.Align(0x40); + + unk_2B0 = allocator.GetSize() - allocator.GetCurrentOffset(); + unk_2A8 = {&workbuffer[allocator.GetCurrentOffset()], unk_2B0}; + + upsampler_infos = allocator.Allocate(upsampler_count, 0x40); + for (auto& upsampler_info : upsampler_infos) { + std::construct_at(&upsampler_info); + } + + std::construct_at(upsampler_manager, upsampler_count, upsampler_infos, + upsampler_workbuffer); + + if (upsampler_infos.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + auto effect_infos{allocator.Allocate(params.effects, 0x40)}; + for (auto& effect_info : effect_infos) { + std::construct_at(&effect_info); + } + + if (effect_infos.empty() && params.effects > 0) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + std::span effect_result_states_dsp{}; + if (behavior.IsEffectInfoVersion2Supported() && params.effects > 0) { + effect_result_states_dsp = allocator.Allocate(params.effects, 0x40); + if (effect_result_states_dsp.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + std::memset(effect_result_states_dsp.data(), 0, effect_result_states_dsp.size_bytes()); + } + + effect_context.Initialize(effect_infos, params.effects, effect_result_states_cpu, + effect_result_states_dsp, effect_result_states_dsp.size()); + + auto sinks{allocator.Allocate(params.sinks, 0x10)}; + for (auto& sink : sinks) { + std::construct_at(&sink); + } + + if (sinks.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + sink_context.Initialize(sinks, params.sinks); + + auto voice_dsp_states{allocator.Allocate(params.voices, 0x40)}; + if (voice_dsp_states.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + for (auto& voice_state : voice_dsp_states) { + voice_state = {}; + } + + voice_context.Initialize(sorted_voice_infos, voice_infos, voice_channel_resources, + voice_cpu_states, voice_dsp_states, params.voices); + + if (params.perf_frames > 0) { + const auto perf_workbuffer_size{ + PerformanceManager::GetRequiredBufferSizeForPerformanceMetricsPerFrame(behavior, + params) * + (params.perf_frames + 1) + + 0xC}; + performance_workbuffer = allocator.Allocate(perf_workbuffer_size, 0x40); + if (performance_workbuffer.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + std::memset(performance_workbuffer.data(), 0, performance_workbuffer.size_bytes()); + performance_manager.Initialize(performance_workbuffer, performance_workbuffer.size_bytes(), + params, behavior, memory_pool_info); + } + + render_time_limit_percent = 100; + drop_voice = params.voice_drop_enabled && params.execution_mode == ExecutionMode::Auto; + + allocator.Align(0x40); + command_workbuffer_size = allocator.GetRemainingSize(); + command_workbuffer = allocator.Allocate(command_workbuffer_size, 0x40); + if (command_workbuffer.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + command_buffer_size = 0; + reset_command_buffers = true; + + // nn::audio::dsp::FlushDataCache(transferMemory, transferMemorySize); + + if (behavior.IsCommandProcessingTimeEstimatorVersion4Supported()) { + command_processing_time_estimator = + std::make_unique(sample_count, + mix_buffer_count); + } else if (behavior.IsCommandProcessingTimeEstimatorVersion3Supported()) { + command_processing_time_estimator = + std::make_unique(sample_count, + mix_buffer_count); + } else if (behavior.IsCommandProcessingTimeEstimatorVersion2Supported()) { + command_processing_time_estimator = + std::make_unique(sample_count, + mix_buffer_count); + } else { + command_processing_time_estimator = + std::make_unique(sample_count, + mix_buffer_count); + } + + initialized = true; + return ResultSuccess; +} + +void System::Finalize() { + if (!initialized) { + return; + } + + if (active) { + Stop(); + } + + applet_resource_user_id = 0; + + PoolMapper pool_mapper(process_handle, false); + pool_mapper.Unmap(memory_pool_info); + + if (process_handle) { + pool_mapper.ClearUseState(memory_pool_workbuffer, memory_pool_count); + for (auto& memory_pool : memory_pool_workbuffer) { + if (memory_pool.IsMapped()) { + pool_mapper.Unmap(memory_pool); + } + } + + // dsp::ProcessCleanup + // close handle + } + initialized = false; +} + +void System::Start() { + std::scoped_lock l{lock}; + frames_elapsed = 0; + state = State::Started; + active = true; +} + +void System::Stop() { + { + std::scoped_lock l{lock}; + state = State::Stopped; + active = false; + } + + if (execution_mode == ExecutionMode::Auto) { + // Should wait for the system to terminate here, but core timing (should have) already + // stopped, so this isn't needed. Find a way to make this definite. + + // terminate_event.Wait(); + } +} + +Result System::Update(std::span input, std::span performance, std::span output) { + std::scoped_lock l{lock}; + + const auto start_time{core.CoreTiming().GetClockTicks()}; + + InfoUpdater info_updater(input, output, process_handle, behavior); + + auto result{info_updater.UpdateBehaviorInfo(behavior)}; + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update BehaviorInfo!"); + return result; + } + + result = info_updater.UpdateMemoryPools(memory_pool_workbuffer, memory_pool_count); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update MemoryPools!"); + return result; + } + + result = info_updater.UpdateVoiceChannelResources(voice_context); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update VoiceChannelResources!"); + return result; + } + + result = info_updater.UpdateVoices(voice_context, memory_pool_workbuffer, memory_pool_count); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update Voices!"); + return result; + } + + result = info_updater.UpdateEffects(effect_context, active, memory_pool_workbuffer, + memory_pool_count); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update Effects!"); + return result; + } + + if (behavior.IsSplitterSupported()) { + result = info_updater.UpdateSplitterInfo(splitter_context); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update SplitterInfo!"); + return result; + } + } + + result = + info_updater.UpdateMixes(mix_context, mix_buffer_count, effect_context, splitter_context); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update Mixes!"); + return result; + } + + result = info_updater.UpdateSinks(sink_context, memory_pool_workbuffer, memory_pool_count); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update Sinks!"); + return result; + } + + PerformanceManager* perf_manager{nullptr}; + if (performance_manager.IsInitialized()) { + perf_manager = &performance_manager; + } + + result = + info_updater.UpdatePerformanceBuffer(performance, performance.size_bytes(), perf_manager); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update PerformanceBuffer!"); + return result; + } + + result = info_updater.UpdateErrorInfo(behavior); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update ErrorInfo!"); + return result; + } + + if (behavior.IsElapsedFrameCountSupported()) { + result = info_updater.UpdateRendererInfo(frames_elapsed); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update RendererInfo!"); + return result; + } + } + + result = info_updater.CheckConsumedSize(); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Invalid consume size!"); + return result; + } + + adsp_rendered_event->GetWritableEvent().Clear(); + num_times_updated++; + + const auto end_time{core.CoreTiming().GetClockTicks()}; + ticks_spent_updating += end_time - start_time; + + return ResultSuccess; +} + +u32 System::GetRenderingTimeLimit() const { + return render_time_limit_percent; +} + +void System::SetRenderingTimeLimit(const u32 limit) { + render_time_limit_percent = limit; +} + +u32 System::GetSessionId() const { + return session_id; +} + +u32 System::GetSampleRate() const { + return sample_rate; +} + +u32 System::GetSampleCount() const { + return sample_count; +} + +u32 System::GetMixBufferCount() const { + return mix_buffer_count; +} + +ExecutionMode System::GetExecutionMode() const { + return execution_mode; +} + +u32 System::GetRenderingDevice() const { + return render_device; +} + +bool System::IsActive() const { + return active; +} + +void System::SendCommandToDsp() { + std::scoped_lock l{lock}; + + if (initialized) { + if (active) { + terminate_event.Reset(); + const auto remaining_command_count{adsp.GetRemainCommandCount(session_id)}; + u64 command_size{0}; + + if (remaining_command_count) { + adsp_behind = true; + command_size = command_buffer_size; + } else { + command_size = GenerateCommand(command_workbuffer, command_workbuffer_size); + } + + auto translated_addr{ + memory_pool_info.Translate(CpuAddr(command_workbuffer.data()), command_size)}; + + auto time_limit_percent{70.0f}; + if (behavior.IsAudioRenererProcessingTimeLimit80PercentSupported()) { + time_limit_percent = 80.0f; + } else if (behavior.IsAudioRenererProcessingTimeLimit75PercentSupported()) { + time_limit_percent = 75.0f; + } else { + // result ignored and 70 is used anyway + behavior.IsAudioRenererProcessingTimeLimit70PercentSupported(); + time_limit_percent = 70.0f; + } + + ADSP::CommandBuffer command_buffer{ + .buffer{translated_addr}, + .size{command_size}, + .time_limit{ + static_cast((time_limit_percent / 100) * 2'880'000.0 * + (static_cast(render_time_limit_percent) / 100.0f))}, + .remaining_command_count{remaining_command_count}, + .reset_buffers{reset_command_buffers}, + .applet_resource_user_id{applet_resource_user_id}, + .render_time_taken{adsp.GetRenderTimeTaken(session_id)}, + }; + + adsp.SendCommandBuffer(session_id, command_buffer); + reset_command_buffers = false; + command_buffer_size = command_size; + if (remaining_command_count == 0) { + adsp_rendered_event->GetWritableEvent().Signal(); + } + } else { + adsp.ClearRemainCount(session_id); + terminate_event.Set(); + } + } +} + +u64 System::GenerateCommand(std::span in_command_buffer, + [[maybe_unused]] const u64 command_buffer_size_) { + PoolMapper::ClearUseState(memory_pool_workbuffer, memory_pool_count); + const auto start_time{core.CoreTiming().GetClockTicks()}; + + auto command_list_header{reinterpret_cast(in_command_buffer.data())}; + + command_list_header->buffer_count = static_cast(voice_channels + mix_buffer_count); + command_list_header->sample_count = sample_count; + command_list_header->sample_rate = sample_rate; + command_list_header->samples_buffer = samples_workbuffer; + + const auto performance_initialized{performance_manager.IsInitialized()}; + if (performance_initialized) { + performance_manager.TapFrame(adsp_behind, num_voices_dropped, render_start_tick); + adsp_behind = false; + num_voices_dropped = 0; + render_start_tick = 0; + } + + s8 channel_count{2}; + if (execution_mode == ExecutionMode::Auto) { + const auto& sink{core.AudioCore().GetOutputSink()}; + channel_count = static_cast(sink.GetDeviceChannels()); + } + + AudioRendererSystemContext render_context{ + .session_id{session_id}, + .channels{channel_count}, + .mix_buffer_count{mix_buffer_count}, + .behavior{&behavior}, + .depop_buffer{depop_buffer}, + .upsampler_manager{upsampler_manager}, + .memory_pool_info{&memory_pool_info}, + }; + + CommandBuffer command_buffer{ + .command_list{in_command_buffer}, + .sample_count{sample_count}, + .sample_rate{sample_rate}, + .size{sizeof(CommandListHeader)}, + .count{0}, + .estimated_process_time{0}, + .memory_pool{&memory_pool_info}, + .time_estimator{command_processing_time_estimator.get()}, + .behavior{&behavior}, + }; + + PerformanceManager* perf_manager{nullptr}; + if (performance_initialized) { + perf_manager = &performance_manager; + } + + CommandGenerator command_generator{command_buffer, *command_list_header, render_context, + voice_context, mix_context, effect_context, + sink_context, splitter_context, perf_manager}; + + voice_context.SortInfo(); + + const auto start_estimated_time{command_buffer.estimated_process_time}; + + command_generator.GenerateVoiceCommands(); + command_generator.GenerateSubMixCommands(); + command_generator.GenerateFinalMixCommands(); + command_generator.GenerateSinkCommands(); + + if (drop_voice) { + f32 time_limit_percent{70.0f}; + if (render_context.behavior->IsAudioRenererProcessingTimeLimit80PercentSupported()) { + time_limit_percent = 80.0f; + } else if (render_context.behavior->IsAudioRenererProcessingTimeLimit75PercentSupported()) { + time_limit_percent = 75.0f; + } else { + // result is ignored + render_context.behavior->IsAudioRenererProcessingTimeLimit70PercentSupported(); + time_limit_percent = 70.0f; + } + const auto time_limit{static_cast( + static_cast(start_estimated_time - command_buffer.estimated_process_time) + + (((time_limit_percent / 100.0f) * 2'880'000.0) * + (static_cast(render_time_limit_percent) / 100.0f)))}; + num_voices_dropped = DropVoices(command_buffer, start_estimated_time, time_limit); + } + + command_list_header->buffer_size = command_buffer.size; + command_list_header->command_count = command_buffer.count; + + voice_context.UpdateStateByDspShared(); + + if (render_context.behavior->IsEffectInfoVersion2Supported()) { + effect_context.UpdateStateByDspShared(); + } + + const auto end_time{core.CoreTiming().GetClockTicks()}; + total_ticks_elapsed += end_time - start_time; + num_command_lists_generated++; + render_start_tick = adsp.GetRenderingStartTick(session_id); + frames_elapsed++; + + return command_buffer.size; +} + +u32 System::DropVoices(CommandBuffer& command_buffer, const u32 estimated_process_time, + const u32 time_limit) { + u32 i{0}; + auto command_list{command_buffer.command_list.data() + sizeof(CommandListHeader)}; + ICommand* cmd{}; + + for (; i < command_buffer.count; i++) { + cmd = reinterpret_cast(command_list); + if (cmd->type != CommandId::Performance && + cmd->type != CommandId::DataSourcePcmInt16Version1 && + cmd->type != CommandId::DataSourcePcmInt16Version2 && + cmd->type != CommandId::DataSourcePcmFloatVersion1 && + cmd->type != CommandId::DataSourcePcmFloatVersion2 && + cmd->type != CommandId::DataSourceAdpcmVersion1 && + cmd->type != CommandId::DataSourceAdpcmVersion2) { + break; + } + command_list += cmd->size; + } + + if (cmd == nullptr || command_buffer.count == 0 || i >= command_buffer.count) { + return 0; + } + + auto voices_dropped{0}; + while (i < command_buffer.count) { + const auto node_id{cmd->node_id}; + const auto node_id_type{cmd->node_id >> 28}; + const auto node_id_base{cmd->node_id & 0xFFF}; + + if (estimated_process_time <= time_limit) { + break; + } + + if (node_id_type != 1) { + break; + } + + auto& voice_info{voice_context.GetInfo(node_id_base)}; + if (voice_info.priority == HighestVoicePriority) { + break; + } + + voices_dropped++; + voice_info.voice_dropped = true; + + if (i < command_buffer.count) { + while (cmd->node_id == node_id) { + if (cmd->type == CommandId::DepopPrepare) { + cmd->enabled = true; + } else if (cmd->type == CommandId::Performance || !cmd->enabled) { + cmd->enabled = false; + } + i++; + command_list += cmd->size; + cmd = reinterpret_cast(command_list); + } + } + } + return voices_dropped; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/system.h b/src/audio_core/renderer/system.h new file mode 100755 index 000000000..bcbe65b07 --- /dev/null +++ b/src/audio_core/renderer/system.h @@ -0,0 +1,307 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/command/command_processing_time_estimator.h" +#include "audio_core/renderer/effect/effect_context.h" +#include "audio_core/renderer/memory/memory_pool_info.h" +#include "audio_core/renderer/mix/mix_context.h" +#include "audio_core/renderer/performance/performance_manager.h" +#include "audio_core/renderer/sink/sink_context.h" +#include "audio_core/renderer/splitter/splitter_context.h" +#include "audio_core/renderer/upsampler/upsampler_manager.h" +#include "audio_core/renderer/voice/voice_context.h" +#include "common/thread.h" +#include "core/hle/service/audio/errors.h" + +namespace Core { +namespace Memory { +class Memory; +} +class System; +} // namespace Core + +namespace Kernel { +class KEvent; +class KTransferMemory; +} // namespace Kernel + +namespace AudioCore { +struct AudioRendererParameterInternal; + +namespace AudioRenderer { +class CommandBuffer; +namespace ADSP { +class ADSP; +} + +/** + * Audio Renderer System, the main worker for audio rendering. + */ +class System { + enum class State { + Started = 0, + Stopped = 2, + }; + +public: + explicit System(Core::System& core, Kernel::KEvent* adsp_rendered_event); + + /** + * Calculate the total size required for all audio render workbuffers. + * + * @param params - Input parameters with the numbers of voices/mixes/sinks/etc. + * @return Size (in bytes) required for the audio renderer. + */ + static u64 GetWorkBufferSize(const AudioRendererParameterInternal& params); + + /** + * Initialize the renderer system. + * Allocates workbuffers and initializes everything to a default state, ready to receive a + * RequestUpdate. + * + * @param params - Input parameters to initialize the system with. + * @param transfer_memory - Game-supplied memory for all workbuffers. Unused. + * @param transfer_memory_size - Size of the transfer memory. Unused. + * @param process_handle - Process handle, also used for memory. Unused. + * @param applet_resource_user_id - Applet id for this renderer. Unused. + * @param session_id - Session id of this renderer. + * @return Result code. + */ + Result Initialize(const AudioRendererParameterInternal& params, + Kernel::KTransferMemory* transfer_memory, u64 transfer_memory_size, + u32 process_handle, u64 applet_resource_user_id, s32 session_id); + + /** + * Finalize the system. + */ + void Finalize(); + + /** + * Start the system. + */ + void Start(); + + /** + * Stop the system. + */ + void Stop(); + + /** + * Update the system. + * + * @param input - Inout buffer containing the update data. + * @param performance - Optional buffer for writing back performance metrics. + * @param output - Output information from rendering. + * @return Result code. + */ + Result Update(std::span input, std::span performance, std::span output); + + /** + * Get the time limit (percent) for rendering + * + * @return Time limit as a percent. + */ + u32 GetRenderingTimeLimit() const; + + /** + * Set the time limit (percent) for rendering + * + * @param limit - New time limit. + */ + void SetRenderingTimeLimit(u32 limit); + + /** + * Get the session id for this system. + * + * @return Session id of this system. + */ + u32 GetSessionId() const; + + /** + * Get the sample rate of this system. + * + * @return Sample rate of this system. + */ + u32 GetSampleRate() const; + + /** + * Get the sample count of this system. + * + * @return Sample count of this system. + */ + u32 GetSampleCount() const; + + /** + * Get the number of mix buffers for this system. + * + * @return Number of mix buffers in the system. + */ + u32 GetMixBufferCount() const; + + /** + * Get the execution mode of this system. + * Note: Only Auto is implemented. + * + * @return Execution mode for this system. + */ + ExecutionMode GetExecutionMode() const; + + /** + * Get the rendering deivce for this system. + * This is unused. + * + * @return Rendering device for this system. + */ + u32 GetRenderingDevice() const; + + /** + * Check if this system is currently active. + * + * @return True if active, otherwise false. + */ + bool IsActive() const; + + /** + * Prepare and generate a list of commands for the AudioRenderer based on current state, + * signalling the buffer event when all processed. + */ + void SendCommandToDsp(); + + /** + * Generate a list of commands for the AudioRenderer based on current state. + * + * @param command_buffer - Buffer for commands to be written to. + * @param command_buffer_size - Size of the command_buffer. + * + * @return Number of bytes written. + */ + u64 GenerateCommand(std::span command_buffer, u64 command_buffer_size); + + /** + * Try to drop some voices if the AudioRenderer fell behind. + * + * @param command_buffer - Command buffer to drop voices from. + * @param estimated_process_time - Current estimated processing time of all commands. + * @param time_limit - Time limit for rendering, voices are dropped if estimated + * exceeds this. + * + * @return Number of voices dropped. + */ + u32 DropVoices(CommandBuffer& command_buffer, u32 estimated_process_time, u32 time_limit); + +private: + /// Core system + Core::System& core; + /// Reference to the ADSP for communication + ADSP::ADSP& adsp; + /// Is this system initialized? + bool initialized{}; + /// Is this system currently active? + std::atomic active{}; + /// State of the system + State state{State::Stopped}; + /// Sample rate for the system + u32 sample_rate{}; + /// Sample count of the system + u32 sample_count{}; + /// Number of mix buffers in use by the system + s16 mix_buffer_count{}; + /// Workbuffer for mix buffers, used by the AudioRenderer + std::span samples_workbuffer{}; + /// Depop samples for depopping commands + std::span depop_buffer{}; + /// Number of memory pools in the buffer + u32 memory_pool_count{}; + /// Workbuffer for memory pools + std::span memory_pool_workbuffer{}; + /// System memory pool info + MemoryPoolInfo memory_pool_info{}; + /// Workbuffer that commands will be generated into + std::span command_workbuffer{}; + /// Size of command workbuffer + u64 command_workbuffer_size{}; + /// Numebr of commands in the workbuffer + u64 command_buffer_size{}; + /// Manager for upsamplers + UpsamplerManager* upsampler_manager{}; + /// Upsampler workbuffer + std::span upsampler_infos{}; + /// Number of upsamplers in the workbuffer + u32 upsampler_count{}; + /// Holds and controls all voices + VoiceContext voice_context{}; + /// Holds and controls all mixes + MixContext mix_context{}; + /// Holds and controls all effects + EffectContext effect_context{}; + /// Holds and controls all sinks + SinkContext sink_context{}; + /// Holds and controls all splitters + SplitterContext splitter_context{}; + /// Estimates the time taken for each command + std::unique_ptr command_processing_time_estimator{}; + /// Session id of this system + s32 session_id{}; + /// Number of channels in use by voices + s32 voice_channels{}; + /// Event to be called when the AudioRenderer processes a command list + Kernel::KEvent* adsp_rendered_event{}; + /// Event signalled on system terminate + Common::Event terminate_event{}; + /// Does what locks do + std::mutex lock{}; + /// Handle for the process for this system, unused + u32 process_handle{}; + /// Applet resource id for this system, unused + u64 applet_resource_user_id{}; + /// Controls performance input and output + PerformanceManager performance_manager{}; + /// Workbuffer for performance metrics + std::span performance_workbuffer{}; + /// Main workbuffer, from which all other workbuffers here allocate into + std::unique_ptr workbuffer{}; + /// Size of the main workbuffer + u64 workbuffer_size{}; + /// Unknown buffer/marker + std::span unk_2A8{}; + /// Size of the above unknown buffer/marker + u64 unk_2B0{}; + /// Rendering time limit (percent) + u32 render_time_limit_percent{}; + /// Should any voices be dropped? + bool drop_voice{}; + /// Should the backend stream have its buffers flushed? + bool reset_command_buffers{}; + /// Execution mode of this system, only Auto is supported + ExecutionMode execution_mode{ExecutionMode::Auto}; + /// Render device, unused + u32 render_device{}; + /// Behaviour to check which features are supported by the user revision + BehaviorInfo behavior{}; + /// Total ticks the audio system has been running + u64 total_ticks_elapsed{}; + /// Ticks the system has spent in updates + u64 ticks_spent_updating{}; + /// Number of times a command list was generated + u64 num_command_lists_generated{}; + /// Number of times the system has updated + u64 num_times_updated{}; + /// Number of frames generated, written back to the game + std::atomic frames_elapsed{}; + /// Is the AudioRenderer running too slow? + bool adsp_behind{}; + /// Number of voices dropped + u32 num_voices_dropped{}; + /// Tick that rendering started + u64 render_start_tick{}; +}; + +} // namespace AudioRenderer +} // namespace AudioCore diff --git a/src/audio_core/renderer/system_manager.cpp b/src/audio_core/renderer/system_manager.cpp new file mode 100755 index 000000000..098e58a81 --- /dev/null +++ b/src/audio_core/renderer/system_manager.cpp @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/audio_core.h" +#include "audio_core/renderer/adsp/adsp.h" +#include "audio_core/renderer/system_manager.h" +#include "common/microprofile.h" +#include "common/thread.h" +#include "core/core.h" +#include "core/core_timing.h" + +MICROPROFILE_DEFINE(Audio_RenderSystemManager, "Audio", "Render System Manager", + MP_RGB(60, 19, 97)); + +namespace AudioCore::AudioRenderer { + +SystemManager::SystemManager(Core::System& core_) + : core{core_}, adsp{core.AudioCore().GetADSP()}, mailbox{adsp.GetRenderMailbox()}, + thread_event{Core::Timing::CreateEvent( + "AudioRendererSystemManager", + [this](std::uintptr_t userdata, std::chrono::nanoseconds ns_late) { ThreadFunc2(); })} {} + +SystemManager::~SystemManager() { + Stop(); +} + +bool SystemManager::InitializeUnsafe() { + if (!active) { + if (adsp.Start()) { + active = true; + core.CoreTiming().ScheduleLoopingEvent(std::chrono::nanoseconds(2'304'000ULL * 2), + thread_event); + thread = std::jthread([this](std::stop_token stop_token) { ThreadFunc(); }); + } + } + + return adsp.GetState() == ADSP::State::Started; +} + +void SystemManager::Stop() { + if (!active) { + return; + } + core.CoreTiming().UnscheduleEvent(thread_event, {}); + active = false; + update.store(true); + update.notify_all(); + thread.join(); + adsp.Stop(); +} + +bool SystemManager::Add(System& system_) { + std::scoped_lock l2{mutex2}; + + if (systems.size() + 1 > MaxRendererSessions) { + LOG_ERROR(Service_Audio, "Maximum AudioRenderer Systems active, cannot add more!"); + return false; + } + + { + std::scoped_lock l{mutex1}; + if (systems.empty()) { + if (!InitializeUnsafe()) { + LOG_ERROR(Service_Audio, "Failed to start the AudioRenderer SystemManager"); + return false; + } + } + } + + systems.push_back(&system_); + return true; +} + +bool SystemManager::Remove(System& system_) { + std::scoped_lock l2{mutex2}; + + { + std::scoped_lock l{mutex1}; + if (systems.remove(&system_) == 0) { + LOG_ERROR(Service_Audio, + "Failed to remove a render system, it was not found in the list!"); + return false; + } + } + + if (systems.empty()) { + Stop(); + } + return true; +} + +void SystemManager::ThreadFunc() { + constexpr char name[]{"yuzu:AudioRenderSystemManager"}; + MicroProfileOnThreadCreate(name); + Common::SetCurrentThreadName(name); + Common::SetCurrentThreadPriority(Common::ThreadPriority::Critical); + while (active) { + { + std::scoped_lock l{mutex1}; + MICROPROFILE_SCOPE(Audio_RenderSystemManager); + for (auto system : systems) { + system->SendCommandToDsp(); + } + } + + adsp.Signal(); + adsp.Wait(); + + update.wait(false); + update.store(false); + } +} + +void SystemManager::ThreadFunc2() { + update.store(true); + update.notify_all(); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/system_manager.h b/src/audio_core/renderer/system_manager.h new file mode 100755 index 000000000..2f7964a1d --- /dev/null +++ b/src/audio_core/renderer/system_manager.h @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "audio_core/renderer/system.h" + +namespace Core { +namespace Timing { +struct EventType; +} +class System; +} // namespace Core + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class ADSP; +class AudioRenderer_Mailbox; +} // namespace ADSP + +/** + * Manages all audio renderers, responsible for triggering command list generation and signalling + * the ADSP. + */ +class SystemManager { +public: + explicit SystemManager(Core::System& core); + ~SystemManager(); + + /** + * Initialize the system manager, called when any system is registered. + * + * @return True if sucessfully initialized, otherwise false. + */ + bool InitializeUnsafe(); + + /** + * Stop the system manager. + */ + void Stop(); + + /** + * Add an audio render system to the manager. + * The manager does not own the system, so do not free it without calling Remove. + * + * @param system - The system to add. + * @return True if succesfully added, otherwise false. + */ + bool Add(System& system); + + /** + * Remove an audio render system from the manager. + * + * @param system - The system to remove. + * @return True if succesfully removed, otherwise false. + */ + bool Remove(System& system); + +private: + /** + * Main thread responsible for command generation. + */ + void ThreadFunc(); + + /** + * Signalling core timing thread to run ThreadFunc. + */ + void ThreadFunc2(); + + /// Core system + Core::System& core; + /// List of pointers to managed systems + std::list systems{}; + /// Main worker thread for generating command lists + std::jthread thread; + /// Mutex for the systems + std::mutex mutex1{}; + /// Mutex for adding/removing systems + std::mutex mutex2{}; + /// Is the system manager thread active? + std::atomic active{}; + /// Reference to the ADSP for communication + ADSP::ADSP& adsp; + /// AudioRenderer mailbox for communication + ADSP::AudioRenderer_Mailbox* mailbox{}; + /// Core timing event to signal main thread + std::shared_ptr thread_event; + /// Atomic for main thread to wait on + std::atomic update{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/upsampler/upsampler_info.cpp b/src/audio_core/renderer/upsampler/upsampler_info.cpp new file mode 100755 index 000000000..e3d2f7db0 --- /dev/null +++ b/src/audio_core/renderer/upsampler/upsampler_info.cpp @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/upsampler/upsampler_info.h" + +namespace AudioCore::AudioRenderer {} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/upsampler/upsampler_info.h b/src/audio_core/renderer/upsampler/upsampler_info.h new file mode 100755 index 000000000..a43c15af3 --- /dev/null +++ b/src/audio_core/renderer/upsampler/upsampler_info.h @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/upsampler/upsampler_state.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +class UpsamplerManager; + +/** + * Manages information needed to upsample a mix buffer. + */ +struct UpsamplerInfo { + /// States used by the AudioRenderer across calls. + std::array states{}; + /// Pointer to the manager + UpsamplerManager* manager{}; + /// Pointer to the samples to be upsampled + CpuAddr samples_pos{}; + /// Target number of samples to upsample to + u32 sample_count{}; + /// Number of channels to upsample + u32 input_count{}; + /// Is this upsampler enabled? + bool enabled{}; + /// Mix buffer indexes to be upsampled + std::array inputs{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/upsampler/upsampler_manager.cpp b/src/audio_core/renderer/upsampler/upsampler_manager.cpp new file mode 100755 index 000000000..4c76a5066 --- /dev/null +++ b/src/audio_core/renderer/upsampler/upsampler_manager.cpp @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/upsampler/upsampler_manager.h" + +namespace AudioCore::AudioRenderer { + +UpsamplerManager::UpsamplerManager(const u32 count_, std::span infos_, + std::span workbuffer_) + : count{count_}, upsampler_infos{infos_}, workbuffer{workbuffer_} {} + +UpsamplerInfo* UpsamplerManager::Allocate() { + std::scoped_lock l{lock}; + + if (count == 0) { + return nullptr; + } + + u32 free_index{0}; + for (auto& upsampler : upsampler_infos) { + if (!upsampler.enabled) { + break; + } + free_index++; + } + + if (free_index >= count) { + return nullptr; + } + + auto& upsampler{upsampler_infos[free_index]}; + upsampler.manager = this; + upsampler.sample_count = TargetSampleCount; + upsampler.samples_pos = CpuAddr(&workbuffer[upsampler.sample_count * MaxChannels]); + upsampler.enabled = true; + return &upsampler; +} + +void UpsamplerManager::Free(UpsamplerInfo* info) { + std::scoped_lock l{lock}; + info->enabled = false; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/upsampler/upsampler_manager.h b/src/audio_core/renderer/upsampler/upsampler_manager.h new file mode 100755 index 000000000..70cd42b08 --- /dev/null +++ b/src/audio_core/renderer/upsampler/upsampler_manager.h @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/upsampler/upsampler_info.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Manages and has utility functions for upsampler infos. + */ +class UpsamplerManager { +public: + UpsamplerManager(u32 count, std::span infos, std::span workbuffer); + + /** + * Allocate a new UpsamplerInfo. + * + * @return The allocated upsampler, may be nullptr if alloc failed. + */ + UpsamplerInfo* Allocate(); + + /** + * Free the given upsampler. + * + * @param The upsampler to be freed. + */ + void Free(UpsamplerInfo* info); + +private: + /// Maximum number of upsamplers in the buffer + const u32 count; + /// Upsamplers buffer + std::span upsampler_infos; + /// Workbuffer for upsampling samples + std::span workbuffer; + /// Lock for allocate/free + std::mutex lock{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/upsampler/upsampler_state.h b/src/audio_core/renderer/upsampler/upsampler_state.h new file mode 100755 index 000000000..28cebe200 --- /dev/null +++ b/src/audio_core/renderer/upsampler/upsampler_state.h @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "common/common_types.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { +/** + * Upsampling state used by the AudioRenderer across calls. + */ +struct UpsamplerState { + static constexpr u16 HistorySize = 20; + + /// Source data to target data ratio. E.g 48'000/32'000 = 1.5 + Common::FixedPoint<16, 16> ratio; + /// Sample history + std::array, HistorySize> history; + /// Size of the sinc coefficient window + u16 window_size; + /// Read index for the history + u16 history_output_index; + /// Write index for the history + u16 history_input_index; + /// Start offset within the history, fixed to 0 + u16 history_start_index; + /// Ebd offset within the history, fixed to HistorySize + u16 history_end_index; + /// Is this state initialized? + bool initialized; + /// Index of the current sample. + /// E.g 16K -> 48K has a ratio of 3, so this will be 0-2. + /// See the Upsample command in the AudioRenderer for more information. + u8 sample_index; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/voice/voice_channel_resource.h b/src/audio_core/renderer/voice/voice_channel_resource.h new file mode 100755 index 000000000..26ab4ccce --- /dev/null +++ b/src/audio_core/renderer/voice/voice_channel_resource.h @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Represents one channel for mixing a voice. + */ +class VoiceChannelResource { +public: + struct InParameter { + /* 0x00 */ u32 id; + /* 0x04 */ std::array mix_volumes; + /* 0x64 */ bool in_use; + /* 0x65 */ char unk65[0xB]; + }; + static_assert(sizeof(InParameter) == 0x70, + "VoiceChannelResource::InParameter has the wrong size!"); + + explicit VoiceChannelResource(u32 id_) : id{id_} {} + + /// Current volume for each mix buffer + std::array mix_volumes{}; + /// Previous volume for each mix buffer + std::array prev_mix_volumes{}; + /// Id of this resource + const u32 id; + /// Is this resource in use? + bool in_use{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/voice/voice_context.cpp b/src/audio_core/renderer/voice/voice_context.cpp new file mode 100755 index 000000000..eafb51b01 --- /dev/null +++ b/src/audio_core/renderer/voice/voice_context.cpp @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/renderer/voice/voice_context.h" + +namespace AudioCore::AudioRenderer { + +VoiceState& VoiceContext::GetDspSharedState(const u32 index) { + if (index >= dsp_states.size()) { + LOG_ERROR(Service_Audio, "Invalid voice dsp state index {:04X}", index); + } + return dsp_states[index]; +} + +VoiceChannelResource& VoiceContext::GetChannelResource(const u32 index) { + if (index >= channel_resources.size()) { + LOG_ERROR(Service_Audio, "Invalid voice channel resource index {:04X}", index); + } + return channel_resources[index]; +} + +void VoiceContext::Initialize(std::span sorted_voice_infos_, + std::span voice_infos_, + std::span voice_channel_resources_, + std::span cpu_states_, std::span dsp_states_, + const u32 voice_count_) { + sorted_voice_info = sorted_voice_infos_; + voices = voice_infos_; + channel_resources = voice_channel_resources_; + cpu_states = cpu_states_; + dsp_states = dsp_states_; + voice_count = voice_count_; + active_count = 0; +} + +VoiceInfo* VoiceContext::GetSortedInfo(const u32 index) { + if (index >= sorted_voice_info.size()) { + LOG_ERROR(Service_Audio, "Invalid voice sorted info index {:04X}", index); + } + return sorted_voice_info[index]; +} + +VoiceInfo& VoiceContext::GetInfo(const u32 index) { + if (index >= voices.size()) { + LOG_ERROR(Service_Audio, "Invalid voice info index {:04X}", index); + } + return voices[index]; +} + +VoiceState& VoiceContext::GetState(const u32 index) { + if (index >= cpu_states.size()) { + LOG_ERROR(Service_Audio, "Invalid voice cpu state index {:04X}", index); + } + return cpu_states[index]; +} + +u32 VoiceContext::GetCount() const { + return voice_count; +} + +u32 VoiceContext::GetActiveCount() const { + return active_count; +} + +void VoiceContext::SetActiveCount(const u32 active_count_) { + active_count = active_count_; +} + +void VoiceContext::SortInfo() { + for (u32 i = 0; i < voice_count; i++) { + sorted_voice_info[i] = &voices[i]; + } + + std::ranges::sort(sorted_voice_info, [](const VoiceInfo* a, const VoiceInfo* b) { + return a->priority != b->priority ? a->priority < b->priority + : a->sort_order < b->sort_order; + }); +} + +void VoiceContext::UpdateStateByDspShared() { + std::memcpy(cpu_states.data(), dsp_states.data(), voice_count * sizeof(VoiceState)); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/voice/voice_context.h b/src/audio_core/renderer/voice/voice_context.h new file mode 100755 index 000000000..43b677154 --- /dev/null +++ b/src/audio_core/renderer/voice/voice_context.h @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/voice/voice_channel_resource.h" +#include "audio_core/renderer/voice/voice_info.h" +#include "audio_core/renderer/voice/voice_state.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Contains all voices, with utility functions for managing them. + */ +class VoiceContext { +public: + /** + * Get the AudioRenderer state for a given index + * + * @param index - State index to get. + * @return The requested voice state. + */ + VoiceState& GetDspSharedState(u32 index); + + /** + * Get the channel resource for a given index + * + * @param index - Resource index to get. + * @return The requested voice resource. + */ + VoiceChannelResource& GetChannelResource(u32 index); + + /** + * Initialize the voice context. + * + * @param sorted_voice_infos - Workbuffer for the sorted voices. + * @param voice_infos - Workbuffer for the voices. + * @param voice_channel_resources - Workbuffer for the voice channel resources. + * @param cpu_states - Workbuffer for the host-side voice states. + * @param dsp_states - Workbuffer for the AudioRenderer-side voice states. + * @param voice_count - The number of voices in each workbuffer. + */ + void Initialize(std::span sorted_voice_infos, std::span voice_infos, + std::span voice_channel_resources, + std::span cpu_states, std::span dsp_states, + u32 voice_count); + + /** + * Get a sorted voice with the given index. + * + * @param index - The sorted voice index to get. + * @return The sorted voice. + */ + VoiceInfo* GetSortedInfo(u32 index); + + /** + * Get a voice with the given index. + * + * @param index - The voice index to get. + * @return The voice. + */ + VoiceInfo& GetInfo(u32 index); + + /** + * Get a host voice state with the given index. + * + * @param index - The host voice state index to get. + * @return The voice state. + */ + VoiceState& GetState(u32 index); + + /** + * Get the maximum number of voices. + * Not all voices in the buffers may be in use, see GetActiveCount. + * + * @return The maximum number of voices. + */ + u32 GetCount() const; + + /** + * Get the number of active voices. + * Can be less than or equal to the maximum number of voices. + * + * @return The number of active voices. + */ + u32 GetActiveCount() const; + + /** + * Set the number of active voices. + * Can be less than or equal to the maximum number of voices. + * + * @param active_count - The new number of active voices. + */ + void SetActiveCount(u32 active_count); + + /** + * Sort all voices. Results are available via GetSortedInfo. + * Voices are sorted descendingly, according to priority, and then sort order. + */ + void SortInfo(); + + /** + * Update all voice states, copying AudioRenderer-side states to host-side states. + */ + void UpdateStateByDspShared(); + +private: + /// Sorted voices + std::span sorted_voice_info{}; + /// Voices + std::span voices{}; + /// Channel resources + std::span channel_resources{}; + /// Host-side voice states + std::span cpu_states{}; + /// AudioRenderer-side voice states + std::span dsp_states{}; + /// Maximum number of voices + u32 voice_count{}; + /// Number of active voices + u32 active_count{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/voice/voice_info.cpp b/src/audio_core/renderer/voice/voice_info.cpp new file mode 100755 index 000000000..1849eeb57 --- /dev/null +++ b/src/audio_core/renderer/voice/voice_info.cpp @@ -0,0 +1,408 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/memory/pool_mapper.h" +#include "audio_core/renderer/voice/voice_context.h" +#include "audio_core/renderer/voice/voice_info.h" +#include "audio_core/renderer/voice/voice_state.h" + +namespace AudioCore::AudioRenderer { + +VoiceInfo::VoiceInfo() { + Initialize(); +} + +void VoiceInfo::Initialize() { + in_use = false; + is_new = false; + id = 0; + node_id = 0; + current_play_state = ServerPlayState::Stopped; + src_quality = SrcQuality::Medium; + priority = LowestVoicePriority; + sample_format = SampleFormat::Invalid; + sample_rate = 0; + channel_count = 0; + wave_buffer_count = 0; + wave_buffer_index = 0; + pitch = 0.0f; + volume = 0.0f; + prev_volume = 0.0f; + mix_id = UnusedMixId; + splitter_id = UnusedSplitterId; + biquads = {}; + biquad_initialized = {}; + voice_dropped = false; + data_unmapped = false; + buffer_unmapped = false; + flush_buffer_count = 0; + + data_address.Setup(0, 0); + for (auto& wavebuffer : wavebuffers) { + wavebuffer.Initialize(); + } +} + +bool VoiceInfo::ShouldUpdateParameters(const InParameter& params) const { + return data_address.GetCpuAddr() != params.src_data_address || + data_address.GetSize() != params.src_data_size || data_unmapped; +} + +void VoiceInfo::UpdateParameters(BehaviorInfo::ErrorInfo& error_info, const InParameter& params, + const PoolMapper& pool_mapper, const BehaviorInfo& behavior) { + in_use = params.in_use; + id = params.id; + node_id = params.node_id; + UpdatePlayState(params.play_state); + UpdateSrcQuality(params.src_quality); + priority = params.priority; + sort_order = params.sort_order; + sample_rate = params.sample_rate; + sample_format = params.sample_format; + channel_count = static_cast(params.channel_count); + pitch = params.pitch; + volume = params.volume; + biquads = params.biquads; + wave_buffer_count = params.wave_buffer_count; + wave_buffer_index = params.wave_buffer_index; + + if (behavior.IsFlushVoiceWaveBuffersSupported()) { + flush_buffer_count += params.flush_buffer_count; + } + + mix_id = params.mix_id; + + if (behavior.IsSplitterSupported()) { + splitter_id = params.splitter_id; + } else { + splitter_id = UnusedSplitterId; + } + + channel_resource_ids = params.channel_resource_ids; + + flags &= u16(~0b11); + if (behavior.IsVoicePlayedSampleCountResetAtLoopPointSupported()) { + flags |= u16(params.flags.IsVoicePlayedSampleCountResetAtLoopPointSupported); + } + + if (behavior.IsVoicePitchAndSrcSkippedSupported()) { + flags |= u16(params.flags.IsVoicePitchAndSrcSkippedSupported); + } + + if (params.clear_voice_drop) { + voice_dropped = false; + } + + if (ShouldUpdateParameters(params)) { + data_unmapped = !pool_mapper.TryAttachBuffer(error_info, data_address, + params.src_data_address, params.src_data_size); + } else { + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + } +} + +void VoiceInfo::UpdatePlayState(const PlayState state) { + last_play_state = current_play_state; + + switch (state) { + case PlayState::Started: + current_play_state = ServerPlayState::Started; + break; + case PlayState::Stopped: + if (current_play_state != ServerPlayState::Stopped) { + current_play_state = ServerPlayState::RequestStop; + } + break; + case PlayState::Paused: + current_play_state = ServerPlayState::Paused; + break; + default: + LOG_ERROR(Service_Audio, "Invalid input play state {}", static_cast(state)); + break; + } +} + +void VoiceInfo::UpdateSrcQuality(const SrcQuality quality) { + switch (quality) { + case SrcQuality::Medium: + src_quality = quality; + break; + case SrcQuality::High: + src_quality = quality; + break; + case SrcQuality::Low: + src_quality = quality; + break; + default: + LOG_ERROR(Service_Audio, "Invalid input src quality {}", static_cast(quality)); + break; + } +} + +void VoiceInfo::UpdateWaveBuffers(std::span> error_infos, + [[maybe_unused]] u32 error_count, const InParameter& params, + std::span voice_states, + const PoolMapper& pool_mapper, const BehaviorInfo& behavior) { + if (params.is_new) { + for (size_t i = 0; i < wavebuffers.size(); i++) { + wavebuffers[i].Initialize(); + } + + for (s8 channel = 0; channel < static_cast(params.channel_count); channel++) { + voice_states[channel]->wave_buffer_valid.fill(false); + } + } + + for (u32 i = 0; i < MaxWaveBuffers; i++) { + UpdateWaveBuffer(error_infos[i], wavebuffers[i], params.wave_buffer_internal[i], + params.sample_format, voice_states[0]->wave_buffer_valid[i], pool_mapper, + behavior); + } +} + +void VoiceInfo::UpdateWaveBuffer(std::span error_info, + WaveBuffer& wave_buffer, + const WaveBufferInternal& wave_buffer_internal, + const SampleFormat sample_format_, const bool valid, + const PoolMapper& pool_mapper, const BehaviorInfo& behavior) { + if (!valid && wave_buffer.sent_to_DSP && wave_buffer.buffer_address.GetCpuAddr() != 0) { + pool_mapper.ForceUnmapPointer(wave_buffer.buffer_address); + wave_buffer.buffer_address.Setup(0, 0); + } + + if (!ShouldUpdateWaveBuffer(wave_buffer_internal)) { + return; + } + + switch (sample_format_) { + case SampleFormat::PcmInt16: { + constexpr auto byte_size{GetSampleFormatByteSize(SampleFormat::PcmInt16)}; + if (wave_buffer_internal.start_offset * byte_size > wave_buffer_internal.size || + wave_buffer_internal.end_offset * byte_size > wave_buffer_internal.size) { + LOG_ERROR(Service_Audio, "Invalid PCM16 start/end wavebuffer sizes!"); + error_info[0].error_code = Service::Audio::ERR_INVALID_UPDATE_DATA; + error_info[0].address = wave_buffer_internal.address; + return; + } + } break; + + case SampleFormat::PcmFloat: { + constexpr auto byte_size{GetSampleFormatByteSize(SampleFormat::PcmFloat)}; + if (wave_buffer_internal.start_offset * byte_size > wave_buffer_internal.size || + wave_buffer_internal.end_offset * byte_size > wave_buffer_internal.size) { + LOG_ERROR(Service_Audio, "Invalid PCMFloat start/end wavebuffer sizes!"); + error_info[0].error_code = Service::Audio::ERR_INVALID_UPDATE_DATA; + error_info[0].address = wave_buffer_internal.address; + return; + } + } break; + + case SampleFormat::Adpcm: { + const auto start_frame{wave_buffer_internal.start_offset / 14}; + auto start_extra{wave_buffer_internal.start_offset % 14 == 0 + ? 0 + : (wave_buffer_internal.start_offset % 14) / 2 + 1 + + ((wave_buffer_internal.start_offset % 14) % 2)}; + const auto start{start_frame * 8 + start_extra}; + + const auto end_frame{wave_buffer_internal.end_offset / 14}; + const auto end_extra{wave_buffer_internal.end_offset % 14 == 0 + ? 0 + : (wave_buffer_internal.end_offset % 14) / 2 + 1 + + ((wave_buffer_internal.end_offset % 14) % 2)}; + const auto end{end_frame * 8 + end_extra}; + + if (start > static_cast(wave_buffer_internal.size) || + end > static_cast(wave_buffer_internal.size)) { + LOG_ERROR(Service_Audio, "Invalid ADPCM start/end wavebuffer sizes!"); + error_info[0].error_code = Service::Audio::ERR_INVALID_UPDATE_DATA; + error_info[0].address = wave_buffer_internal.address; + return; + } + } break; + + default: + break; + } + + if (wave_buffer_internal.start_offset < 0 || wave_buffer_internal.end_offset < 0) { + LOG_ERROR(Service_Audio, "Invalid input start/end wavebuffer sizes!"); + error_info[0].error_code = Service::Audio::ERR_INVALID_UPDATE_DATA; + error_info[0].address = wave_buffer_internal.address; + return; + } + + wave_buffer.start_offset = wave_buffer_internal.start_offset; + wave_buffer.end_offset = wave_buffer_internal.end_offset; + wave_buffer.loop = wave_buffer_internal.loop; + wave_buffer.stream_ended = wave_buffer_internal.stream_ended; + wave_buffer.sent_to_DSP = false; + wave_buffer.loop_start_offset = wave_buffer_internal.loop_start; + wave_buffer.loop_end_offset = wave_buffer_internal.loop_end; + wave_buffer.loop_count = wave_buffer_internal.loop_count; + + buffer_unmapped = + !pool_mapper.TryAttachBuffer(error_info[0], wave_buffer.buffer_address, + wave_buffer_internal.address, wave_buffer_internal.size); + + if (sample_format_ == SampleFormat::Adpcm && behavior.IsAdpcmLoopContextBugFixed() && + wave_buffer_internal.context_address != 0) { + buffer_unmapped = !pool_mapper.TryAttachBuffer(error_info[1], wave_buffer.context_address, + wave_buffer_internal.context_address, + wave_buffer_internal.context_size) || + data_unmapped; + } else { + wave_buffer.context_address.Setup(0, 0); + } +} + +bool VoiceInfo::ShouldUpdateWaveBuffer(const WaveBufferInternal& wave_buffer_internal) const { + return !wave_buffer_internal.sent_to_DSP || buffer_unmapped; +} + +void VoiceInfo::WriteOutStatus(OutStatus& out_status, const InParameter& params, + std::span voice_states) { + if (params.is_new) { + is_new = true; + } + + if (params.is_new || is_new) { + out_status.played_sample_count = 0; + out_status.wave_buffers_consumed = 0; + out_status.voice_dropped = false; + } else { + out_status.played_sample_count = voice_states[0]->played_sample_count; + out_status.wave_buffers_consumed = voice_states[0]->wave_buffers_consumed; + out_status.voice_dropped = voice_dropped; + } +} + +bool VoiceInfo::ShouldSkip() const { + return !in_use || wave_buffer_count == 0 || data_unmapped || buffer_unmapped || voice_dropped; +} + +bool VoiceInfo::HasAnyConnection() const { + return mix_id != UnusedMixId || splitter_id != UnusedSplitterId; +} + +void VoiceInfo::FlushWaveBuffers(const u32 flush_count, std::span voice_states, + const s8 channel_count_) { + auto wave_index{wave_buffer_index}; + + for (size_t i = 0; i < flush_count; i++) { + wavebuffers[wave_index].sent_to_DSP = true; + + for (s8 j = 0; j < channel_count_; j++) { + auto voice_state{voice_states[j]}; + if (voice_state->wave_buffer_index == wave_index) { + voice_state->wave_buffer_index = + (voice_state->wave_buffer_index + 1) % MaxWaveBuffers; + voice_state->wave_buffers_consumed++; + } + voice_state->wave_buffer_valid[wave_index] = false; + } + + wave_index = (wave_index + 1) % MaxWaveBuffers; + } +} + +bool VoiceInfo::UpdateParametersForCommandGeneration(std::span voice_states) { + if (flush_buffer_count > 0) { + FlushWaveBuffers(flush_buffer_count, voice_states, channel_count); + flush_buffer_count = 0; + } + + switch (current_play_state) { + case ServerPlayState::Started: + for (u32 i = 0; i < MaxWaveBuffers; i++) { + if (!wavebuffers[i].sent_to_DSP) { + for (s8 channel = 0; channel < channel_count; channel++) { + voice_states[channel]->wave_buffer_valid[i] = true; + } + wavebuffers[i].sent_to_DSP = true; + } + } + + was_playing = false; + + for (u32 i = 0; i < MaxWaveBuffers; i++) { + if (voice_states[0]->wave_buffer_valid[i]) { + return true; + } + } + break; + + case ServerPlayState::Stopped: + case ServerPlayState::Paused: + for (auto& wavebuffer : wavebuffers) { + if (!wavebuffer.sent_to_DSP) { + wavebuffer.buffer_address.GetReference(true); + wavebuffer.context_address.GetReference(true); + } + } + + if (sample_format == SampleFormat::Adpcm && data_address.GetCpuAddr() != 0) { + data_address.GetReference(true); + } + + was_playing = last_play_state == ServerPlayState::Started; + break; + + case ServerPlayState::RequestStop: + for (u32 i = 0; i < MaxWaveBuffers; i++) { + wavebuffers[i].sent_to_DSP = true; + + for (s8 channel = 0; channel < channel_count; channel++) { + if (voice_states[channel]->wave_buffer_valid[i]) { + voice_states[channel]->wave_buffer_index = + (voice_states[channel]->wave_buffer_index + 1) % MaxWaveBuffers; + voice_states[channel]->wave_buffers_consumed++; + } + voice_states[channel]->wave_buffer_valid[i] = false; + } + } + + for (s8 channel = 0; channel < channel_count; channel++) { + voice_states[channel]->offset = 0; + voice_states[channel]->played_sample_count = 0; + voice_states[channel]->adpcm_context = {}; + voice_states[channel]->sample_history.fill(0); + voice_states[channel]->fraction = 0; + } + + current_play_state = ServerPlayState::Stopped; + was_playing = last_play_state == ServerPlayState::Started; + break; + } + + return was_playing; +} + +bool VoiceInfo::UpdateForCommandGeneration(VoiceContext& voice_context) { + std::array voice_states{}; + + if (is_new) { + ResetResources(voice_context); + prev_volume = volume; + is_new = false; + } + + for (s8 channel = 0; channel < channel_count; channel++) { + voice_states[channel] = &voice_context.GetDspSharedState(channel_resource_ids[channel]); + } + + return UpdateParametersForCommandGeneration(voice_states); +} + +void VoiceInfo::ResetResources(VoiceContext& voice_context) const { + for (s8 channel = 0; channel < channel_count; channel++) { + auto& state{voice_context.GetDspSharedState(channel_resource_ids[channel])}; + state = {}; + + auto& channel_resource{voice_context.GetChannelResource(channel_resource_ids[channel])}; + channel_resource.prev_mix_volumes = channel_resource.mix_volumes; + } +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/voice/voice_info.h b/src/audio_core/renderer/voice/voice_info.h new file mode 100755 index 000000000..896723e0c --- /dev/null +++ b/src/audio_core/renderer/voice/voice_info.h @@ -0,0 +1,378 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/common/common.h" +#include "audio_core/common/wave_buffer.h" +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/memory/address_info.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +class PoolMapper; +class VoiceContext; +struct VoiceState; + +/** + * Represents one voice. Voices are essentially noises, and they can be further mixed and have + * effects applied to them, but voices are the basis of all sounds. + */ +class VoiceInfo { +public: + enum class ServerPlayState { + Started, + Stopped, + RequestStop, + Paused, + }; + + struct Flags { + u8 IsVoicePlayedSampleCountResetAtLoopPointSupported : 1; + u8 IsVoicePitchAndSrcSkippedSupported : 1; + }; + + /** + * A wavebuffer contains information on the data source buffers. + */ + struct WaveBuffer { + void Copy(WaveBufferVersion1& other) { + other.buffer = buffer_address.GetReference(true); + other.buffer_size = buffer_address.GetSize(); + other.start_offset = start_offset; + other.end_offset = end_offset; + other.loop = loop; + other.stream_ended = stream_ended; + + if (context_address.GetCpuAddr()) { + other.context = context_address.GetReference(true); + other.context_size = context_address.GetSize(); + } else { + other.context = CpuAddr(0); + other.context_size = 0; + } + } + + void Copy(WaveBufferVersion2& other) { + other.buffer = buffer_address.GetReference(true); + other.buffer_size = buffer_address.GetSize(); + other.start_offset = start_offset; + other.end_offset = end_offset; + other.loop_start_offset = loop_start_offset; + other.loop_end_offset = loop_end_offset; + other.loop = loop; + other.loop_count = loop_count; + other.stream_ended = stream_ended; + + if (context_address.GetCpuAddr()) { + other.context = context_address.GetReference(true); + other.context_size = context_address.GetSize(); + } else { + other.context = CpuAddr(0); + other.context_size = 0; + } + } + + void Initialize() { + buffer_address.Setup(0, 0); + context_address.Setup(0, 0); + start_offset = 0; + end_offset = 0; + loop = false; + stream_ended = false; + sent_to_DSP = true; + loop_start_offset = 0; + loop_end_offset = 0; + loop_count = 0; + } + /// Game memory address of the wavebuffer data + AddressInfo buffer_address{0, 0}; + /// Context for decoding, used for ADPCM + AddressInfo context_address{0, 0}; + /// Starting offset for the wavebuffer + u32 start_offset{}; + /// Ending offset the wavebuffer + u32 end_offset{}; + /// Should this wavebuffer loop? + bool loop{}; + /// Has this wavebuffer ended? + bool stream_ended{}; + /// Has this wavebuffer been sent to the AudioRenderer? + bool sent_to_DSP{true}; + /// Starting offset when looping, can differ from start_offset + u32 loop_start_offset{}; + /// Ending offset when looping, can differ from end_offset + u32 loop_end_offset{}; + /// Number of times to loop this wavebuffer + s32 loop_count{}; + }; + + struct WaveBufferInternal { + /* 0x00 */ CpuAddr address; + /* 0x08 */ u64 size; + /* 0x10 */ s32 start_offset; + /* 0x14 */ s32 end_offset; + /* 0x18 */ bool loop; + /* 0x19 */ bool stream_ended; + /* 0x1A */ bool sent_to_DSP; + /* 0x1C */ s32 loop_count; + /* 0x20 */ CpuAddr context_address; + /* 0x28 */ u64 context_size; + /* 0x30 */ u32 loop_start; + /* 0x34 */ u32 loop_end; + }; + static_assert(sizeof(WaveBufferInternal) == 0x38, + "VoiceInfo::WaveBufferInternal has the wrong size!"); + + struct BiquadFilterParameter { + /* 0x00 */ bool enabled; + /* 0x02 */ std::array b; + /* 0x08 */ std::array a; + }; + static_assert(sizeof(BiquadFilterParameter) == 0xC, + "VoiceInfo::BiquadFilterParameter has the wrong size!"); + + struct InParameter { + /* 0x000 */ u32 id; + /* 0x004 */ u32 node_id; + /* 0x008 */ bool is_new; + /* 0x009 */ bool in_use; + /* 0x00A */ PlayState play_state; + /* 0x00B */ SampleFormat sample_format; + /* 0x00C */ u32 sample_rate; + /* 0x010 */ s32 priority; + /* 0x014 */ s32 sort_order; + /* 0x018 */ u32 channel_count; + /* 0x01C */ f32 pitch; + /* 0x020 */ f32 volume; + /* 0x024 */ std::array biquads; + /* 0x03C */ u32 wave_buffer_count; + /* 0x040 */ u16 wave_buffer_index; + /* 0x042 */ char unk042[0x6]; + /* 0x048 */ CpuAddr src_data_address; + /* 0x050 */ u64 src_data_size; + /* 0x058 */ u32 mix_id; + /* 0x05C */ u32 splitter_id; + /* 0x060 */ std::array wave_buffer_internal; + /* 0x140 */ std::array channel_resource_ids; + /* 0x158 */ bool clear_voice_drop; + /* 0x159 */ u8 flush_buffer_count; + /* 0x15A */ char unk15A[0x2]; + /* 0x15C */ Flags flags; + /* 0x15D */ char unk15D[0x1]; + /* 0x15E */ SrcQuality src_quality; + /* 0x15F */ char unk15F[0x11]; + }; + static_assert(sizeof(InParameter) == 0x170, "VoiceInfo::InParameter has the wrong size!"); + + struct OutStatus { + /* 0x00 */ u64 played_sample_count; + /* 0x08 */ u32 wave_buffers_consumed; + /* 0x0C */ bool voice_dropped; + }; + static_assert(sizeof(OutStatus) == 0x10, "OutStatus::InParameter has the wrong size!"); + + VoiceInfo(); + + /** + * Initialize this voice. + */ + void Initialize(); + + /** + * Does this voice ned an update? + * + * @param params - Input parametetrs to check matching. + * @return True if this voice needs an update, otherwise false. + */ + bool ShouldUpdateParameters(const InParameter& params) const; + + /** + * Update the parameters of this voice. + * + * @param error_info - Output error code. + * @param params - Input parametters to udpate from. + * @param pool_mapper - Used to map buffers. + * @param behavior - behavior to check supported features. + */ + void UpdateParameters(BehaviorInfo::ErrorInfo& error_info, const InParameter& params, + const PoolMapper& pool_mapper, const BehaviorInfo& behavior); + + /** + * Update the current play state. + * + * @param state - New play state for this voice. + */ + void UpdatePlayState(PlayState state); + + /** + * Update the current sample rate conversion quality. + * + * @param quality - New quality. + */ + void UpdateSrcQuality(SrcQuality quality); + + /** + * Update all wavebuffers. + * + * @param error_infos - Output 2D array of errors, 2 per wavebuffer. + * @param error_count - Number of errors provided. Unused. + * @param params - Input parametters to be used for the update. + * @param voice_states - The voice states for each channel in this voice to be updated. + * @param pool_mapper - Used to map the wavebuffers. + * @param behavior - Used to check for supported features. + */ + void UpdateWaveBuffers(std::span> error_infos, + u32 error_count, const InParameter& params, + std::span voice_states, const PoolMapper& pool_mapper, + const BehaviorInfo& behavior); + + /** + * Update a wavebuffer. + * + * @param error_infos - Output array of errors. + * @param wave_buffer - The wavebuffer to be updated. + * @param wave_buffer_internal - Input parametters to be used for the update. + * @param sample_format - Sample format of the wavebuffer. + * @param valid - Is this wavebuffer valid? + * @param pool_mapper - Used to map the wavebuffers. + * @param behavior - Used to check for supported features. + */ + void UpdateWaveBuffer(std::span error_info, WaveBuffer& wave_buffer, + const WaveBufferInternal& wave_buffer_internal, + SampleFormat sample_format, bool valid, const PoolMapper& pool_mapper, + const BehaviorInfo& behavior); + + /** + * Check if the input wavebuffer needs an update. + * + * @param wave_buffer_internal - Input wavebuffer parameters to check. + * @return True if the given wavebuffer needs an update, otherwise false. + */ + bool ShouldUpdateWaveBuffer(const WaveBufferInternal& wave_buffer_internal) const; + + /** + * Write the number of played samples, number of consumed wavebuffers and if this voice was + * dropped, to the given out_status. + * + * @param out_status - Output status to be written to. + * @param in_params - Input parameters to check if the wavebuffer is new. + * @param voice_states - Current host voice states for this voice, source of the output. + */ + void WriteOutStatus(OutStatus& out_status, const InParameter& in_params, + std::span voice_states); + + /** + * Check if this voice should be skipped for command generation. + * Checks various things such as usage state, whether data is mapped etc. + * + * @return True if this voice should not be generated, otherwise false. + */ + bool ShouldSkip() const; + + /** + * Check if this voice has any mixing connections. + * + * @return True if this voice participes in mixing, otherwise false. + */ + bool HasAnyConnection() const; + + /** + * Flush flush_count wavebuffers, marking them as consumed. + * + * @param flush_count - Number of wavebuffers to flush. + * @param voice_states - Voice states for these wavebuffers. + * @param channel_count - Number of active channels. + */ + void FlushWaveBuffers(u32 flush_count, std::span voice_states, s8 channel_count); + + /** + * Update this voice's parameters on command generation, + * updating voice states and flushing if needed. + * + * @param voice_states - Voice states for these wavebuffers. + * @return True if this voice should be generated, otherwise false. + */ + bool UpdateParametersForCommandGeneration(std::span voice_states); + + /** + * Update this voice on command generation. + * + * @param voice_states - Voice states for these wavebuffers. + * @return True if this voice should be generated, otherwise false. + */ + bool UpdateForCommandGeneration(VoiceContext& voice_context); + + /** + * Reset the AudioRenderer-side voice states, and the channel resources for this voice. + * + * @param voice_context - Context from which to get the resources. + */ + void ResetResources(VoiceContext& voice_context) const; + + /// Is this voice in use? + bool in_use{}; + /// Is this voice new? + bool is_new{}; + /// Was this voice last playing? Used for depopping + bool was_playing{}; + /// Sample format of the wavebuffers in this voice + SampleFormat sample_format{}; + /// Sample rate of the wavebuffers in this voice + u32 sample_rate{}; + /// Number of channels in this voice + s8 channel_count{}; + /// Id of this voice + u32 id{}; + /// Node id of this voice + u32 node_id{}; + /// Mix id this voice is mixed to + u32 mix_id{}; + /// Play state of this voice + ServerPlayState current_play_state{ServerPlayState::Stopped}; + /// Last play state of this voice + ServerPlayState last_play_state{ServerPlayState::Started}; + /// Priority of this voice, lower is higher + s32 priority{}; + /// Sort order of this voice, used when same priority + s32 sort_order{}; + /// Pitch of this voice (for sample rate conversion) + f32 pitch{}; + /// Current volume of this voice + f32 volume{}; + /// Previous volume of this voice + f32 prev_volume{}; + /// Biquad filters for generating filter commands on this voice + std::array biquads{}; + /// Number of active wavebuffers + u32 wave_buffer_count{}; + /// Current playing wavebuffer index + u16 wave_buffer_index{}; + /// Flags controlling decode behavior + u16 flags{}; + /// Game memory for ADPCM coefficients + AddressInfo data_address{0, 0}; + /// Wavebuffers + std::array wavebuffers{}; + /// Channel resources for this voice + std::array channel_resource_ids{}; + /// Splitter id this voice is connected with + s32 splitter_id{UnusedSplitterId}; + /// Sample rate conversion quality + SrcQuality src_quality{SrcQuality::Medium}; + /// Was this voice dropped due to limited time? + bool voice_dropped{}; + /// Is this voice's coefficient (data_address) unmapped? + bool data_unmapped{}; + /// Is this voice's buffers (wavebuffer data and ADPCM context) unmapped? + bool buffer_unmapped{}; + /// Initialisation state of the biquads + std::array biquad_initialized{}; + /// Number of wavebuffers to flush + u8 flush_buffer_count{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/voice/voice_state.h b/src/audio_core/renderer/voice/voice_state.h new file mode 100755 index 000000000..d5497e2fb --- /dev/null +++ b/src/audio_core/renderer/voice/voice_state.h @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "common/common_types.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { +/** + * Holds a state for a voice. One is kept host-side, and one is used by the AudioRenderer, + * host-side is updated on the next iteration. + */ +struct VoiceState { + /** + * State of the voice's biquad filter. + */ + struct BiquadFilterState { + Common::FixedPoint<50, 14> s0; + Common::FixedPoint<50, 14> s1; + Common::FixedPoint<50, 14> s2; + Common::FixedPoint<50, 14> s3; + }; + + /** + * Context for ADPCM decoding. + */ + struct AdpcmContext { + u16 header; + s16 yn0; + s16 yn1; + }; + + /// Number of samples played + u64 played_sample_count; + /// Current offset from the starting offset + u32 offset; + /// Currently active wavebuffer index + u32 wave_buffer_index; + /// Array of which wavebuffers are currently valid + + std::array wave_buffer_valid; + /// Number of wavebuffers consumed, given back to the game + u32 wave_buffers_consumed; + /// History of samples, used for rate conversion + + std::array sample_history; + /// Current read fraction, used for resampling + Common::FixedPoint<49, 15> fraction; + /// Current adpcm context + AdpcmContext adpcm_context; + /// Current biquad states, used when filtering + + std::array, MaxBiquadFilters> biquad_states; + /// Previous samples + std::array previous_samples; + /// Unused + u32 external_context_size; + /// Unused + bool external_context_enabled; + /// Was this voice dropped? + bool voice_dropped; + /// Number of times the wavebuffer has looped + s32 loop_count; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/sink/cubeb_sink.cpp b/src/audio_core/sink/cubeb_sink.cpp new file mode 100755 index 000000000..56021a31a --- /dev/null +++ b/src/audio_core/sink/cubeb_sink.cpp @@ -0,0 +1,611 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include "audio_core/audio_core.h" +#include "audio_core/audio_event.h" +#include "audio_core/audio_manager.h" +#include "audio_core/sink/cubeb_sink.h" +#include "audio_core/sink/sink_stream.h" +#include "common/assert.h" +#include "common/fixed_point.h" +#include "common/logging/log.h" +#include "common/reader_writer_queue.h" +#include "common/ring_buffer.h" +#include "common/settings.h" +#include "core/core.h" + +#ifdef _WIN32 +#include +#undef CreateEvent +#endif + +namespace AudioCore::Sink { +/** + * Cubeb sink stream, responsible for sinking samples to hardware. + */ +class CubebSinkStream final : public SinkStream { +public: + /** + * Create a new sink stream. + * + * @param ctx_ - Cubeb context to create this stream with. + * @param device_channels_ - Number of channels supported by the hardware. + * @param system_channels_ - Number of channels the audio systems expect. + * @param output_device - Cubeb output device id. + * @param input_device - Cubeb input device id. + * @param name_ - Name of this stream. + * @param type_ - Type of this stream. + * @param system_ - Core system. + * @param event - Event used only for audio renderer, signalled on buffer consume. + */ + CubebSinkStream(cubeb* ctx_, const u32 device_channels_, const u32 system_channels_, + cubeb_devid output_device, cubeb_devid input_device, const std::string& name_, + const StreamType type_, Core::System& system_, Common::Event* event) + : ctx{ctx_}, type{type_}, system{system_}, render_event{event} { +#ifdef _WIN32 + CoInitializeEx(nullptr, COINIT_MULTITHREADED); +#endif + name = name_; + device_channels = device_channels_; + system_channels = system_channels_; + + cubeb_stream_params params{}; + params.rate = TargetSampleRate; + params.channels = device_channels; + params.format = CUBEB_SAMPLE_S16LE; + params.prefs = CUBEB_STREAM_PREF_NONE; + switch (params.channels) { + case 1: + params.layout = CUBEB_LAYOUT_MONO; + break; + case 2: + params.layout = CUBEB_LAYOUT_STEREO; + break; + case 6: + params.layout = CUBEB_LAYOUT_3F2_LFE; + break; + } + + u32 minimum_latency{0}; + const auto latency_error = cubeb_get_min_latency(ctx, ¶ms, &minimum_latency); + if (latency_error != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "Error getting minimum latency, error: {}", latency_error); + minimum_latency = 256U; + } + + minimum_latency = std::max(minimum_latency, 256u); + + playing_buffer.consumed = true; + + LOG_DEBUG(Service_Audio, + "Opening cubeb stream {} type {} with: rate {} channels {} (system channels {}) " + "latency {}", + name, type, params.rate, params.channels, system_channels, minimum_latency); + + auto init_error{0}; + if (type == StreamType::In) { + init_error = cubeb_stream_init(ctx, &stream_backend, name.c_str(), input_device, + ¶ms, output_device, nullptr, minimum_latency, + &CubebSinkStream::DataCallback, + &CubebSinkStream::StateCallback, this); + } else { + init_error = cubeb_stream_init(ctx, &stream_backend, name.c_str(), input_device, + nullptr, output_device, ¶ms, minimum_latency, + &CubebSinkStream::DataCallback, + &CubebSinkStream::StateCallback, this); + } + + if (init_error != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "Error initializing cubeb stream, error: {}", init_error); + return; + } + } + + /** + * Destroy the sink stream. + */ + ~CubebSinkStream() override { + LOG_DEBUG(Service_Audio, "Destructing cubeb stream {}", name); + + if (!ctx) { + return; + } + + Finalize(); + +#ifdef _WIN32 + CoUninitialize(); +#endif + } + + /** + * Finalize the sink stream. + */ + void Finalize() override { + Stop(); + cubeb_stream_destroy(stream_backend); + } + + /** + * Start the sink stream. + * + * @param resume - Set to true if this is resuming the stream a previously-active stream. + * Default false. + */ + void Start(const bool resume = false) override { + if (!ctx) { + return; + } + + if (resume && was_playing) { + if (cubeb_stream_start(stream_backend) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "Error starting cubeb stream"); + } + paused = false; + } else if (!resume) { + if (cubeb_stream_start(stream_backend) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "Error starting cubeb stream"); + } + paused = false; + } + } + + /** + * Stop the sink stream. + */ + void Stop() override { + if (!ctx) { + return; + } + + if (cubeb_stream_stop(stream_backend) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "Error stopping cubeb stream"); + } + + was_playing.store(!paused); + paused = true; + } + + /** + * Append a new buffer and its samples to a waiting queue to play. + * + * @param buffer - Audio buffer information to be queued. + * @param samples - The s16 samples to be queue for playback. + */ + void AppendBuffer(::AudioCore::Sink::SinkBuffer& buffer, std::vector& samples) override { + if (type == StreamType::In) { + queue.enqueue(buffer); + queued_buffers++; + } else { + static constexpr s32 min = std::numeric_limits::min(); + static constexpr s32 max = std::numeric_limits::max(); + + auto yuzu_volume{Settings::Volume()}; + auto volume{system_volume * device_volume * yuzu_volume}; + if (system_channels == 6 && device_channels == 2) { + std::array down_mix_coeff{1.0f, 0.707f, 0.251f, 0.707f}; + for (u32 read_index = 0, write_index = 0; read_index < samples.size(); + read_index += system_channels, write_index += device_channels) { + const auto left_sample = + ((Common::FixedPoint<49, 15>(samples[read_index + 0]) * down_mix_coeff[0] + + samples[read_index + 2] * down_mix_coeff[1] + + samples[read_index + 3] * down_mix_coeff[2] + + samples[read_index + 4] * down_mix_coeff[3]) * + volume) + .to_int(); + + const auto right_sample = + ((Common::FixedPoint<49, 15>(samples[read_index + 1]) * down_mix_coeff[0] + + samples[read_index + 2] * down_mix_coeff[1] + + samples[read_index + 3] * down_mix_coeff[2] + + samples[read_index + 5] * down_mix_coeff[3]) * + volume) + .to_int(); + + samples[write_index + 0] = static_cast(std::clamp(left_sample, min, max)); + samples[write_index + 1] = static_cast(std::clamp(right_sample, min, max)); + } + + samples.resize(samples.size() / system_channels * device_channels); + } else if (volume != 1.0f) { + for (u32 i = 0; i < samples.size(); i++) { + samples[i] = static_cast(std::clamp( + static_cast(static_cast(samples[i]) * volume), min, max)); + } + } + + samples_buffer.Push(samples); + queue.enqueue(buffer); + queued_buffers++; + } + } + + /** + * Release a buffer. Audio In only, will fill a buffer with recorded samples. + * + * @param num_samples - Maximum number of samples to receive. + * @return Vector of recorded samples. May have fewer than num_samples. + */ + std::vector ReleaseBuffer(const u64 num_samples) override { + static constexpr s32 min = std::numeric_limits::min(); + static constexpr s32 max = std::numeric_limits::max(); + + auto samples{samples_buffer.Pop(num_samples)}; + + // TODO: Up-mix to 6 channels if the game expects it. + // For audio input this is unlikely to ever be the case though. + + // Incoming mic volume seems to always be very quiet, so multiply by an additional 8 here. + // TODO: Play with this and find something that works better. + auto volume{system_volume * device_volume * 8}; + for (u32 i = 0; i < samples.size(); i++) { + samples[i] = static_cast( + std::clamp(static_cast(static_cast(samples[i]) * volume), min, max)); + } + + if (samples.size() < num_samples) { + samples.resize(num_samples, 0); + } + return samples; + } + + /** + * Check if a certain buffer has been consumed (fully played). + * + * @param tag - Unique tag of a buffer to check for. + * @return True if the buffer has been played, otherwise false. + */ + bool IsBufferConsumed(const u64 tag) override { + if (released_buffer.tag == 0) { + if (!released_buffers.try_dequeue(released_buffer)) { + return false; + } + } + + if (released_buffer.tag == tag) { + released_buffer.tag = 0; + return true; + } + return false; + } + + /** + * Empty out the buffer queue. + */ + void ClearQueue() override { + samples_buffer.Pop(); + while (queue.pop()) { + } + while (released_buffers.pop()) { + } + queued_buffers = 0; + released_buffer = {}; + playing_buffer = {}; + playing_buffer.consumed = true; + } + +private: + /** + * Signal events back to the audio system that a buffer was played/can be filled. + * + * @param buffer - Consumed audio buffer to be released. + */ + void SignalEvent(const ::AudioCore::Sink::SinkBuffer& buffer) { + auto& manager{system.AudioCore().GetAudioManager()}; + switch (type) { + case StreamType::Out: + released_buffers.enqueue(buffer); + manager.SetEvent(Event::Type::AudioOutManager, true); + break; + case StreamType::In: + released_buffers.enqueue(buffer); + manager.SetEvent(Event::Type::AudioInManager, true); + break; + case StreamType::Render: + render_event->Set(); + break; + } + } + + /** + * Main callback from Cubeb. Either expects samples from us (audio render/audio out), or will + * provide samples to be copied (audio in). + * + * @param stream - Cubeb-specific data about the stream. + * @param user_data - Custom data pointer passed along, points to a CubebSinkStream. + * @param in_buff - Input buffer to be used if the stream is an input type. + * @param out_buff - Output buffer to be used if the stream is an output type. + * @param num_frames_ - Number of frames of audio in the buffers. Note: Not number of samples. + */ + static long DataCallback([[maybe_unused]] cubeb_stream* stream, void* user_data, + [[maybe_unused]] const void* in_buff, void* out_buff, + long num_frames_) { + auto* impl = static_cast(user_data); + if (!impl) { + return -1; + } + + const std::size_t num_channels = impl->GetDeviceChannels(); + const std::size_t frame_size = num_channels; + const std::size_t frame_size_bytes = frame_size * sizeof(s16); + const std::size_t num_frames{static_cast(num_frames_)}; + size_t frames_written{0}; + [[maybe_unused]] bool underrun{false}; + + if (impl->type == StreamType::In) { + // INPUT + std::span input_buffer{reinterpret_cast(in_buff), + num_frames * frame_size}; + + while (frames_written < num_frames) { + auto& playing_buffer{impl->playing_buffer}; + + // If the playing buffer has been consumed or has no frames, we need a new one + if (playing_buffer.consumed || playing_buffer.frames == 0) { + if (!impl->queue.try_dequeue(impl->playing_buffer)) { + // If no buffer was available we've underrun, just push the samples and + // continue. + underrun = true; + impl->samples_buffer.Push(&input_buffer[frames_written * frame_size], + (num_frames - frames_written) * frame_size); + frames_written = num_frames; + continue; + } else { + // Successfully got a new buffer, mark the old one as consumed and signal. + impl->queued_buffers--; + impl->SignalEvent(impl->playing_buffer); + } + } + + // Get the minimum frames available between the currently playing buffer, and the + // amount we have left to fill + size_t frames_available{ + std::min(playing_buffer.frames - playing_buffer.frames_played, + num_frames - frames_written)}; + + impl->samples_buffer.Push(&input_buffer[frames_written * frame_size], + frames_available * frame_size); + + frames_written += frames_available; + playing_buffer.frames_played += frames_available; + + // If that's all the frames in the current buffer, add its samples and mark it as + // consumed + if (playing_buffer.frames_played >= playing_buffer.frames) { + impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels); + impl->playing_buffer.consumed = true; + } + } + + std::memcpy(&impl->last_frame[0], &input_buffer[(frames_written - 1) * frame_size], + frame_size_bytes); + } else { + // OUTPUT + std::span output_buffer{reinterpret_cast(out_buff), num_frames * frame_size}; + + while (frames_written < num_frames) { + auto& playing_buffer{impl->playing_buffer}; + + // If the playing buffer has been consumed or has no frames, we need a new one + if (playing_buffer.consumed || playing_buffer.frames == 0) { + if (!impl->queue.try_dequeue(impl->playing_buffer)) { + // If no buffer was available we've underrun, fill the remaining buffer with + // the last written frame and continue. + underrun = true; + for (size_t i = frames_written; i < num_frames; i++) { + std::memcpy(&output_buffer[i * frame_size], &impl->last_frame[0], + frame_size_bytes); + } + frames_written = num_frames; + continue; + } else { + // Successfully got a new buffer, mark the old one as consumed and signal. + impl->queued_buffers--; + impl->SignalEvent(impl->playing_buffer); + } + } + + // Get the minimum frames available between the currently playing buffer, and the + // amount we have left to fill + size_t frames_available{ + std::min(playing_buffer.frames - playing_buffer.frames_played, + num_frames - frames_written)}; + + impl->samples_buffer.Pop(&output_buffer[frames_written * frame_size], + frames_available * frame_size); + + frames_written += frames_available; + playing_buffer.frames_played += frames_available; + + // If that's all the frames in the current buffer, add its samples and mark it as + // consumed + if (playing_buffer.frames_played >= playing_buffer.frames) { + impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels); + impl->playing_buffer.consumed = true; + } + } + + std::memcpy(&impl->last_frame[0], &output_buffer[(frames_written - 1) * frame_size], + frame_size_bytes); + } + + return num_frames_; + } + + /** + * Cubeb callback for if a device state changes. Unused currently. + * + * @param stream - Cubeb-specific data about the stream. + * @param user_data - Custom data pointer passed along, points to a CubebSinkStream. + * @param state - New state of the device. + */ + static void StateCallback([[maybe_unused]] cubeb_stream* stream, + [[maybe_unused]] void* user_data, + [[maybe_unused]] cubeb_state state) {} + + /// Main Cubeb context + cubeb* ctx{}; + /// Cubeb stream backend + cubeb_stream* stream_backend{}; + /// Name of this stream + std::string name{}; + /// Type of this stream + StreamType type; + /// Core system + Core::System& system; + /// Ring buffer of the samples waiting to be played or consumed + Common::RingBuffer samples_buffer; + /// Audio buffers queued and waiting to play + Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> queue; + /// The currently-playing audio buffer + ::AudioCore::Sink::SinkBuffer playing_buffer{}; + /// Audio buffers which have been played and are in queue to be released by the audio system + Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> released_buffers{}; + /// Currently released buffer waiting to be taken by the audio system + ::AudioCore::Sink::SinkBuffer released_buffer{}; + /// The last played (or received) frame of audio, used when the callback underruns + std::array last_frame{}; + /// Audio render-only event, signalled when a render buffer is consumed + Common::Event* render_event; +}; + +CubebSink::CubebSink(std::string_view target_device_name) { + // Cubeb requires COM to be initialized on the thread calling cubeb_init on Windows +#ifdef _WIN32 + com_init_result = CoInitializeEx(nullptr, COINIT_MULTITHREADED); +#endif + + if (cubeb_init(&ctx, "yuzu", nullptr) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "cubeb_init failed"); + return; + } + + if (target_device_name != auto_device_name && !target_device_name.empty()) { + cubeb_device_collection collection; + if (cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection) != CUBEB_OK) { + LOG_WARNING(Audio_Sink, "Audio output device enumeration not supported"); + } else { + const auto collection_end{collection.device + collection.count}; + const auto device{ + std::find_if(collection.device, collection_end, [&](const cubeb_device_info& info) { + return info.friendly_name != nullptr && + target_device_name == std::string(info.friendly_name); + })}; + if (device != collection_end) { + output_device = device->devid; + } + cubeb_device_collection_destroy(ctx, &collection); + } + } + + cubeb_get_max_channel_count(ctx, &device_channels); + device_channels = std::clamp(device_channels, 2U, 6U); +} + +CubebSink::~CubebSink() { + if (!ctx) { + return; + } + + for (auto& sink_stream : sink_streams) { + sink_stream.reset(); + } + + cubeb_destroy(ctx); + +#ifdef _WIN32 + if (SUCCEEDED(com_init_result)) { + CoUninitialize(); + } +#endif +} + +SinkStream* CubebSink::AcquireSinkStream(Core::System& system, const u32 system_channels, + const std::string& name, const StreamType type, + Common::Event* event) { + SinkStreamPtr& stream = sink_streams.emplace_back( + std::make_unique(ctx, device_channels, system_channels, output_device, + input_device, name, type, system, event)); + + return stream.get(); +} + +void CubebSink::CloseStream(const SinkStream* stream) { + for (size_t i = 0; i < sink_streams.size(); i++) { + if (sink_streams[i].get() == stream) { + sink_streams[i].reset(); + sink_streams.erase(sink_streams.begin() + i); + break; + } + } +} + +void CubebSink::CloseStreams() { + sink_streams.clear(); +} + +void CubebSink::PauseStreams() { + for (auto& stream : sink_streams) { + stream->Stop(); + } +} + +void CubebSink::UnpauseStreams() { + for (auto& stream : sink_streams) { + stream->Start(true); + } +} + +f32 CubebSink::GetDeviceVolume() const { + if (sink_streams.empty()) { + return 1.0f; + } + + return sink_streams[0]->GetDeviceVolume(); +} + +void CubebSink::SetDeviceVolume(const f32 volume) { + for (auto& stream : sink_streams) { + stream->SetDeviceVolume(volume); + } +} + +void CubebSink::SetSystemVolume(const f32 volume) { + for (auto& stream : sink_streams) { + stream->SetSystemVolume(volume); + } +} + +std::vector ListCubebSinkDevices(const bool capture) { + std::vector device_list; + cubeb* ctx; + + if (cubeb_init(&ctx, "yuzu Device Enumerator", nullptr) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "cubeb_init failed"); + return {}; + } + + auto type{capture ? CUBEB_DEVICE_TYPE_INPUT : CUBEB_DEVICE_TYPE_OUTPUT}; + cubeb_device_collection collection; + if (cubeb_enumerate_devices(ctx, type, &collection) != CUBEB_OK) { + LOG_WARNING(Audio_Sink, "Audio output device enumeration not supported"); + } else { + for (std::size_t i = 0; i < collection.count; i++) { + const cubeb_device_info& device = collection.device[i]; + if (device.friendly_name && device.friendly_name[0] != '\0' && + device.state == CUBEB_DEVICE_STATE_ENABLED) { + device_list.emplace_back(device.friendly_name); + } + } + cubeb_device_collection_destroy(ctx, &collection); + } + + cubeb_destroy(ctx); + return device_list; +} + +} // namespace AudioCore::Sink diff --git a/src/audio_core/sink/cubeb_sink.h b/src/audio_core/sink/cubeb_sink.h new file mode 100755 index 000000000..aebdd2fc2 --- /dev/null +++ b/src/audio_core/sink/cubeb_sink.h @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include + +#include "audio_core/sink/sink.h" + +namespace Core { +class System; +} + +namespace AudioCore::Sink { +class SinkStream; + +/** + * Cubeb backend sink, holds multiple output streams and is responsible for sinking samples to + * hardware. Used by Audio Render, Audio In and Audio Out. + */ +class CubebSink final : public Sink { +public: + explicit CubebSink(std::string_view device_id); + ~CubebSink() override; + + /** + * Create a new sink stream. + * + * @param system - Core system. + * @param system_channels - Number of channels the audio system expects. + * May differ from the device's channel count. + * @param name - Name of this stream. + * @param type - Type of this stream, render/in/out. + * @param event - Audio render only, a signal used to prevent the renderer running too + * fast. + * @return A pointer to the created SinkStream + */ + SinkStream* AcquireSinkStream(Core::System& system, u32 system_channels, + const std::string& name, StreamType type, + Common::Event* event = nullptr) override; + + /** + * Close a given stream. + * + * @param stream - The stream to close. + */ + void CloseStream(const SinkStream* stream) override; + + /** + * Close all streams. + */ + void CloseStreams() override; + + /** + * Pause all streams. + */ + void PauseStreams() override; + + /** + * Unpause all streams. + */ + void UnpauseStreams() override; + + /** + * Get the device volume. Set from calls to the IAudioDevice service. + * + * @return Volume of the device. + */ + f32 GetDeviceVolume() const override; + + /** + * Set the device volume. Set from calls to the IAudioDevice service. + * + * @param volume - New volume of the device. + */ + void SetDeviceVolume(f32 volume) override; + + /** + * Set the system volume. Comes from the audio system using this stream. + * + * @param volume - New volume of the system. + */ + void SetSystemVolume(f32 volume) override; + +private: + /// Backend Cubeb context + cubeb* ctx{}; + /// Cubeb id of the actual hardware output device + cubeb_devid output_device{}; + /// Cubeb id of the actual hardware input device + cubeb_devid input_device{}; + /// Vector of streams managed by this sink + std::vector sink_streams{}; + +#ifdef _WIN32 + /// Cubeb required COM to be initialized multi-threaded on Windows + u32 com_init_result = 0; +#endif +}; + +/** + * Get a list of conencted devices from Cubeb. + * + * @param capture - Return input (capture) devices if true, otherwise output devices. + */ +std::vector ListCubebSinkDevices(bool capture); + +} // namespace AudioCore::Sink diff --git a/src/audio_core/sink/null_sink.h b/src/audio_core/sink/null_sink.h new file mode 100755 index 000000000..340db311e --- /dev/null +++ b/src/audio_core/sink/null_sink.h @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/sink/sink.h" +#include "audio_core/sink/sink_stream.h" + +namespace AudioCore::Sink { +/** + * A no-op sink for when no audio out is wanted. + */ +class NullSink final : public Sink { +public: + explicit NullSink(std::string_view) {} + ~NullSink() override = default; + + SinkStream* AcquireSinkStream([[maybe_unused]] Core::System& system, + [[maybe_unused]] u32 system_channels, + [[maybe_unused]] const std::string& name, + [[maybe_unused]] StreamType type, + [[maybe_unused]] Common::Event* event = nullptr) override { + return &null_sink_stream; + } + + void CloseStream([[maybe_unused]] const SinkStream* stream) override {} + void CloseStreams() override {} + void PauseStreams() override {} + void UnpauseStreams() override {} + f32 GetDeviceVolume() const override { + return 1.0f; + } + void SetDeviceVolume(f32 volume) override {} + void SetSystemVolume(f32 volume) override {} + +private: + struct NullSinkStreamImpl final : SinkStream { + void Finalize() override {} + void Start(bool resume = false) override {} + void Stop() override {} + void AppendBuffer([[maybe_unused]] ::AudioCore::Sink::SinkBuffer& buffer, + [[maybe_unused]] std::vector& samples) override {} + std::vector ReleaseBuffer([[maybe_unused]] u64 num_samples) override { + return {}; + } + bool IsBufferConsumed([[maybe_unused]] const u64 tag) { + return true; + } + void ClearQueue() override {} + } null_sink_stream; +}; + +} // namespace AudioCore::Sink diff --git a/src/audio_core/sink/sdl2_sink.cpp b/src/audio_core/sink/sdl2_sink.cpp new file mode 100755 index 000000000..3ad9fab56 --- /dev/null +++ b/src/audio_core/sink/sdl2_sink.cpp @@ -0,0 +1,515 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "audio_core/audio_core.h" +#include "audio_core/audio_event.h" +#include "audio_core/audio_manager.h" +#include "audio_core/sink/sdl2_sink.h" +#include "audio_core/sink/sink_stream.h" +#include "common/assert.h" +#include "common/fixed_point.h" +#include "common/logging/log.h" +#include "common/reader_writer_queue.h" +#include "common/ring_buffer.h" +#include "common/settings.h" +#include "core/core.h" + +// Ignore -Wimplicit-fallthrough due to https://github.com/libsdl-org/SDL/issues/4307 +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wimplicit-fallthrough" +#endif +#include +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +namespace AudioCore::Sink { +/** + * SDL sink stream, responsible for sinking samples to hardware. + */ +class SDLSinkStream final : public SinkStream { +public: + /** + * Create a new sink stream. + * + * @param device_channels_ - Number of channels supported by the hardware. + * @param system_channels_ - Number of channels the audio systems expect. + * @param output_device - Name of the output device to use for this stream. + * @param input_device - Name of the input device to use for this stream. + * @param type_ - Type of this stream. + * @param system_ - Core system. + * @param event - Event used only for audio renderer, signalled on buffer consume. + */ + SDLSinkStream(u32 device_channels_, const u32 system_channels_, + const std::string& output_device, const std::string& input_device, + const StreamType type_, Core::System& system_, Common::Event* event) + : type{type_}, system{system_}, render_event{event} { + system_channels = system_channels_; + device_channels = device_channels_; + + SDL_AudioSpec spec; + spec.freq = TargetSampleRate; + spec.channels = static_cast(device_channels); + spec.format = AUDIO_S16SYS; + if (type == StreamType::Render) { + spec.samples = TargetSampleCount; + } else { + spec.samples = 1024; + } + spec.callback = &SDLSinkStream::DataCallback; + spec.userdata = this; + + playing_buffer.consumed = true; + + std::string device_name{output_device}; + bool capture{false}; + if (type == StreamType::In) { + device_name = input_device; + capture = true; + } + + SDL_AudioSpec obtained; + if (device_name.empty()) { + device = SDL_OpenAudioDevice(nullptr, capture, &spec, &obtained, false); + } else { + device = SDL_OpenAudioDevice(device_name.c_str(), capture, &spec, &obtained, false); + } + + if (device == 0) { + LOG_CRITICAL(Audio_Sink, "Error opening SDL audio device: {}", SDL_GetError()); + return; + } + + LOG_DEBUG(Service_Audio, + "Opening sdl stream {} with: rate {} channels {} (system channels {}) " + " samples {}", + device, obtained.freq, obtained.channels, system_channels, obtained.samples); + } + + /** + * Destroy the sink stream. + */ + ~SDLSinkStream() override { + if (device == 0) { + return; + } + + SDL_CloseAudioDevice(device); + } + + /** + * Finalize the sink stream. + */ + void Finalize() override { + if (device == 0) { + return; + } + + SDL_CloseAudioDevice(device); + } + + /** + * Start the sink stream. + * + * @param resume - Set to true if this is resuming the stream a previously-active stream. + * Default false. + */ + void Start(const bool resume = false) override { + if (device == 0) { + return; + } + + if (resume && was_playing) { + SDL_PauseAudioDevice(device, 0); + paused = false; + } else if (!resume) { + SDL_PauseAudioDevice(device, 0); + paused = false; + } + } + + /** + * Stop the sink stream. + */ + void Stop() { + if (device == 0) { + return; + } + SDL_PauseAudioDevice(device, 1); + paused = true; + } + + /** + * Append a new buffer and its samples to a waiting queue to play. + * + * @param buffer - Audio buffer information to be queued. + * @param samples - The s16 samples to be queue for playback. + */ + void AppendBuffer(::AudioCore::Sink::SinkBuffer& buffer, std::vector& samples) override { + if (type == StreamType::In) { + queue.enqueue(buffer); + queued_buffers++; + } else { + static constexpr s32 min = std::numeric_limits::min(); + static constexpr s32 max = std::numeric_limits::max(); + + auto yuzu_volume{Settings::Volume()}; + auto volume{system_volume * device_volume * yuzu_volume}; + if (system_channels == 6 && device_channels == 2) { + std::array down_mix_coeff{1.0f, 0.707f, 0.251f, 0.707f}; + for (u32 read_index = 0, write_index = 0; read_index < samples.size(); + read_index += system_channels, write_index += device_channels) { + const auto left_sample = + ((Common::FixedPoint<49, 15>(samples[read_index + 0]) * down_mix_coeff[0] + + samples[read_index + 2] * down_mix_coeff[1] + + samples[read_index + 3] * down_mix_coeff[2] + + samples[read_index + 4] * down_mix_coeff[3]) * + volume) + .to_int(); + + const auto right_sample = + ((Common::FixedPoint<49, 15>(samples[read_index + 1]) * down_mix_coeff[0] + + samples[read_index + 2] * down_mix_coeff[1] + + samples[read_index + 3] * down_mix_coeff[2] + + samples[read_index + 5] * down_mix_coeff[3]) * + volume) + .to_int(); + + samples[write_index + 0] = static_cast(std::clamp(left_sample, min, max)); + samples[write_index + 1] = static_cast(std::clamp(right_sample, min, max)); + } + + samples.resize(samples.size() / system_channels * device_channels); + } else if (volume != 1.0f) { + for (u32 i = 0; i < samples.size(); i++) { + auto sample{Common::FixedPoint<49, 15>(samples[i]) * volume}; + samples[i] = static_cast(std::clamp(sample.to_int(), min, max)); + } + } + + samples_buffer.Push(samples); + queue.enqueue(buffer); + queued_buffers++; + } + } + + /** + * Release a buffer. Audio In only, will fill a buffer with recorded samples. + * + * @param num_samples - Maximum number of samples to receive. + * @return Vector of recorded samples. May have fewer than num_samples. + */ + std::vector ReleaseBuffer(const u64 num_samples) override { + static constexpr s32 min = std::numeric_limits::min(); + static constexpr s32 max = std::numeric_limits::max(); + + auto samples{samples_buffer.Pop(num_samples)}; + + // TODO: Up-mix to 6 channels if the game expects it. + // For audio input this is unlikely to ever be the case though. + + // Incoming mic volume seems to always be very quiet, so multiply by an additional 8 here. + // TODO: Play with this and find something that works better. + auto volume{system_volume * device_volume * 8}; + for (u32 i = 0; i < samples.size(); i++) { + samples[i] = static_cast( + std::clamp(static_cast(static_cast(samples[i]) * volume), min, max)); + } + + if (samples.size() < num_samples) { + samples.resize(num_samples, 0); + } + return samples; + } + + /** + * Check if a certain buffer has been consumed (fully played). + * + * @param tag - Unique tag of a buffer to check for. + * @return True if the buffer has been played, otherwise false. + */ + bool IsBufferConsumed(const u64 tag) override { + if (released_buffer.tag == 0) { + if (!released_buffers.try_dequeue(released_buffer)) { + return false; + } + } + + if (released_buffer.tag == tag) { + released_buffer.tag = 0; + return true; + } + return false; + } + + /** + * Empty out the buffer queue. + */ + void ClearQueue() override { + samples_buffer.Pop(); + while (queue.pop()) { + } + while (released_buffers.pop()) { + } + released_buffer = {}; + playing_buffer = {}; + playing_buffer.consumed = true; + queued_buffers = 0; + } + +private: + /** + * Signal events back to the audio system that a buffer was played/can be filled. + * + * @param buffer - Consumed audio buffer to be released. + */ + void SignalEvent(const ::AudioCore::Sink::SinkBuffer& buffer) { + auto& manager{system.AudioCore().GetAudioManager()}; + switch (type) { + case StreamType::Out: + released_buffers.enqueue(buffer); + manager.SetEvent(Event::Type::AudioOutManager, true); + break; + case StreamType::In: + released_buffers.enqueue(buffer); + manager.SetEvent(Event::Type::AudioInManager, true); + break; + case StreamType::Render: + render_event->Set(); + break; + } + } + + /** + * Main callback from SDL. Either expects samples from us (audio render/audio out), or will + * provide samples to be copied (audio in). + * + * @param userdata - Custom data pointer passed along, points to a SDLSinkStream. + * @param stream - Buffer of samples to be filled or read. + * @param len - Length of the stream in bytes. + */ + static void DataCallback(void* userdata, Uint8* stream, int len) { + auto* impl = static_cast(userdata); + + if (!impl) { + return; + } + + const std::size_t num_channels = impl->GetDeviceChannels(); + const std::size_t frame_size = num_channels; + const std::size_t frame_size_bytes = frame_size * sizeof(s16); + const std::size_t num_frames{len / num_channels / sizeof(s16)}; + size_t frames_written{0}; + [[maybe_unused]] bool underrun{false}; + + if (impl->type == StreamType::In) { + std::span input_buffer{reinterpret_cast(stream), num_frames * frame_size}; + + while (frames_written < num_frames) { + auto& playing_buffer{impl->playing_buffer}; + + // If the playing buffer has been consumed or has no frames, we need a new one + if (playing_buffer.consumed || playing_buffer.frames == 0) { + if (!impl->queue.try_dequeue(impl->playing_buffer)) { + // If no buffer was available we've underrun, just push the samples and + // continue. + underrun = true; + impl->samples_buffer.Push(&input_buffer[frames_written * frame_size], + (num_frames - frames_written) * frame_size); + frames_written = num_frames; + continue; + } else { + impl->queued_buffers--; + impl->SignalEvent(impl->playing_buffer); + } + } + + // Get the minimum frames available between the currently playing buffer, and the + // amount we have left to fill + size_t frames_available{ + std::min(playing_buffer.frames - playing_buffer.frames_played, + num_frames - frames_written)}; + + impl->samples_buffer.Push(&input_buffer[frames_written * frame_size], + frames_available * frame_size); + + frames_written += frames_available; + playing_buffer.frames_played += frames_available; + + // If that's all the frames in the current buffer, add its samples and mark it as + // consumed + if (playing_buffer.frames_played >= playing_buffer.frames) { + impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels); + impl->playing_buffer.consumed = true; + } + } + + std::memcpy(&impl->last_frame[0], &input_buffer[(frames_written - 1) * frame_size], + frame_size_bytes); + } else { + std::span output_buffer{reinterpret_cast(stream), num_frames * frame_size}; + + while (frames_written < num_frames) { + auto& playing_buffer{impl->playing_buffer}; + + // If the playing buffer has been consumed or has no frames, we need a new one + if (playing_buffer.consumed || playing_buffer.frames == 0) { + if (!impl->queue.try_dequeue(impl->playing_buffer)) { + // If no buffer was available we've underrun, fill the remaining buffer with + // the last written frame and continue. + underrun = true; + for (size_t i = frames_written; i < num_frames; i++) { + std::memcpy(&output_buffer[i * frame_size], &impl->last_frame[0], + frame_size_bytes); + } + frames_written = num_frames; + continue; + } else { + impl->queued_buffers--; + impl->SignalEvent(impl->playing_buffer); + } + } + + // Get the minimum frames available between the currently playing buffer, and the + // amount we have left to fill + size_t frames_available{ + std::min(playing_buffer.frames - playing_buffer.frames_played, + num_frames - frames_written)}; + + impl->samples_buffer.Pop(&output_buffer[frames_written * frame_size], + frames_available * frame_size); + + frames_written += frames_available; + playing_buffer.frames_played += frames_available; + + // If that's all the frames in the current buffer, add its samples and mark it as + // consumed + if (playing_buffer.frames_played >= playing_buffer.frames) { + impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels); + impl->playing_buffer.consumed = true; + } + } + + std::memcpy(&impl->last_frame[0], &output_buffer[(frames_written - 1) * frame_size], + frame_size_bytes); + } + } + + /// SDL device id of the opened input/output device + SDL_AudioDeviceID device{}; + /// Type of this stream + StreamType type; + /// Core system + Core::System& system; + /// Ring buffer of the samples waiting to be played or consumed + Common::RingBuffer samples_buffer; + /// Audio buffers queued and waiting to play + Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> queue; + /// The currently-playing audio buffer + ::AudioCore::Sink::SinkBuffer playing_buffer{}; + /// Audio buffers which have been played and are in queue to be released by the audio system + Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> released_buffers{}; + /// Currently released buffer waiting to be taken by the audio system + ::AudioCore::Sink::SinkBuffer released_buffer{}; + /// The last played (or received) frame of audio, used when the callback underruns + std::array last_frame{}; + /// Audio render-only event, signalled when a render buffer is consumed + Common::Event* render_event; +}; + +SDLSink::SDLSink(std::string_view target_device_name) { + if (!SDL_WasInit(SDL_INIT_AUDIO)) { + if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { + LOG_CRITICAL(Audio_Sink, "SDL_InitSubSystem audio failed: {}", SDL_GetError()); + return; + } + } + + if (target_device_name != auto_device_name && !target_device_name.empty()) { + output_device = target_device_name; + } else { + output_device.clear(); + } + + device_channels = 2; +} + +SDLSink::~SDLSink() = default; + +SinkStream* SDLSink::AcquireSinkStream(Core::System& system, const u32 system_channels, + const std::string&, const StreamType type, + Common::Event* event) { + SinkStreamPtr& stream = sink_streams.emplace_back(std::make_unique( + device_channels, system_channels, output_device, input_device, type, system, event)); + return stream.get(); +} + +void SDLSink::CloseStream(const SinkStream* stream) { + for (size_t i = 0; i < sink_streams.size(); i++) { + if (sink_streams[i].get() == stream) { + sink_streams[i].reset(); + sink_streams.erase(sink_streams.begin() + i); + break; + } + } +} + +void SDLSink::CloseStreams() { + sink_streams.clear(); +} + +void SDLSink::PauseStreams() { + for (auto& stream : sink_streams) { + stream->Stop(); + } +} + +void SDLSink::UnpauseStreams() { + for (auto& stream : sink_streams) { + stream->Start(); + } +} + +f32 SDLSink::GetDeviceVolume() const { + if (sink_streams.empty()) { + return 1.0f; + } + + return sink_streams[0]->GetDeviceVolume(); +} + +void SDLSink::SetDeviceVolume(const f32 volume) { + for (auto& stream : sink_streams) { + stream->SetDeviceVolume(volume); + } +} + +void SDLSink::SetSystemVolume(const f32 volume) { + for (auto& stream : sink_streams) { + stream->SetSystemVolume(volume); + } +} + +std::vector ListSDLSinkDevices(const bool capture) { + std::vector device_list; + + if (!SDL_WasInit(SDL_INIT_AUDIO)) { + if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { + LOG_CRITICAL(Audio_Sink, "SDL_InitSubSystem audio failed: {}", SDL_GetError()); + return {}; + } + } + + const int device_count = SDL_GetNumAudioDevices(capture); + for (int i = 0; i < device_count; ++i) { + device_list.emplace_back(SDL_GetAudioDeviceName(i, 0)); + } + + return device_list; +} + +} // namespace AudioCore::Sink diff --git a/src/audio_core/sink/sdl2_sink.h b/src/audio_core/sink/sdl2_sink.h new file mode 100755 index 000000000..953807cbf --- /dev/null +++ b/src/audio_core/sink/sdl2_sink.h @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/sink/sink.h" + +namespace Core { +class System; +} + +namespace AudioCore::Sink { +class SinkStream; + +/** + * SDL backend sink, holds multiple output streams and is responsible for sinking samples to + * hardware. Used by Audio Render, Audio In and Audio Out. + */ +class SDLSink final : public Sink { +public: + explicit SDLSink(std::string_view device_id); + ~SDLSink() override; + + /** + * Create a new sink stream. + * + * @param system - Core system. + * @param system_channels - Number of channels the audio system expects. + * May differ from the device's channel count. + * @param name - Name of this stream. + * @param type - Type of this stream, render/in/out. + * @param event - Audio render only, a signal used to prevent the renderer running too + * fast. + * @return A pointer to the created SinkStream + */ + SinkStream* AcquireSinkStream(Core::System& system, u32 system_channels, + const std::string& name, StreamType type, + Common::Event* event = nullptr) override; + + /** + * Close a given stream. + * + * @param stream - The stream to close. + */ + void CloseStream(const SinkStream* stream) override; + + /** + * Close all streams. + */ + void CloseStreams() override; + + /** + * Pause all streams. + */ + void PauseStreams() override; + + /** + * Unpause all streams. + */ + void UnpauseStreams() override; + + /** + * Get the device volume. Set from calls to the IAudioDevice service. + * + * @return Volume of the device. + */ + f32 GetDeviceVolume() const override; + + /** + * Set the device volume. Set from calls to the IAudioDevice service. + * + * @param volume - New volume of the device. + */ + void SetDeviceVolume(f32 volume) override; + + /** + * Set the system volume. Comes from the audio system using this stream. + * + * @param volume - New volume of the system. + */ + void SetSystemVolume(f32 volume) override; + +private: + /// Name of the output device used by streams + std::string output_device; + /// Name of the input device used by streams + std::string input_device; + /// Vector of streams managed by this sink + std::vector sink_streams; +}; + +/** + * Get a list of conencted devices from Cubeb. + * + * @param capture - Return input (capture) devices if true, otherwise output devices. + */ +std::vector ListSDLSinkDevices(bool capture); + +} // namespace AudioCore::Sink diff --git a/src/audio_core/sink/sink.h b/src/audio_core/sink/sink.h new file mode 100755 index 000000000..d6b31e270 --- /dev/null +++ b/src/audio_core/sink/sink.h @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/sink/sink_stream.h" +#include "common/common_types.h" + +namespace Common { +class Event; +} +namespace Core { +class System; +} + +namespace AudioCore::Sink { + +constexpr char auto_device_name[] = "auto"; + +/** + * This class is an interface for an audio sink, holds multiple output streams and is responsible + * for sinking samples to hardware. Used by Audio Render, Audio In and Audio Out. + */ +class Sink { +public: + virtual ~Sink() = default; + /** + * Close a given stream. + * + * @param stream - The stream to close. + */ + virtual void CloseStream(const SinkStream* stream) = 0; + + /** + * Close all streams. + */ + virtual void CloseStreams() = 0; + + /** + * Pause all streams. + */ + virtual void PauseStreams() = 0; + + /** + * Unpause all streams. + */ + virtual void UnpauseStreams() = 0; + + /** + * Create a new sink stream, kept within this sink, with a pointer returned for use. + * Do not free the returned pointer. When done with the stream, call CloseStream on the sink. + * + * @param system - Core system. + * @param system_channels - Number of channels the audio system expects. + * May differ from the device's channel count. + * @param name - Name of this stream. + * @param type - Type of this stream, render/in/out. + * @param event - Audio render only, a signal used to prevent the renderer running too + * fast. + * @return A pointer to the created SinkStream + */ + virtual SinkStream* AcquireSinkStream(Core::System& system, u32 system_channels, + const std::string& name, StreamType type, + Common::Event* event = nullptr) = 0; + + /** + * Get the number of channels the hardware device supports. + * Either 2 or 6. + * + * @return Number of device channels. + */ + u32 GetDeviceChannels() const { + return device_channels; + } + + /** + * Get the device volume. Set from calls to the IAudioDevice service. + * + * @return Volume of the device. + */ + virtual f32 GetDeviceVolume() const = 0; + + /** + * Set the device volume. Set from calls to the IAudioDevice service. + * + * @param volume - New volume of the device. + */ + virtual void SetDeviceVolume(f32 volume) = 0; + + /** + * Set the system volume. Comes from the audio system using this stream. + * + * @param volume - New volume of the system. + */ + virtual void SetSystemVolume(f32 volume) = 0; + +protected: + /// Number of device channels supported by the hardware + u32 device_channels{2}; +}; + +using SinkPtr = std::unique_ptr; + +} // namespace AudioCore::Sink diff --git a/src/audio_core/sink/sink_details.cpp b/src/audio_core/sink/sink_details.cpp new file mode 100755 index 000000000..253c0fd1e --- /dev/null +++ b/src/audio_core/sink/sink_details.cpp @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include "audio_core/sink/null_sink.h" +#include "audio_core/sink/sink_details.h" +#ifdef HAVE_CUBEB +#include "audio_core/sink/cubeb_sink.h" +#endif +#ifdef HAVE_SDL2 +#include "audio_core/sink/sdl2_sink.h" +#endif +#include "common/logging/log.h" + +namespace AudioCore::Sink { +namespace { +struct SinkDetails { + using FactoryFn = std::unique_ptr (*)(std::string_view); + using ListDevicesFn = std::vector (*)(bool); + + /// Name for this sink. + const char* id; + /// A method to call to construct an instance of this type of sink. + FactoryFn factory; + /// A method to call to list available devices. + ListDevicesFn list_devices; +}; + +// sink_details is ordered in terms of desirability, with the best choice at the top. +constexpr SinkDetails sink_details[] = { +#ifdef HAVE_CUBEB + SinkDetails{"cubeb", + [](std::string_view device_id) -> std::unique_ptr { + return std::make_unique(device_id); + }, + &ListCubebSinkDevices}, +#endif +#ifdef HAVE_SDL2 + SinkDetails{"sdl2", + [](std::string_view device_id) -> std::unique_ptr { + return std::make_unique(device_id); + }, + &ListSDLSinkDevices}, +#endif + SinkDetails{"null", + [](std::string_view device_id) -> std::unique_ptr { + return std::make_unique(device_id); + }, + [](bool capture) { return std::vector{"null"}; }}, +}; + +const SinkDetails& GetOutputSinkDetails(std::string_view sink_id) { + auto iter = + std::find_if(std::begin(sink_details), std::end(sink_details), + [sink_id](const auto& sink_detail) { return sink_detail.id == sink_id; }); + + if (sink_id == "auto" || iter == std::end(sink_details)) { + if (sink_id != "auto") { + LOG_ERROR(Audio, "AudioCore::Sink::GetOutputSinkDetails given invalid sink_id {}", + sink_id); + } + // Auto-select. + // sink_details is ordered in terms of desirability, with the best choice at the front. + iter = std::begin(sink_details); + } + + return *iter; +} +} // Anonymous namespace + +std::vector GetSinkIDs() { + std::vector sink_ids(std::size(sink_details)); + + std::transform(std::begin(sink_details), std::end(sink_details), std::begin(sink_ids), + [](const auto& sink) { return sink.id; }); + + return sink_ids; +} + +std::vector GetDeviceListForSink(std::string_view sink_id, bool capture) { + return GetOutputSinkDetails(sink_id).list_devices(capture); +} + +std::unique_ptr CreateSinkFromID(std::string_view sink_id, std::string_view device_id) { + return GetOutputSinkDetails(sink_id).factory(device_id); +} + +} // namespace AudioCore::Sink diff --git a/src/audio_core/sink/sink_details.h b/src/audio_core/sink/sink_details.h new file mode 100755 index 000000000..3ebdb1e30 --- /dev/null +++ b/src/audio_core/sink/sink_details.h @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +namespace AudioCore { +class AudioManager; + +namespace Sink { + +class Sink; + +/** + * Retrieves the IDs for all available audio sinks. + * + * @return Vector of available sink names. + */ +std::vector GetSinkIDs(); + +/** + * Gets the list of devices for a particular sink identified by the given ID. + * + * @param sink_id - Id of the sink to get devices from. + * @param capture - Get capture (input) devices, or output devices? + * @return Vector of device names. + */ +std::vector GetDeviceListForSink(std::string_view sink_id, bool capture); + +/** + * Creates an audio sink identified by the given device ID. + * + * @param sink_id - Id of the sink to create. + * @param device_id - Name of the device to create. + * @return Pointer to the created sink. + */ +std::unique_ptr CreateSinkFromID(std::string_view sink_id, std::string_view device_id); + +} // namespace Sink +} // namespace AudioCore diff --git a/src/audio_core/sink/sink_stream.h b/src/audio_core/sink/sink_stream.h new file mode 100755 index 000000000..17ed6593f --- /dev/null +++ b/src/audio_core/sink/sink_stream.h @@ -0,0 +1,224 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "audio_core/common/common.h" +#include "common/common_types.h" + +namespace AudioCore::Sink { + +enum class StreamType { + Render, + Out, + In, +}; + +struct SinkBuffer { + u64 frames; + u64 frames_played; + u64 tag; + bool consumed; +}; + +/** + * Contains a real backend stream for outputting samples to hardware, + * created only via a Sink (See Sink::AcquireSinkStream). + * + * Accepts a SinkBuffer and samples in PCM16 format to be output (see AppendBuffer). + * Appended buffers act as a FIFO queue, and will be held until played. + * You should regularly call IsBufferConsumed with the unique SinkBuffer tag to check if the buffer + * has been consumed. + * + * Since these are a FIFO queue, always check IsBufferConsumed in the same order you appended the + * buffers, skipping a buffer will result in all following buffers to never release. + * + * If the buffers appear to be stuck, you can stop and re-open an IAudioIn/IAudioOut service (this + * is what games do), or call ClearQueue to flush all of the buffers without a full restart. + */ +class SinkStream { +public: + virtual ~SinkStream() = default; + + /** + * Finalize the sink stream. + */ + virtual void Finalize() = 0; + + /** + * Start the sink stream. + * + * @param resume - Set to true if this is resuming the stream a previously-active stream. + * Default false. + */ + virtual void Start(bool resume = false) = 0; + + /** + * Stop the sink stream. + */ + virtual void Stop() = 0; + + /** + * Append a new buffer and its samples to a waiting queue to play. + * + * @param buffer - Audio buffer information to be queued. + * @param samples - The s16 samples to be queue for playback. + */ + virtual void AppendBuffer(SinkBuffer& buffer, std::vector& samples) = 0; + + /** + * Release a buffer. Audio In only, will fill a buffer with recorded samples. + * + * @param num_samples - Maximum number of samples to receive. + * @return Vector of recorded samples. May have fewer than num_samples. + */ + virtual std::vector ReleaseBuffer(u64 num_samples) = 0; + + /** + * Check if a certain buffer has been consumed (fully played). + * + * @param tag - Unique tag of a buffer to check for. + * @return True if the buffer has been played, otherwise false. + */ + virtual bool IsBufferConsumed(u64 tag) = 0; + + /** + * Empty out the buffer queue. + */ + virtual void ClearQueue() = 0; + + /** + * Check if the stream is paused. + * + * @return True if paused, otherwise false. + */ + bool IsPaused() { + return paused; + } + + /** + * Get the number of system channels in this stream. + * + * @return Number of system channels. + */ + u32 GetSystemChannels() const { + return system_channels; + } + + /** + * Set the number of channels the system expects. + * + * @param channels - New number of system channels. + */ + void SetSystemChannels(u32 channels) { + system_channels = channels; + } + + /** + * Get the number of channels the hardware supports. + * + * @return Number of channels supported. + */ + u32 GetDeviceChannels() const { + return device_channels; + } + + /** + * Get the total number of samples played by this stream. + * + * @return Number of samples played. + */ + u64 GetPlayedSampleCount() const { + return played_sample_count; + } + + /** + * Set the number of samples played. + * This is started and stopped on system start/stop. + * + * @param played_sample_count_ - Number of samples to set. + */ + void SetPlayedSampleCount(u64 played_sample_count_) { + played_sample_count = played_sample_count_; + } + + /** + * Add to the played sample count. + * + * @param num_samples - Number of samples to add. + */ + void AddPlayedSampleCount(u64 num_samples) { + played_sample_count += num_samples; + } + + /** + * Get the system volume. + * + * @return The current system volume. + */ + f32 GetSystemVolume() const { + return system_volume; + } + + /** + * Get the device volume. + * + * @return The current device volume. + */ + f32 GetDeviceVolume() const { + return device_volume; + } + + /** + * Set the system volume. + * + * @param volume_ - The new system volume. + */ + void SetSystemVolume(f32 volume_) { + system_volume = volume_; + } + + /** + * Set the device volume. + * + * @param volume_ - The new device volume. + */ + void SetDeviceVolume(f32 volume_) { + device_volume = volume_; + } + + /** + * Get the number of queued audio buffers. + * + * @return The number of queued buffers. + */ + u32 GetQueueSize() { + return queued_buffers.load(); + } + +protected: + /// Number of buffers waiting to be played + std::atomic queued_buffers{}; + /// Total samples played by this stream + std::atomic played_sample_count{}; + /// Set by the audio render/in/out system which uses this stream + f32 system_volume{1.0f}; + /// Set via IAudioDevice service calls + f32 device_volume{1.0f}; + /// Set by the audio render/in/out systen which uses this stream + u32 system_channels{2}; + /// Channels supported by hardware + u32 device_channels{2}; + /// Is this stream currently paused? + std::atomic paused{true}; + /// Was this stream previously playing? + std::atomic was_playing{false}; +}; + +using SinkStreamPtr = std::unique_ptr; + +} // namespace AudioCore::Sink diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 907715758..dbfc371df 100755 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -45,6 +45,7 @@ add_library(common STATIC alignment.h assert.cpp assert.h + atomic_helpers.h atomic_ops.h detached_tasks.cpp detached_tasks.h @@ -66,6 +67,7 @@ add_library(common STATIC expected.h fiber.cpp fiber.h + fixed_point.h fs/file.cpp fs/file.h fs/fs.cpp @@ -113,6 +115,7 @@ add_library(common STATIC parent_of_member.h point.h quaternion.h + reader_writer_queue.h ring_buffer.h scm_rev.cpp scm_rev.h diff --git a/src/common/atomic_helpers.h b/src/common/atomic_helpers.h new file mode 100755 index 000000000..6d912b52e --- /dev/null +++ b/src/common/atomic_helpers.h @@ -0,0 +1,772 @@ +// ©2013-2016 Cameron Desrochers. +// Distributed under the simplified BSD license (see the license file that +// should have come with this header). +// Uses Jeff Preshing's semaphore implementation (under the terms of its +// separate zlib license, embedded below). + +#pragma once + +// Provides portable (VC++2010+, Intel ICC 13, GCC 4.7+, and anything C++11 compliant) +// implementation of low-level memory barriers, plus a few semi-portable utility macros (for +// inlining and alignment). Also has a basic atomic type (limited to hardware-supported atomics with +// no memory ordering guarantees). Uses the AE_* prefix for macros (historical reasons), and the +// "moodycamel" namespace for symbols. + +#include +#include +#include +#include +#include + +// Platform detection +#if defined(__INTEL_COMPILER) +#define AE_ICC +#elif defined(_MSC_VER) +#define AE_VCPP +#elif defined(__GNUC__) +#define AE_GCC +#endif + +#if defined(_M_IA64) || defined(__ia64__) +#define AE_ARCH_IA64 +#elif defined(_WIN64) || defined(__amd64__) || defined(_M_X64) || defined(__x86_64__) +#define AE_ARCH_X64 +#elif defined(_M_IX86) || defined(__i386__) +#define AE_ARCH_X86 +#elif defined(_M_PPC) || defined(__powerpc__) +#define AE_ARCH_PPC +#else +#define AE_ARCH_UNKNOWN +#endif + +// AE_UNUSED +#define AE_UNUSED(x) ((void)x) + +// AE_NO_TSAN/AE_TSAN_ANNOTATE_* +#if defined(__has_feature) +#if __has_feature(thread_sanitizer) +#if __cplusplus >= 201703L // inline variables require C++17 +namespace Common { +inline int ae_tsan_global; +} +#define AE_TSAN_ANNOTATE_RELEASE() \ + AnnotateHappensBefore(__FILE__, __LINE__, (void*)(&::moodycamel::ae_tsan_global)) +#define AE_TSAN_ANNOTATE_ACQUIRE() \ + AnnotateHappensAfter(__FILE__, __LINE__, (void*)(&::moodycamel::ae_tsan_global)) +extern "C" void AnnotateHappensBefore(const char*, int, void*); +extern "C" void AnnotateHappensAfter(const char*, int, void*); +#else // when we can't work with tsan, attempt to disable its warnings +#define AE_NO_TSAN __attribute__((no_sanitize("thread"))) +#endif +#endif +#endif +#ifndef AE_NO_TSAN +#define AE_NO_TSAN +#endif +#ifndef AE_TSAN_ANNOTATE_RELEASE +#define AE_TSAN_ANNOTATE_RELEASE() +#define AE_TSAN_ANNOTATE_ACQUIRE() +#endif + +// AE_FORCEINLINE +#if defined(AE_VCPP) || defined(AE_ICC) +#define AE_FORCEINLINE __forceinline +#elif defined(AE_GCC) +//#define AE_FORCEINLINE __attribute__((always_inline)) +#define AE_FORCEINLINE inline +#else +#define AE_FORCEINLINE inline +#endif + +// AE_ALIGN +#if defined(AE_VCPP) || defined(AE_ICC) +#define AE_ALIGN(x) __declspec(align(x)) +#elif defined(AE_GCC) +#define AE_ALIGN(x) __attribute__((aligned(x))) +#else +// Assume GCC compliant syntax... +#define AE_ALIGN(x) __attribute__((aligned(x))) +#endif + +// Portable atomic fences implemented below: + +namespace Common { + +enum memory_order { + memory_order_relaxed, + memory_order_acquire, + memory_order_release, + memory_order_acq_rel, + memory_order_seq_cst, + + // memory_order_sync: Forces a full sync: + // #LoadLoad, #LoadStore, #StoreStore, and most significantly, #StoreLoad + memory_order_sync = memory_order_seq_cst +}; + +} // namespace Common + +#if (defined(AE_VCPP) && (_MSC_VER < 1700 || defined(__cplusplus_cli))) || \ + (defined(AE_ICC) && __INTEL_COMPILER < 1600) +// VS2010 and ICC13 don't support std::atomic_*_fence, implement our own fences + +#include + +#if defined(AE_ARCH_X64) || defined(AE_ARCH_X86) +#define AeFullSync _mm_mfence +#define AeLiteSync _mm_mfence +#elif defined(AE_ARCH_IA64) +#define AeFullSync __mf +#define AeLiteSync __mf +#elif defined(AE_ARCH_PPC) +#include +#define AeFullSync __sync +#define AeLiteSync __lwsync +#endif + +#ifdef AE_VCPP +#pragma warning(push) +#pragma warning(disable : 4365) // Disable erroneous 'conversion from long to unsigned int, + // signed/unsigned mismatch' error when using `assert` +#ifdef __cplusplus_cli +#pragma managed(push, off) +#endif +#endif + +namespace Common { + +AE_FORCEINLINE void compiler_fence(memory_order order) AE_NO_TSAN { + switch (order) { + case memory_order_relaxed: + break; + case memory_order_acquire: + _ReadBarrier(); + break; + case memory_order_release: + _WriteBarrier(); + break; + case memory_order_acq_rel: + _ReadWriteBarrier(); + break; + case memory_order_seq_cst: + _ReadWriteBarrier(); + break; + default: + assert(false); + } +} + +// x86/x64 have a strong memory model -- all loads and stores have +// acquire and release semantics automatically (so only need compiler +// barriers for those). +#if defined(AE_ARCH_X86) || defined(AE_ARCH_X64) +AE_FORCEINLINE void fence(memory_order order) AE_NO_TSAN { + switch (order) { + case memory_order_relaxed: + break; + case memory_order_acquire: + _ReadBarrier(); + break; + case memory_order_release: + _WriteBarrier(); + break; + case memory_order_acq_rel: + _ReadWriteBarrier(); + break; + case memory_order_seq_cst: + _ReadWriteBarrier(); + AeFullSync(); + _ReadWriteBarrier(); + break; + default: + assert(false); + } +} +#else +AE_FORCEINLINE void fence(memory_order order) AE_NO_TSAN { + // Non-specialized arch, use heavier memory barriers everywhere just in case :-( + switch (order) { + case memory_order_relaxed: + break; + case memory_order_acquire: + _ReadBarrier(); + AeLiteSync(); + _ReadBarrier(); + break; + case memory_order_release: + _WriteBarrier(); + AeLiteSync(); + _WriteBarrier(); + break; + case memory_order_acq_rel: + _ReadWriteBarrier(); + AeLiteSync(); + _ReadWriteBarrier(); + break; + case memory_order_seq_cst: + _ReadWriteBarrier(); + AeFullSync(); + _ReadWriteBarrier(); + break; + default: + assert(false); + } +} +#endif +} // namespace Common +#else +// Use standard library of atomics +#include + +namespace Common { + +AE_FORCEINLINE void compiler_fence(memory_order order) AE_NO_TSAN { + switch (order) { + case memory_order_relaxed: + break; + case memory_order_acquire: + std::atomic_signal_fence(std::memory_order_acquire); + break; + case memory_order_release: + std::atomic_signal_fence(std::memory_order_release); + break; + case memory_order_acq_rel: + std::atomic_signal_fence(std::memory_order_acq_rel); + break; + case memory_order_seq_cst: + std::atomic_signal_fence(std::memory_order_seq_cst); + break; + default: + assert(false); + } +} + +AE_FORCEINLINE void fence(memory_order order) AE_NO_TSAN { + switch (order) { + case memory_order_relaxed: + break; + case memory_order_acquire: + AE_TSAN_ANNOTATE_ACQUIRE(); + std::atomic_thread_fence(std::memory_order_acquire); + break; + case memory_order_release: + AE_TSAN_ANNOTATE_RELEASE(); + std::atomic_thread_fence(std::memory_order_release); + break; + case memory_order_acq_rel: + AE_TSAN_ANNOTATE_ACQUIRE(); + AE_TSAN_ANNOTATE_RELEASE(); + std::atomic_thread_fence(std::memory_order_acq_rel); + break; + case memory_order_seq_cst: + AE_TSAN_ANNOTATE_ACQUIRE(); + AE_TSAN_ANNOTATE_RELEASE(); + std::atomic_thread_fence(std::memory_order_seq_cst); + break; + default: + assert(false); + } +} + +} // namespace Common + +#endif + +#if !defined(AE_VCPP) || (_MSC_VER >= 1700 && !defined(__cplusplus_cli)) +#define AE_USE_STD_ATOMIC_FOR_WEAK_ATOMIC +#endif + +#ifdef AE_USE_STD_ATOMIC_FOR_WEAK_ATOMIC +#include +#endif +#include + +// WARNING: *NOT* A REPLACEMENT FOR std::atomic. READ CAREFULLY: +// Provides basic support for atomic variables -- no memory ordering guarantees are provided. +// The guarantee of atomicity is only made for types that already have atomic load and store +// guarantees at the hardware level -- on most platforms this generally means aligned pointers and +// integers (only). +namespace Common { +template +class weak_atomic { +public: + AE_NO_TSAN weak_atomic() : value() {} +#ifdef AE_VCPP +#pragma warning(push) +#pragma warning(disable : 4100) // Get rid of (erroneous) 'unreferenced formal parameter' warning +#endif + template + AE_NO_TSAN weak_atomic(U&& x) : value(std::forward(x)) {} +#ifdef __cplusplus_cli + // Work around bug with universal reference/nullptr combination that only appears when /clr is + // on + AE_NO_TSAN weak_atomic(nullptr_t) : value(nullptr) {} +#endif + AE_NO_TSAN weak_atomic(weak_atomic const& other) : value(other.load()) {} + AE_NO_TSAN weak_atomic(weak_atomic&& other) : value(std::move(other.load())) {} +#ifdef AE_VCPP +#pragma warning(pop) +#endif + + AE_FORCEINLINE operator T() const AE_NO_TSAN { + return load(); + } + +#ifndef AE_USE_STD_ATOMIC_FOR_WEAK_ATOMIC + template + AE_FORCEINLINE weak_atomic const& operator=(U&& x) AE_NO_TSAN { + value = std::forward(x); + return *this; + } + AE_FORCEINLINE weak_atomic const& operator=(weak_atomic const& other) AE_NO_TSAN { + value = other.value; + return *this; + } + + AE_FORCEINLINE T load() const AE_NO_TSAN { + return value; + } + + AE_FORCEINLINE T fetch_add_acquire(T increment) AE_NO_TSAN { +#if defined(AE_ARCH_X64) || defined(AE_ARCH_X86) + if (sizeof(T) == 4) + return _InterlockedExchangeAdd((long volatile*)&value, (long)increment); +#if defined(_M_AMD64) + else if (sizeof(T) == 8) + return _InterlockedExchangeAdd64((long long volatile*)&value, (long long)increment); +#endif +#else +#error Unsupported platform +#endif + assert(false && "T must be either a 32 or 64 bit type"); + return value; + } + + AE_FORCEINLINE T fetch_add_release(T increment) AE_NO_TSAN { +#if defined(AE_ARCH_X64) || defined(AE_ARCH_X86) + if (sizeof(T) == 4) + return _InterlockedExchangeAdd((long volatile*)&value, (long)increment); +#if defined(_M_AMD64) + else if (sizeof(T) == 8) + return _InterlockedExchangeAdd64((long long volatile*)&value, (long long)increment); +#endif +#else +#error Unsupported platform +#endif + assert(false && "T must be either a 32 or 64 bit type"); + return value; + } +#else + template + AE_FORCEINLINE weak_atomic const& operator=(U&& x) AE_NO_TSAN { + value.store(std::forward(x), std::memory_order_relaxed); + return *this; + } + + AE_FORCEINLINE weak_atomic const& operator=(weak_atomic const& other) AE_NO_TSAN { + value.store(other.value.load(std::memory_order_relaxed), std::memory_order_relaxed); + return *this; + } + + AE_FORCEINLINE T load() const AE_NO_TSAN { + return value.load(std::memory_order_relaxed); + } + + AE_FORCEINLINE T fetch_add_acquire(T increment) AE_NO_TSAN { + return value.fetch_add(increment, std::memory_order_acquire); + } + + AE_FORCEINLINE T fetch_add_release(T increment) AE_NO_TSAN { + return value.fetch_add(increment, std::memory_order_release); + } +#endif + +private: +#ifndef AE_USE_STD_ATOMIC_FOR_WEAK_ATOMIC + // No std::atomic support, but still need to circumvent compiler optimizations. + // `volatile` will make memory access slow, but is guaranteed to be reliable. + volatile T value; +#else + std::atomic value; +#endif +}; + +} // namespace Common + +// Portable single-producer, single-consumer semaphore below: + +#if defined(_WIN32) +// Avoid including windows.h in a header; we only need a handful of +// items, so we'll redeclare them here (this is relatively safe since +// the API generally has to remain stable between Windows versions). +// I know this is an ugly hack but it still beats polluting the global +// namespace with thousands of generic names or adding a .cpp for nothing. +extern "C" { +struct _SECURITY_ATTRIBUTES; +__declspec(dllimport) void* __stdcall CreateSemaphoreW(_SECURITY_ATTRIBUTES* lpSemaphoreAttributes, + long lInitialCount, long lMaximumCount, + const wchar_t* lpName); +__declspec(dllimport) int __stdcall CloseHandle(void* hObject); +__declspec(dllimport) unsigned long __stdcall WaitForSingleObject(void* hHandle, + unsigned long dwMilliseconds); +__declspec(dllimport) int __stdcall ReleaseSemaphore(void* hSemaphore, long lReleaseCount, + long* lpPreviousCount); +} +#elif defined(__MACH__) +#include +#elif defined(__unix__) +#include +#elif defined(FREERTOS) +#include +#include +#include +#endif + +namespace Common { +// Code in the spsc_sema namespace below is an adaptation of Jeff Preshing's +// portable + lightweight semaphore implementations, originally from +// https://github.com/preshing/cpp11-on-multicore/blob/master/common/sema.h +// LICENSE: +// Copyright (c) 2015 Jeff Preshing +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgement in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +namespace spsc_sema { +#if defined(_WIN32) +class Semaphore { +private: + void* m_hSema; + + Semaphore(const Semaphore& other); + Semaphore& operator=(const Semaphore& other); + +public: + AE_NO_TSAN Semaphore(int initialCount = 0) : m_hSema() { + assert(initialCount >= 0); + const long maxLong = 0x7fffffff; + m_hSema = CreateSemaphoreW(nullptr, initialCount, maxLong, nullptr); + assert(m_hSema); + } + + AE_NO_TSAN ~Semaphore() { + CloseHandle(m_hSema); + } + + bool wait() AE_NO_TSAN { + const unsigned long infinite = 0xffffffff; + return WaitForSingleObject(m_hSema, infinite) == 0; + } + + bool try_wait() AE_NO_TSAN { + return WaitForSingleObject(m_hSema, 0) == 0; + } + + bool timed_wait(std::uint64_t usecs) AE_NO_TSAN { + return WaitForSingleObject(m_hSema, (unsigned long)(usecs / 1000)) == 0; + } + + void signal(int count = 1) AE_NO_TSAN { + while (!ReleaseSemaphore(m_hSema, count, nullptr)) + ; + } +}; +#elif defined(__MACH__) +//--------------------------------------------------------- +// Semaphore (Apple iOS and OSX) +// Can't use POSIX semaphores due to +// http://lists.apple.com/archives/darwin-kernel/2009/Apr/msg00010.html +//--------------------------------------------------------- +class Semaphore { +private: + semaphore_t m_sema; + + Semaphore(const Semaphore& other); + Semaphore& operator=(const Semaphore& other); + +public: + AE_NO_TSAN Semaphore(int initialCount = 0) : m_sema() { + assert(initialCount >= 0); + kern_return_t rc = + semaphore_create(mach_task_self(), &m_sema, SYNC_POLICY_FIFO, initialCount); + assert(rc == KERN_SUCCESS); + AE_UNUSED(rc); + } + + AE_NO_TSAN ~Semaphore() { + semaphore_destroy(mach_task_self(), m_sema); + } + + bool wait() AE_NO_TSAN { + return semaphore_wait(m_sema) == KERN_SUCCESS; + } + + bool try_wait() AE_NO_TSAN { + return timed_wait(0); + } + + bool timed_wait(std::uint64_t timeout_usecs) AE_NO_TSAN { + mach_timespec_t ts; + ts.tv_sec = static_cast(timeout_usecs / 1000000); + ts.tv_nsec = static_cast((timeout_usecs % 1000000) * 1000); + + // added in OSX 10.10: + // https://developer.apple.com/library/prerelease/mac/documentation/General/Reference/APIDiffsMacOSX10_10SeedDiff/modules/Darwin.html + kern_return_t rc = semaphore_timedwait(m_sema, ts); + return rc == KERN_SUCCESS; + } + + void signal() AE_NO_TSAN { + while (semaphore_signal(m_sema) != KERN_SUCCESS) + ; + } + + void signal(int count) AE_NO_TSAN { + while (count-- > 0) { + while (semaphore_signal(m_sema) != KERN_SUCCESS) + ; + } + } +}; +#elif defined(__unix__) +//--------------------------------------------------------- +// Semaphore (POSIX, Linux) +//--------------------------------------------------------- +class Semaphore { +private: + sem_t m_sema; + + Semaphore(const Semaphore& other); + Semaphore& operator=(const Semaphore& other); + +public: + AE_NO_TSAN Semaphore(int initialCount = 0) : m_sema() { + assert(initialCount >= 0); + int rc = sem_init(&m_sema, 0, static_cast(initialCount)); + assert(rc == 0); + AE_UNUSED(rc); + } + + AE_NO_TSAN ~Semaphore() { + sem_destroy(&m_sema); + } + + bool wait() AE_NO_TSAN { + // http://stackoverflow.com/questions/2013181/gdb-causes-sem-wait-to-fail-with-eintr-error + int rc; + do { + rc = sem_wait(&m_sema); + } while (rc == -1 && errno == EINTR); + return rc == 0; + } + + bool try_wait() AE_NO_TSAN { + int rc; + do { + rc = sem_trywait(&m_sema); + } while (rc == -1 && errno == EINTR); + return rc == 0; + } + + bool timed_wait(std::uint64_t usecs) AE_NO_TSAN { + struct timespec ts; + const int usecs_in_1_sec = 1000000; + const int nsecs_in_1_sec = 1000000000; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += static_cast(usecs / usecs_in_1_sec); + ts.tv_nsec += static_cast(usecs % usecs_in_1_sec) * 1000; + // sem_timedwait bombs if you have more than 1e9 in tv_nsec + // so we have to clean things up before passing it in + if (ts.tv_nsec >= nsecs_in_1_sec) { + ts.tv_nsec -= nsecs_in_1_sec; + ++ts.tv_sec; + } + + int rc; + do { + rc = sem_timedwait(&m_sema, &ts); + } while (rc == -1 && errno == EINTR); + return rc == 0; + } + + void signal() AE_NO_TSAN { + while (sem_post(&m_sema) == -1) + ; + } + + void signal(int count) AE_NO_TSAN { + while (count-- > 0) { + while (sem_post(&m_sema) == -1) + ; + } + } +}; +#elif defined(FREERTOS) +//--------------------------------------------------------- +// Semaphore (FreeRTOS) +//--------------------------------------------------------- +class Semaphore { +private: + SemaphoreHandle_t m_sema; + + Semaphore(const Semaphore& other); + Semaphore& operator=(const Semaphore& other); + +public: + AE_NO_TSAN Semaphore(int initialCount = 0) : m_sema() { + assert(initialCount >= 0); + m_sema = xSemaphoreCreateCounting(static_cast(~0ull), + static_cast(initialCount)); + assert(m_sema); + } + + AE_NO_TSAN ~Semaphore() { + vSemaphoreDelete(m_sema); + } + + bool wait() AE_NO_TSAN { + return xSemaphoreTake(m_sema, portMAX_DELAY) == pdTRUE; + } + + bool try_wait() AE_NO_TSAN { + // Note: In an ISR context, if this causes a task to unblock, + // the caller won't know about it + if (xPortIsInsideInterrupt()) + return xSemaphoreTakeFromISR(m_sema, NULL) == pdTRUE; + return xSemaphoreTake(m_sema, 0) == pdTRUE; + } + + bool timed_wait(std::uint64_t usecs) AE_NO_TSAN { + std::uint64_t msecs = usecs / 1000; + TickType_t ticks = static_cast(msecs / portTICK_PERIOD_MS); + if (ticks == 0) + return try_wait(); + return xSemaphoreTake(m_sema, ticks) == pdTRUE; + } + + void signal() AE_NO_TSAN { + // Note: In an ISR context, if this causes a task to unblock, + // the caller won't know about it + BaseType_t rc; + if (xPortIsInsideInterrupt()) + rc = xSemaphoreGiveFromISR(m_sema, NULL); + else + rc = xSemaphoreGive(m_sema); + assert(rc == pdTRUE); + AE_UNUSED(rc); + } + + void signal(int count) AE_NO_TSAN { + while (count-- > 0) + signal(); + } +}; +#else +#error Unsupported platform! (No semaphore wrapper available) +#endif + +//--------------------------------------------------------- +// LightweightSemaphore +//--------------------------------------------------------- +class LightweightSemaphore { +public: + typedef std::make_signed::type ssize_t; + +private: + weak_atomic m_count; + Semaphore m_sema; + + bool waitWithPartialSpinning(std::int64_t timeout_usecs = -1) AE_NO_TSAN { + ssize_t oldCount; + // Is there a better way to set the initial spin count? + // If we lower it to 1000, testBenaphore becomes 15x slower on my Core i7-5930K Windows PC, + // as threads start hitting the kernel semaphore. + int spin = 1024; + while (--spin >= 0) { + if (m_count.load() > 0) { + m_count.fetch_add_acquire(-1); + return true; + } + compiler_fence(memory_order_acquire); // Prevent the compiler from collapsing the loop. + } + oldCount = m_count.fetch_add_acquire(-1); + if (oldCount > 0) + return true; + if (timeout_usecs < 0) { + if (m_sema.wait()) + return true; + } + if (timeout_usecs > 0 && m_sema.timed_wait(static_cast(timeout_usecs))) + return true; + // At this point, we've timed out waiting for the semaphore, but the + // count is still decremented indicating we may still be waiting on + // it. So we have to re-adjust the count, but only if the semaphore + // wasn't signaled enough times for us too since then. If it was, we + // need to release the semaphore too. + while (true) { + oldCount = m_count.fetch_add_release(1); + if (oldCount < 0) + return false; // successfully restored things to the way they were + // Oh, the producer thread just signaled the semaphore after all. Try again: + oldCount = m_count.fetch_add_acquire(-1); + if (oldCount > 0 && m_sema.try_wait()) + return true; + } + } + +public: + AE_NO_TSAN LightweightSemaphore(ssize_t initialCount = 0) : m_count(initialCount), m_sema() { + assert(initialCount >= 0); + } + + bool tryWait() AE_NO_TSAN { + if (m_count.load() > 0) { + m_count.fetch_add_acquire(-1); + return true; + } + return false; + } + + bool wait() AE_NO_TSAN { + return tryWait() || waitWithPartialSpinning(); + } + + bool wait(std::int64_t timeout_usecs) AE_NO_TSAN { + return tryWait() || waitWithPartialSpinning(timeout_usecs); + } + + void signal(ssize_t count = 1) AE_NO_TSAN { + assert(count >= 0); + ssize_t oldCount = m_count.fetch_add_release(count); + assert(oldCount >= -1); + if (oldCount < 0) { + m_sema.signal(1); + } + } + + std::size_t availableApprox() const AE_NO_TSAN { + ssize_t count = m_count.load(); + return count > 0 ? static_cast(count) : 0; + } +}; +} // namespace spsc_sema +} // namespace Common + +#if defined(AE_VCPP) && (_MSC_VER < 1700 || defined(__cplusplus_cli)) +#pragma warning(pop) +#ifdef __cplusplus_cli +#pragma managed(pop) +#endif +#endif diff --git a/src/common/fixed_point.h b/src/common/fixed_point.h new file mode 100755 index 000000000..1d45e51b3 --- /dev/null +++ b/src/common/fixed_point.h @@ -0,0 +1,726 @@ +// From: https://github.com/eteran/cpp-utilities/blob/master/fixed/include/cpp-utilities/fixed.h +// See also: http://stackoverflow.com/questions/79677/whats-the-best-way-to-do-fixed-point-math +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 Evan Teran + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef FIXED_H_ +#define FIXED_H_ + +#if __cplusplus >= 201402L +#define CONSTEXPR14 constexpr +#else +#define CONSTEXPR14 +#endif + +#include // for size_t +#include +#include +#include +#include + +namespace Common { + +template +class FixedPoint; + +namespace detail { + +// helper templates to make magic with types :) +// these allow us to determine resonable types from +// a desired size, they also let us infer the next largest type +// from a type which is nice for the division op +template +struct type_from_size { + using value_type = void; + using unsigned_type = void; + using signed_type = void; + static constexpr bool is_specialized = false; +}; + +#if defined(__GNUC__) && defined(__x86_64__) && !defined(__STRICT_ANSI__) +template <> +struct type_from_size<128> { + static constexpr bool is_specialized = true; + static constexpr size_t size = 128; + + using value_type = __int128; + using unsigned_type = unsigned __int128; + using signed_type = __int128; + using next_size = type_from_size<256>; +}; +#endif + +template <> +struct type_from_size<64> { + static constexpr bool is_specialized = true; + static constexpr size_t size = 64; + + using value_type = int64_t; + using unsigned_type = std::make_unsigned::type; + using signed_type = std::make_signed::type; + using next_size = type_from_size<128>; +}; + +template <> +struct type_from_size<32> { + static constexpr bool is_specialized = true; + static constexpr size_t size = 32; + + using value_type = int32_t; + using unsigned_type = std::make_unsigned::type; + using signed_type = std::make_signed::type; + using next_size = type_from_size<64>; +}; + +template <> +struct type_from_size<16> { + static constexpr bool is_specialized = true; + static constexpr size_t size = 16; + + using value_type = int16_t; + using unsigned_type = std::make_unsigned::type; + using signed_type = std::make_signed::type; + using next_size = type_from_size<32>; +}; + +template <> +struct type_from_size<8> { + static constexpr bool is_specialized = true; + static constexpr size_t size = 8; + + using value_type = int8_t; + using unsigned_type = std::make_unsigned::type; + using signed_type = std::make_signed::type; + using next_size = type_from_size<16>; +}; + +// this is to assist in adding support for non-native base +// types (for adding big-int support), this should be fine +// unless your bit-int class doesn't nicely support casting +template +constexpr B next_to_base(N rhs) { + return static_cast(rhs); +} + +struct divide_by_zero : std::exception {}; + +template +CONSTEXPR14 FixedPoint divide( + FixedPoint numerator, FixedPoint denominator, FixedPoint& remainder, + typename std::enable_if::next_size::is_specialized>::type* = nullptr) { + + using next_type = typename FixedPoint::next_type; + using base_type = typename FixedPoint::base_type; + constexpr size_t fractional_bits = FixedPoint::fractional_bits; + + next_type t(numerator.to_raw()); + t <<= fractional_bits; + + FixedPoint quotient; + + quotient = FixedPoint::from_base(next_to_base(t / denominator.to_raw())); + remainder = FixedPoint::from_base(next_to_base(t % denominator.to_raw())); + + return quotient; +} + +template +CONSTEXPR14 FixedPoint divide( + FixedPoint numerator, FixedPoint denominator, FixedPoint& remainder, + typename std::enable_if::next_size::is_specialized>::type* = nullptr) { + + using unsigned_type = typename FixedPoint::unsigned_type; + + constexpr int bits = FixedPoint::total_bits; + + if (denominator == 0) { + throw divide_by_zero(); + } else { + + int sign = 0; + + FixedPoint quotient; + + if (numerator < 0) { + sign ^= 1; + numerator = -numerator; + } + + if (denominator < 0) { + sign ^= 1; + denominator = -denominator; + } + + unsigned_type n = numerator.to_raw(); + unsigned_type d = denominator.to_raw(); + unsigned_type x = 1; + unsigned_type answer = 0; + + // egyptian division algorithm + while ((n >= d) && (((d >> (bits - 1)) & 1) == 0)) { + x <<= 1; + d <<= 1; + } + + while (x != 0) { + if (n >= d) { + n -= d; + answer += x; + } + + x >>= 1; + d >>= 1; + } + + unsigned_type l1 = n; + unsigned_type l2 = denominator.to_raw(); + + // calculate the lower bits (needs to be unsigned) + while (l1 >> (bits - F) > 0) { + l1 >>= 1; + l2 >>= 1; + } + const unsigned_type lo = (l1 << F) / l2; + + quotient = FixedPoint::from_base((answer << F) | lo); + remainder = n; + + if (sign) { + quotient = -quotient; + } + + return quotient; + } +} + +// this is the usual implementation of multiplication +template +CONSTEXPR14 FixedPoint multiply( + FixedPoint lhs, FixedPoint rhs, + typename std::enable_if::next_size::is_specialized>::type* = nullptr) { + + using next_type = typename FixedPoint::next_type; + using base_type = typename FixedPoint::base_type; + + constexpr size_t fractional_bits = FixedPoint::fractional_bits; + + next_type t(static_cast(lhs.to_raw()) * static_cast(rhs.to_raw())); + t >>= fractional_bits; + + return FixedPoint::from_base(next_to_base(t)); +} + +// this is the fall back version we use when we don't have a next size +// it is slightly slower, but is more robust since it doesn't +// require and upgraded type +template +CONSTEXPR14 FixedPoint multiply( + FixedPoint lhs, FixedPoint rhs, + typename std::enable_if::next_size::is_specialized>::type* = nullptr) { + + using base_type = typename FixedPoint::base_type; + + constexpr size_t fractional_bits = FixedPoint::fractional_bits; + constexpr base_type integer_mask = FixedPoint::integer_mask; + constexpr base_type fractional_mask = FixedPoint::fractional_mask; + + // more costly but doesn't need a larger type + const base_type a_hi = (lhs.to_raw() & integer_mask) >> fractional_bits; + const base_type b_hi = (rhs.to_raw() & integer_mask) >> fractional_bits; + const base_type a_lo = (lhs.to_raw() & fractional_mask); + const base_type b_lo = (rhs.to_raw() & fractional_mask); + + const base_type x1 = a_hi * b_hi; + const base_type x2 = a_hi * b_lo; + const base_type x3 = a_lo * b_hi; + const base_type x4 = a_lo * b_lo; + + return FixedPoint::from_base((x1 << fractional_bits) + (x3 + x2) + + (x4 >> fractional_bits)); +} +} // namespace detail + +template +class FixedPoint { + static_assert(detail::type_from_size::is_specialized, "invalid combination of sizes"); + +public: + static constexpr size_t fractional_bits = F; + static constexpr size_t integer_bits = I; + static constexpr size_t total_bits = I + F; + + using base_type_info = detail::type_from_size; + + using base_type = typename base_type_info::value_type; + using next_type = typename base_type_info::next_size::value_type; + using unsigned_type = typename base_type_info::unsigned_type; + +public: +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Woverflow" +#endif + static constexpr base_type fractional_mask = + ~(static_cast(~base_type(0)) << fractional_bits); + static constexpr base_type integer_mask = ~fractional_mask; +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +public: + static constexpr base_type one = base_type(1) << fractional_bits; + +public: // constructors + FixedPoint() = default; + FixedPoint(const FixedPoint&) = default; + FixedPoint(FixedPoint&&) = default; + FixedPoint& operator=(const FixedPoint&) = default; + + template + constexpr FixedPoint( + Number n, typename std::enable_if::value>::type* = nullptr) + : data_(static_cast(n * one)) {} + +public: // conversion + template + CONSTEXPR14 explicit FixedPoint(FixedPoint other) { + static_assert(I2 <= I && F2 <= F, "Scaling conversion can only upgrade types"); + using T = FixedPoint; + + const base_type fractional = (other.data_ & T::fractional_mask); + const base_type integer = (other.data_ & T::integer_mask) >> T::fractional_bits; + data_ = + (integer << fractional_bits) | (fractional << (fractional_bits - T::fractional_bits)); + } + +private: + // this makes it simpler to create a FixedPoint point object from + // a native type without scaling + // use "FixedPoint::from_base" in order to perform this. + struct NoScale {}; + + constexpr FixedPoint(base_type n, const NoScale&) : data_(n) {} + +public: + static constexpr FixedPoint from_base(base_type n) { + return FixedPoint(n, NoScale()); + } + +public: // comparison operators + constexpr bool operator==(FixedPoint rhs) const { + return data_ == rhs.data_; + } + + constexpr bool operator!=(FixedPoint rhs) const { + return data_ != rhs.data_; + } + + constexpr bool operator<(FixedPoint rhs) const { + return data_ < rhs.data_; + } + + constexpr bool operator>(FixedPoint rhs) const { + return data_ > rhs.data_; + } + + constexpr bool operator<=(FixedPoint rhs) const { + return data_ <= rhs.data_; + } + + constexpr bool operator>=(FixedPoint rhs) const { + return data_ >= rhs.data_; + } + +public: // unary operators + constexpr bool operator!() const { + return !data_; + } + + constexpr FixedPoint operator~() const { + // NOTE(eteran): this will often appear to "just negate" the value + // that is not an error, it is because -x == (~x+1) + // and that "+1" is adding an infinitesimally small fraction to the + // complimented value + return FixedPoint::from_base(~data_); + } + + constexpr FixedPoint operator-() const { + return FixedPoint::from_base(-data_); + } + + constexpr FixedPoint operator+() const { + return FixedPoint::from_base(+data_); + } + + CONSTEXPR14 FixedPoint& operator++() { + data_ += one; + return *this; + } + + CONSTEXPR14 FixedPoint& operator--() { + data_ -= one; + return *this; + } + + CONSTEXPR14 FixedPoint operator++(int) { + FixedPoint tmp(*this); + data_ += one; + return tmp; + } + + CONSTEXPR14 FixedPoint operator--(int) { + FixedPoint tmp(*this); + data_ -= one; + return tmp; + } + +public: // basic math operators + CONSTEXPR14 FixedPoint& operator+=(FixedPoint n) { + data_ += n.data_; + return *this; + } + + CONSTEXPR14 FixedPoint& operator-=(FixedPoint n) { + data_ -= n.data_; + return *this; + } + + CONSTEXPR14 FixedPoint& operator*=(FixedPoint n) { + return assign(detail::multiply(*this, n)); + } + + CONSTEXPR14 FixedPoint& operator/=(FixedPoint n) { + FixedPoint temp; + return assign(detail::divide(*this, n, temp)); + } + +private: + CONSTEXPR14 FixedPoint& assign(FixedPoint rhs) { + data_ = rhs.data_; + return *this; + } + +public: // binary math operators, effects underlying bit pattern since these + // don't really typically make sense for non-integer values + CONSTEXPR14 FixedPoint& operator&=(FixedPoint n) { + data_ &= n.data_; + return *this; + } + + CONSTEXPR14 FixedPoint& operator|=(FixedPoint n) { + data_ |= n.data_; + return *this; + } + + CONSTEXPR14 FixedPoint& operator^=(FixedPoint n) { + data_ ^= n.data_; + return *this; + } + + template ::value>::type> + CONSTEXPR14 FixedPoint& operator>>=(Integer n) { + data_ >>= n; + return *this; + } + + template ::value>::type> + CONSTEXPR14 FixedPoint& operator<<=(Integer n) { + data_ <<= n; + return *this; + } + +public: // conversion to basic types + constexpr void round_up() { + data_ += (data_ & fractional_mask) >> 1; + } + + constexpr int to_int() { + round_up(); + return static_cast((data_ & integer_mask) >> fractional_bits); + } + + constexpr unsigned int to_uint() const { + round_up(); + return static_cast((data_ & integer_mask) >> fractional_bits); + } + + constexpr int64_t to_long() { + round_up(); + return static_cast((data_ & integer_mask) >> fractional_bits); + } + + constexpr int to_int_floor() const { + return static_cast((data_ & integer_mask) >> fractional_bits); + } + + constexpr int64_t to_long_floor() { + return static_cast((data_ & integer_mask) >> fractional_bits); + } + + constexpr unsigned int to_uint_floor() const { + return static_cast((data_ & integer_mask) >> fractional_bits); + } + + constexpr float to_float() const { + return static_cast(data_) / FixedPoint::one; + } + + constexpr double to_double() const { + return static_cast(data_) / FixedPoint::one; + } + + constexpr base_type to_raw() const { + return data_; + } + + constexpr void clear_int() { + data_ &= fractional_mask; + } + + constexpr base_type get_frac() const { + return data_ & fractional_mask; + } + +public: + CONSTEXPR14 void swap(FixedPoint& rhs) { + using std::swap; + swap(data_, rhs.data_); + } + +public: + base_type data_; +}; + +// if we have the same fractional portion, but differing integer portions, we trivially upgrade the +// smaller type +template +CONSTEXPR14 typename std::conditional= I2, FixedPoint, FixedPoint>::type +operator+(FixedPoint lhs, FixedPoint rhs) { + + using T = typename std::conditional= I2, FixedPoint, FixedPoint>::type; + + const T l = T::from_base(lhs.to_raw()); + const T r = T::from_base(rhs.to_raw()); + return l + r; +} + +template +CONSTEXPR14 typename std::conditional= I2, FixedPoint, FixedPoint>::type +operator-(FixedPoint lhs, FixedPoint rhs) { + + using T = typename std::conditional= I2, FixedPoint, FixedPoint>::type; + + const T l = T::from_base(lhs.to_raw()); + const T r = T::from_base(rhs.to_raw()); + return l - r; +} + +template +CONSTEXPR14 typename std::conditional= I2, FixedPoint, FixedPoint>::type +operator*(FixedPoint lhs, FixedPoint rhs) { + + using T = typename std::conditional= I2, FixedPoint, FixedPoint>::type; + + const T l = T::from_base(lhs.to_raw()); + const T r = T::from_base(rhs.to_raw()); + return l * r; +} + +template +CONSTEXPR14 typename std::conditional= I2, FixedPoint, FixedPoint>::type +operator/(FixedPoint lhs, FixedPoint rhs) { + + using T = typename std::conditional= I2, FixedPoint, FixedPoint>::type; + + const T l = T::from_base(lhs.to_raw()); + const T r = T::from_base(rhs.to_raw()); + return l / r; +} + +template +std::ostream& operator<<(std::ostream& os, FixedPoint f) { + os << f.to_double(); + return os; +} + +// basic math operators +template +CONSTEXPR14 FixedPoint operator+(FixedPoint lhs, FixedPoint rhs) { + lhs += rhs; + return lhs; +} +template +CONSTEXPR14 FixedPoint operator-(FixedPoint lhs, FixedPoint rhs) { + lhs -= rhs; + return lhs; +} +template +CONSTEXPR14 FixedPoint operator*(FixedPoint lhs, FixedPoint rhs) { + lhs *= rhs; + return lhs; +} +template +CONSTEXPR14 FixedPoint operator/(FixedPoint lhs, FixedPoint rhs) { + lhs /= rhs; + return lhs; +} + +template ::value>::type> +CONSTEXPR14 FixedPoint operator+(FixedPoint lhs, Number rhs) { + lhs += FixedPoint(rhs); + return lhs; +} +template ::value>::type> +CONSTEXPR14 FixedPoint operator-(FixedPoint lhs, Number rhs) { + lhs -= FixedPoint(rhs); + return lhs; +} +template ::value>::type> +CONSTEXPR14 FixedPoint operator*(FixedPoint lhs, Number rhs) { + lhs *= FixedPoint(rhs); + return lhs; +} +template ::value>::type> +CONSTEXPR14 FixedPoint operator/(FixedPoint lhs, Number rhs) { + lhs /= FixedPoint(rhs); + return lhs; +} + +template ::value>::type> +CONSTEXPR14 FixedPoint operator+(Number lhs, FixedPoint rhs) { + FixedPoint tmp(lhs); + tmp += rhs; + return tmp; +} +template ::value>::type> +CONSTEXPR14 FixedPoint operator-(Number lhs, FixedPoint rhs) { + FixedPoint tmp(lhs); + tmp -= rhs; + return tmp; +} +template ::value>::type> +CONSTEXPR14 FixedPoint operator*(Number lhs, FixedPoint rhs) { + FixedPoint tmp(lhs); + tmp *= rhs; + return tmp; +} +template ::value>::type> +CONSTEXPR14 FixedPoint operator/(Number lhs, FixedPoint rhs) { + FixedPoint tmp(lhs); + tmp /= rhs; + return tmp; +} + +// shift operators +template ::value>::type> +CONSTEXPR14 FixedPoint operator<<(FixedPoint lhs, Integer rhs) { + lhs <<= rhs; + return lhs; +} +template ::value>::type> +CONSTEXPR14 FixedPoint operator>>(FixedPoint lhs, Integer rhs) { + lhs >>= rhs; + return lhs; +} + +// comparison operators +template ::value>::type> +constexpr bool operator>(FixedPoint lhs, Number rhs) { + return lhs > FixedPoint(rhs); +} +template ::value>::type> +constexpr bool operator<(FixedPoint lhs, Number rhs) { + return lhs < FixedPoint(rhs); +} +template ::value>::type> +constexpr bool operator>=(FixedPoint lhs, Number rhs) { + return lhs >= FixedPoint(rhs); +} +template ::value>::type> +constexpr bool operator<=(FixedPoint lhs, Number rhs) { + return lhs <= FixedPoint(rhs); +} +template ::value>::type> +constexpr bool operator==(FixedPoint lhs, Number rhs) { + return lhs == FixedPoint(rhs); +} +template ::value>::type> +constexpr bool operator!=(FixedPoint lhs, Number rhs) { + return lhs != FixedPoint(rhs); +} + +template ::value>::type> +constexpr bool operator>(Number lhs, FixedPoint rhs) { + return FixedPoint(lhs) > rhs; +} +template ::value>::type> +constexpr bool operator<(Number lhs, FixedPoint rhs) { + return FixedPoint(lhs) < rhs; +} +template ::value>::type> +constexpr bool operator>=(Number lhs, FixedPoint rhs) { + return FixedPoint(lhs) >= rhs; +} +template ::value>::type> +constexpr bool operator<=(Number lhs, FixedPoint rhs) { + return FixedPoint(lhs) <= rhs; +} +template ::value>::type> +constexpr bool operator==(Number lhs, FixedPoint rhs) { + return FixedPoint(lhs) == rhs; +} +template ::value>::type> +constexpr bool operator!=(Number lhs, FixedPoint rhs) { + return FixedPoint(lhs) != rhs; +} + +} // namespace Common + +#undef CONSTEXPR14 + +#endif diff --git a/src/common/reader_writer_queue.h b/src/common/reader_writer_queue.h new file mode 100755 index 000000000..8d2c9408c --- /dev/null +++ b/src/common/reader_writer_queue.h @@ -0,0 +1,941 @@ +// ©2013-2020 Cameron Desrochers. +// Distributed under the simplified BSD license (see the license file that +// should have come with this header). + +#pragma once + +#include +#include +#include // For malloc/free/abort & size_t +#include +#include +#include +#include +#include + +#include "common/atomic_helpers.h" + +#if __cplusplus > 199711L || _MSC_VER >= 1700 // C++11 or VS2012 +#include +#endif + +// A lock-free queue for a single-consumer, single-producer architecture. +// The queue is also wait-free in the common path (except if more memory +// needs to be allocated, in which case malloc is called). +// Allocates memory sparingly, and only once if the original maximum size +// estimate is never exceeded. +// Tested on x86/x64 processors, but semantics should be correct for all +// architectures (given the right implementations in atomicops.h), provided +// that aligned integer and pointer accesses are naturally atomic. +// Note that there should only be one consumer thread and producer thread; +// Switching roles of the threads, or using multiple consecutive threads for +// one role, is not safe unless properly synchronized. +// Using the queue exclusively from one thread is fine, though a bit silly. + +#ifndef MOODYCAMEL_CACHE_LINE_SIZE +#define MOODYCAMEL_CACHE_LINE_SIZE 64 +#endif + +#ifndef MOODYCAMEL_EXCEPTIONS_ENABLED +#if (defined(_MSC_VER) && defined(_CPPUNWIND)) || (defined(__GNUC__) && defined(__EXCEPTIONS)) || \ + (!defined(_MSC_VER) && !defined(__GNUC__)) +#define MOODYCAMEL_EXCEPTIONS_ENABLED +#endif +#endif + +#ifndef MOODYCAMEL_HAS_EMPLACE +#if !defined(_MSC_VER) || \ + _MSC_VER >= 1800 // variadic templates: either a non-MS compiler or VS >= 2013 +#define MOODYCAMEL_HAS_EMPLACE 1 +#endif +#endif + +#ifndef MOODYCAMEL_MAYBE_ALIGN_TO_CACHELINE +#if defined(__APPLE__) && defined(__MACH__) && __cplusplus >= 201703L +// This is required to find out what deployment target we are using +#include +#if !defined(MAC_OS_X_VERSION_MIN_REQUIRED) || \ + MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_14 +// C++17 new(size_t, align_val_t) is not backwards-compatible with older versions of macOS, so we +// can't support over-alignment in this case +#define MOODYCAMEL_MAYBE_ALIGN_TO_CACHELINE +#endif +#endif +#endif + +#ifndef MOODYCAMEL_MAYBE_ALIGN_TO_CACHELINE +#define MOODYCAMEL_MAYBE_ALIGN_TO_CACHELINE AE_ALIGN(MOODYCAMEL_CACHE_LINE_SIZE) +#endif + +#ifdef AE_VCPP +#pragma warning(push) +#pragma warning(disable : 4324) // structure was padded due to __declspec(align()) +#pragma warning(disable : 4820) // padding was added +#pragma warning(disable : 4127) // conditional expression is constant +#endif + +namespace Common { + +template +class MOODYCAMEL_MAYBE_ALIGN_TO_CACHELINE ReaderWriterQueue { + // Design: Based on a queue-of-queues. The low-level queues are just + // circular buffers with front and tail indices indicating where the + // next element to dequeue is and where the next element can be enqueued, + // respectively. Each low-level queue is called a "block". Each block + // wastes exactly one element's worth of space to keep the design simple + // (if front == tail then the queue is empty, and can't be full). + // The high-level queue is a circular linked list of blocks; again there + // is a front and tail, but this time they are pointers to the blocks. + // The front block is where the next element to be dequeued is, provided + // the block is not empty. The back block is where elements are to be + // enqueued, provided the block is not full. + // The producer thread owns all the tail indices/pointers. The consumer + // thread owns all the front indices/pointers. Both threads read each + // other's variables, but only the owning thread updates them. E.g. After + // the consumer reads the producer's tail, the tail may change before the + // consumer is done dequeuing an object, but the consumer knows the tail + // will never go backwards, only forwards. + // If there is no room to enqueue an object, an additional block (of + // equal size to the last block) is added. Blocks are never removed. + +public: + typedef T value_type; + + // Constructs a queue that can hold at least `size` elements without further + // allocations. If more than MAX_BLOCK_SIZE elements are requested, + // then several blocks of MAX_BLOCK_SIZE each are reserved (including + // at least one extra buffer block). + AE_NO_TSAN explicit ReaderWriterQueue(size_t size = 15) +#ifndef NDEBUG + : enqueuing(false), dequeuing(false) +#endif + { + assert(MAX_BLOCK_SIZE == ceilToPow2(MAX_BLOCK_SIZE) && + "MAX_BLOCK_SIZE must be a power of 2"); + assert(MAX_BLOCK_SIZE >= 2 && "MAX_BLOCK_SIZE must be at least 2"); + + Block* firstBlock = nullptr; + + largestBlockSize = + ceilToPow2(size + 1); // We need a spare slot to fit size elements in the block + if (largestBlockSize > MAX_BLOCK_SIZE * 2) { + // We need a spare block in case the producer is writing to a different block the + // consumer is reading from, and wants to enqueue the maximum number of elements. We + // also need a spare element in each block to avoid the ambiguity between front == tail + // meaning "empty" and "full". So the effective number of slots that are guaranteed to + // be usable at any time is the block size - 1 times the number of blocks - 1. Solving + // for size and applying a ceiling to the division gives us (after simplifying): + size_t initialBlockCount = (size + MAX_BLOCK_SIZE * 2 - 3) / (MAX_BLOCK_SIZE - 1); + largestBlockSize = MAX_BLOCK_SIZE; + Block* lastBlock = nullptr; + for (size_t i = 0; i != initialBlockCount; ++i) { + auto block = make_block(largestBlockSize); + if (block == nullptr) { +#ifdef MOODYCAMEL_EXCEPTIONS_ENABLED + throw std::bad_alloc(); +#else + abort(); +#endif + } + if (firstBlock == nullptr) { + firstBlock = block; + } else { + lastBlock->next = block; + } + lastBlock = block; + block->next = firstBlock; + } + } else { + firstBlock = make_block(largestBlockSize); + if (firstBlock == nullptr) { +#ifdef MOODYCAMEL_EXCEPTIONS_ENABLED + throw std::bad_alloc(); +#else + abort(); +#endif + } + firstBlock->next = firstBlock; + } + frontBlock = firstBlock; + tailBlock = firstBlock; + + // Make sure the reader/writer threads will have the initialized memory setup above: + fence(memory_order_sync); + } + + // Note: The queue should not be accessed concurrently while it's + // being moved. It's up to the user to synchronize this. + AE_NO_TSAN ReaderWriterQueue(ReaderWriterQueue&& other) + : frontBlock(other.frontBlock.load()), tailBlock(other.tailBlock.load()), + largestBlockSize(other.largestBlockSize) +#ifndef NDEBUG + , + enqueuing(false), dequeuing(false) +#endif + { + other.largestBlockSize = 32; + Block* b = other.make_block(other.largestBlockSize); + if (b == nullptr) { +#ifdef MOODYCAMEL_EXCEPTIONS_ENABLED + throw std::bad_alloc(); +#else + abort(); +#endif + } + b->next = b; + other.frontBlock = b; + other.tailBlock = b; + } + + // Note: The queue should not be accessed concurrently while it's + // being moved. It's up to the user to synchronize this. + ReaderWriterQueue& operator=(ReaderWriterQueue&& other) AE_NO_TSAN { + Block* b = frontBlock.load(); + frontBlock = other.frontBlock.load(); + other.frontBlock = b; + b = tailBlock.load(); + tailBlock = other.tailBlock.load(); + other.tailBlock = b; + std::swap(largestBlockSize, other.largestBlockSize); + return *this; + } + + // Note: The queue should not be accessed concurrently while it's + // being deleted. It's up to the user to synchronize this. + AE_NO_TSAN ~ReaderWriterQueue() { + // Make sure we get the latest version of all variables from other CPUs: + fence(memory_order_sync); + + // Destroy any remaining objects in queue and free memory + Block* frontBlock_ = frontBlock; + Block* block = frontBlock_; + do { + Block* nextBlock = block->next; + size_t blockFront = block->front; + size_t blockTail = block->tail; + + for (size_t i = blockFront; i != blockTail; i = (i + 1) & block->sizeMask) { + auto element = reinterpret_cast(block->data + i * sizeof(T)); + element->~T(); + (void)element; + } + + auto rawBlock = block->rawThis; + block->~Block(); + std::free(rawBlock); + block = nextBlock; + } while (block != frontBlock_); + } + + // Enqueues a copy of element if there is room in the queue. + // Returns true if the element was enqueued, false otherwise. + // Does not allocate memory. + AE_FORCEINLINE bool try_enqueue(T const& element) AE_NO_TSAN { + return inner_enqueue(element); + } + + // Enqueues a moved copy of element if there is room in the queue. + // Returns true if the element was enqueued, false otherwise. + // Does not allocate memory. + AE_FORCEINLINE bool try_enqueue(T&& element) AE_NO_TSAN { + return inner_enqueue(std::forward(element)); + } + +#if MOODYCAMEL_HAS_EMPLACE + // Like try_enqueue() but with emplace semantics (i.e. construct-in-place). + template + AE_FORCEINLINE bool try_emplace(Args&&... args) AE_NO_TSAN { + return inner_enqueue(std::forward(args)...); + } +#endif + + // Enqueues a copy of element on the queue. + // Allocates an additional block of memory if needed. + // Only fails (returns false) if memory allocation fails. + AE_FORCEINLINE bool enqueue(T const& element) AE_NO_TSAN { + return inner_enqueue(element); + } + + // Enqueues a moved copy of element on the queue. + // Allocates an additional block of memory if needed. + // Only fails (returns false) if memory allocation fails. + AE_FORCEINLINE bool enqueue(T&& element) AE_NO_TSAN { + return inner_enqueue(std::forward(element)); + } + +#if MOODYCAMEL_HAS_EMPLACE + // Like enqueue() but with emplace semantics (i.e. construct-in-place). + template + AE_FORCEINLINE bool emplace(Args&&... args) AE_NO_TSAN { + return inner_enqueue(std::forward(args)...); + } +#endif + + // Attempts to dequeue an element; if the queue is empty, + // returns false instead. If the queue has at least one element, + // moves front to result using operator=, then returns true. + template + bool try_dequeue(U& result) AE_NO_TSAN { +#ifndef NDEBUG + ReentrantGuard guard(this->dequeuing); +#endif + + // High-level pseudocode: + // Remember where the tail block is + // If the front block has an element in it, dequeue it + // Else + // If front block was the tail block when we entered the function, return false + // Else advance to next block and dequeue the item there + + // Note that we have to use the value of the tail block from before we check if the front + // block is full or not, in case the front block is empty and then, before we check if the + // tail block is at the front block or not, the producer fills up the front block *and + // moves on*, which would make us skip a filled block. Seems unlikely, but was consistently + // reproducible in practice. + // In order to avoid overhead in the common case, though, we do a double-checked pattern + // where we have the fast path if the front block is not empty, then read the tail block, + // then re-read the front block and check if it's not empty again, then check if the tail + // block has advanced. + + Block* frontBlock_ = frontBlock.load(); + size_t blockTail = frontBlock_->localTail; + size_t blockFront = frontBlock_->front.load(); + + if (blockFront != blockTail || + blockFront != (frontBlock_->localTail = frontBlock_->tail.load())) { + fence(memory_order_acquire); + + non_empty_front_block: + // Front block not empty, dequeue from here + auto element = reinterpret_cast(frontBlock_->data + blockFront * sizeof(T)); + result = std::move(*element); + element->~T(); + + blockFront = (blockFront + 1) & frontBlock_->sizeMask; + + fence(memory_order_release); + frontBlock_->front = blockFront; + } else if (frontBlock_ != tailBlock.load()) { + fence(memory_order_acquire); + + frontBlock_ = frontBlock.load(); + blockTail = frontBlock_->localTail = frontBlock_->tail.load(); + blockFront = frontBlock_->front.load(); + fence(memory_order_acquire); + + if (blockFront != blockTail) { + // Oh look, the front block isn't empty after all + goto non_empty_front_block; + } + + // Front block is empty but there's another block ahead, advance to it + Block* nextBlock = frontBlock_->next; + // Don't need an acquire fence here since next can only ever be set on the tailBlock, + // and we're not the tailBlock, and we did an acquire earlier after reading tailBlock + // which ensures next is up-to-date on this CPU in case we recently were at tailBlock. + + size_t nextBlockFront = nextBlock->front.load(); + size_t nextBlockTail = nextBlock->localTail = nextBlock->tail.load(); + fence(memory_order_acquire); + + // Since the tailBlock is only ever advanced after being written to, + // we know there's for sure an element to dequeue on it + assert(nextBlockFront != nextBlockTail); + AE_UNUSED(nextBlockTail); + + // We're done with this block, let the producer use it if it needs + fence(memory_order_release); // Expose possibly pending changes to frontBlock->front + // from last dequeue + frontBlock = frontBlock_ = nextBlock; + + compiler_fence(memory_order_release); // Not strictly needed + + auto element = reinterpret_cast(frontBlock_->data + nextBlockFront * sizeof(T)); + + result = std::move(*element); + element->~T(); + + nextBlockFront = (nextBlockFront + 1) & frontBlock_->sizeMask; + + fence(memory_order_release); + frontBlock_->front = nextBlockFront; + } else { + // No elements in current block and no other block to advance to + return false; + } + + return true; + } + + // Returns a pointer to the front element in the queue (the one that + // would be removed next by a call to `try_dequeue` or `pop`). If the + // queue appears empty at the time the method is called, nullptr is + // returned instead. + // Must be called only from the consumer thread. + T* peek() const AE_NO_TSAN { +#ifndef NDEBUG + ReentrantGuard guard(this->dequeuing); +#endif + // See try_dequeue() for reasoning + + Block* frontBlock_ = frontBlock.load(); + size_t blockTail = frontBlock_->localTail; + size_t blockFront = frontBlock_->front.load(); + + if (blockFront != blockTail || + blockFront != (frontBlock_->localTail = frontBlock_->tail.load())) { + fence(memory_order_acquire); + non_empty_front_block: + return reinterpret_cast(frontBlock_->data + blockFront * sizeof(T)); + } else if (frontBlock_ != tailBlock.load()) { + fence(memory_order_acquire); + frontBlock_ = frontBlock.load(); + blockTail = frontBlock_->localTail = frontBlock_->tail.load(); + blockFront = frontBlock_->front.load(); + fence(memory_order_acquire); + + if (blockFront != blockTail) { + goto non_empty_front_block; + } + + Block* nextBlock = frontBlock_->next; + + size_t nextBlockFront = nextBlock->front.load(); + fence(memory_order_acquire); + + assert(nextBlockFront != nextBlock->tail.load()); + return reinterpret_cast(nextBlock->data + nextBlockFront * sizeof(T)); + } + + return nullptr; + } + + // Removes the front element from the queue, if any, without returning it. + // Returns true on success, or false if the queue appeared empty at the time + // `pop` was called. + bool pop() AE_NO_TSAN { +#ifndef NDEBUG + ReentrantGuard guard(this->dequeuing); +#endif + // See try_dequeue() for reasoning + + Block* frontBlock_ = frontBlock.load(); + size_t blockTail = frontBlock_->localTail; + size_t blockFront = frontBlock_->front.load(); + + if (blockFront != blockTail || + blockFront != (frontBlock_->localTail = frontBlock_->tail.load())) { + fence(memory_order_acquire); + + non_empty_front_block: + auto element = reinterpret_cast(frontBlock_->data + blockFront * sizeof(T)); + element->~T(); + + blockFront = (blockFront + 1) & frontBlock_->sizeMask; + + fence(memory_order_release); + frontBlock_->front = blockFront; + } else if (frontBlock_ != tailBlock.load()) { + fence(memory_order_acquire); + frontBlock_ = frontBlock.load(); + blockTail = frontBlock_->localTail = frontBlock_->tail.load(); + blockFront = frontBlock_->front.load(); + fence(memory_order_acquire); + + if (blockFront != blockTail) { + goto non_empty_front_block; + } + + // Front block is empty but there's another block ahead, advance to it + Block* nextBlock = frontBlock_->next; + + size_t nextBlockFront = nextBlock->front.load(); + size_t nextBlockTail = nextBlock->localTail = nextBlock->tail.load(); + fence(memory_order_acquire); + + assert(nextBlockFront != nextBlockTail); + AE_UNUSED(nextBlockTail); + + fence(memory_order_release); + frontBlock = frontBlock_ = nextBlock; + + compiler_fence(memory_order_release); + + auto element = reinterpret_cast(frontBlock_->data + nextBlockFront * sizeof(T)); + element->~T(); + + nextBlockFront = (nextBlockFront + 1) & frontBlock_->sizeMask; + + fence(memory_order_release); + frontBlock_->front = nextBlockFront; + } else { + // No elements in current block and no other block to advance to + return false; + } + + return true; + } + + // Returns the approximate number of items currently in the queue. + // Safe to call from both the producer and consumer threads. + inline size_t size_approx() const AE_NO_TSAN { + size_t result = 0; + Block* frontBlock_ = frontBlock.load(); + Block* block = frontBlock_; + do { + fence(memory_order_acquire); + size_t blockFront = block->front.load(); + size_t blockTail = block->tail.load(); + result += (blockTail - blockFront) & block->sizeMask; + block = block->next.load(); + } while (block != frontBlock_); + return result; + } + + // Returns the total number of items that could be enqueued without incurring + // an allocation when this queue is empty. + // Safe to call from both the producer and consumer threads. + // + // NOTE: The actual capacity during usage may be different depending on the consumer. + // If the consumer is removing elements concurrently, the producer cannot add to + // the block the consumer is removing from until it's completely empty, except in + // the case where the producer was writing to the same block the consumer was + // reading from the whole time. + inline size_t max_capacity() const { + size_t result = 0; + Block* frontBlock_ = frontBlock.load(); + Block* block = frontBlock_; + do { + fence(memory_order_acquire); + result += block->sizeMask; + block = block->next.load(); + } while (block != frontBlock_); + return result; + } + +private: + enum AllocationMode { CanAlloc, CannotAlloc }; + +#if MOODYCAMEL_HAS_EMPLACE + template + bool inner_enqueue(Args&&... args) AE_NO_TSAN +#else + template + bool inner_enqueue(U&& element) AE_NO_TSAN +#endif + { +#ifndef NDEBUG + ReentrantGuard guard(this->enqueuing); +#endif + + // High-level pseudocode (assuming we're allowed to alloc a new block): + // If room in tail block, add to tail + // Else check next block + // If next block is not the head block, enqueue on next block + // Else create a new block and enqueue there + // Advance tail to the block we just enqueued to + + Block* tailBlock_ = tailBlock.load(); + size_t blockFront = tailBlock_->localFront; + size_t blockTail = tailBlock_->tail.load(); + + size_t nextBlockTail = (blockTail + 1) & tailBlock_->sizeMask; + if (nextBlockTail != blockFront || + nextBlockTail != (tailBlock_->localFront = tailBlock_->front.load())) { + fence(memory_order_acquire); + // This block has room for at least one more element + char* location = tailBlock_->data + blockTail * sizeof(T); +#if MOODYCAMEL_HAS_EMPLACE + new (location) T(std::forward(args)...); +#else + new (location) T(std::forward(element)); +#endif + + fence(memory_order_release); + tailBlock_->tail = nextBlockTail; + } else { + fence(memory_order_acquire); + if (tailBlock_->next.load() != frontBlock) { + // Note that the reason we can't advance to the frontBlock and start adding new + // entries there is because if we did, then dequeue would stay in that block, + // eventually reading the new values, instead of advancing to the next full block + // (whose values were enqueued first and so should be consumed first). + + fence(memory_order_acquire); // Ensure we get latest writes if we got the latest + // frontBlock + + // tailBlock is full, but there's a free block ahead, use it + Block* tailBlockNext = tailBlock_->next.load(); + size_t nextBlockFront = tailBlockNext->localFront = tailBlockNext->front.load(); + nextBlockTail = tailBlockNext->tail.load(); + fence(memory_order_acquire); + + // This block must be empty since it's not the head block and we + // go through the blocks in a circle + assert(nextBlockFront == nextBlockTail); + tailBlockNext->localFront = nextBlockFront; + + char* location = tailBlockNext->data + nextBlockTail * sizeof(T); +#if MOODYCAMEL_HAS_EMPLACE + new (location) T(std::forward(args)...); +#else + new (location) T(std::forward(element)); +#endif + + tailBlockNext->tail = (nextBlockTail + 1) & tailBlockNext->sizeMask; + + fence(memory_order_release); + tailBlock = tailBlockNext; + } else if (canAlloc == CanAlloc) { + // tailBlock is full and there's no free block ahead; create a new block + auto newBlockSize = + largestBlockSize >= MAX_BLOCK_SIZE ? largestBlockSize : largestBlockSize * 2; + auto newBlock = make_block(newBlockSize); + if (newBlock == nullptr) { + // Could not allocate a block! + return false; + } + largestBlockSize = newBlockSize; + +#if MOODYCAMEL_HAS_EMPLACE + new (newBlock->data) T(std::forward(args)...); +#else + new (newBlock->data) T(std::forward(element)); +#endif + assert(newBlock->front == 0); + newBlock->tail = newBlock->localTail = 1; + + newBlock->next = tailBlock_->next.load(); + tailBlock_->next = newBlock; + + // Might be possible for the dequeue thread to see the new tailBlock->next + // *without* seeing the new tailBlock value, but this is OK since it can't + // advance to the next block until tailBlock is set anyway (because the only + // case where it could try to read the next is if it's already at the tailBlock, + // and it won't advance past tailBlock in any circumstance). + + fence(memory_order_release); + tailBlock = newBlock; + } else if (canAlloc == CannotAlloc) { + // Would have had to allocate a new block to enqueue, but not allowed + return false; + } else { + assert(false && "Should be unreachable code"); + return false; + } + } + + return true; + } + + // Disable copying + ReaderWriterQueue(ReaderWriterQueue const&) {} + + // Disable assignment + ReaderWriterQueue& operator=(ReaderWriterQueue const&) {} + + AE_FORCEINLINE static size_t ceilToPow2(size_t x) { + // From http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + --x; + x |= x >> 1; + x |= x >> 2; + x |= x >> 4; + for (size_t i = 1; i < sizeof(size_t); i <<= 1) { + x |= x >> (i << 3); + } + ++x; + return x; + } + + template + static AE_FORCEINLINE char* align_for(char* ptr) AE_NO_TSAN { + const std::size_t alignment = std::alignment_of::value; + return ptr + (alignment - (reinterpret_cast(ptr) % alignment)) % alignment; + } + +private: +#ifndef NDEBUG + struct ReentrantGuard { + AE_NO_TSAN ReentrantGuard(weak_atomic& _inSection) : inSection(_inSection) { + assert(!inSection && + "Concurrent (or re-entrant) enqueue or dequeue operation detected (only one " + "thread at a time may hold the producer or consumer role)"); + inSection = true; + } + + AE_NO_TSAN ~ReentrantGuard() { + inSection = false; + } + + private: + ReentrantGuard& operator=(ReentrantGuard const&); + + private: + weak_atomic& inSection; + }; +#endif + + struct Block { + // Avoid false-sharing by putting highly contended variables on their own cache lines + weak_atomic front; // (Atomic) Elements are read from here + size_t localTail; // An uncontended shadow copy of tail, owned by the consumer + + char cachelineFiller0[MOODYCAMEL_CACHE_LINE_SIZE - sizeof(weak_atomic) - + sizeof(size_t)]; + weak_atomic tail; // (Atomic) Elements are enqueued here + size_t localFront; + + char cachelineFiller1[MOODYCAMEL_CACHE_LINE_SIZE - sizeof(weak_atomic) - + sizeof(size_t)]; // next isn't very contended, but we don't want it on + // the same cache line as tail (which is) + weak_atomic next; // (Atomic) + + char* data; // Contents (on heap) are aligned to T's alignment + + const size_t sizeMask; + + // size must be a power of two (and greater than 0) + AE_NO_TSAN Block(size_t const& _size, char* _rawThis, char* _data) + : front(0UL), localTail(0), tail(0UL), localFront(0), next(nullptr), data(_data), + sizeMask(_size - 1), rawThis(_rawThis) {} + + private: + // C4512 - Assignment operator could not be generated + Block& operator=(Block const&); + + public: + char* rawThis; + }; + + static Block* make_block(size_t capacity) AE_NO_TSAN { + // Allocate enough memory for the block itself, as well as all the elements it will contain + auto size = sizeof(Block) + std::alignment_of::value - 1; + size += sizeof(T) * capacity + std::alignment_of::value - 1; + auto newBlockRaw = static_cast(std::malloc(size)); + if (newBlockRaw == nullptr) { + return nullptr; + } + + auto newBlockAligned = align_for(newBlockRaw); + auto newBlockData = align_for(newBlockAligned + sizeof(Block)); + return new (newBlockAligned) Block(capacity, newBlockRaw, newBlockData); + } + +private: + weak_atomic frontBlock; // (Atomic) Elements are dequeued from this block + + char cachelineFiller[MOODYCAMEL_CACHE_LINE_SIZE - sizeof(weak_atomic)]; + weak_atomic tailBlock; // (Atomic) Elements are enqueued to this block + + size_t largestBlockSize; + +#ifndef NDEBUG + weak_atomic enqueuing; + mutable weak_atomic dequeuing; +#endif +}; + +// Like ReaderWriterQueue, but also providees blocking operations +template +class BlockingReaderWriterQueue { +private: + typedef ::Common::ReaderWriterQueue ReaderWriterQueue; + +public: + explicit BlockingReaderWriterQueue(size_t size = 15) AE_NO_TSAN + : inner(size), + sema(new spsc_sema::LightweightSemaphore()) {} + + BlockingReaderWriterQueue(BlockingReaderWriterQueue&& other) AE_NO_TSAN + : inner(std::move(other.inner)), + sema(std::move(other.sema)) {} + + BlockingReaderWriterQueue& operator=(BlockingReaderWriterQueue&& other) AE_NO_TSAN { + std::swap(sema, other.sema); + std::swap(inner, other.inner); + return *this; + } + + // Enqueues a copy of element if there is room in the queue. + // Returns true if the element was enqueued, false otherwise. + // Does not allocate memory. + AE_FORCEINLINE bool try_enqueue(T const& element) AE_NO_TSAN { + if (inner.try_enqueue(element)) { + sema->signal(); + return true; + } + return false; + } + + // Enqueues a moved copy of element if there is room in the queue. + // Returns true if the element was enqueued, false otherwise. + // Does not allocate memory. + AE_FORCEINLINE bool try_enqueue(T&& element) AE_NO_TSAN { + if (inner.try_enqueue(std::forward(element))) { + sema->signal(); + return true; + } + return false; + } + +#if MOODYCAMEL_HAS_EMPLACE + // Like try_enqueue() but with emplace semantics (i.e. construct-in-place). + template + AE_FORCEINLINE bool try_emplace(Args&&... args) AE_NO_TSAN { + if (inner.try_emplace(std::forward(args)...)) { + sema->signal(); + return true; + } + return false; + } +#endif + + // Enqueues a copy of element on the queue. + // Allocates an additional block of memory if needed. + // Only fails (returns false) if memory allocation fails. + AE_FORCEINLINE bool enqueue(T const& element) AE_NO_TSAN { + if (inner.enqueue(element)) { + sema->signal(); + return true; + } + return false; + } + + // Enqueues a moved copy of element on the queue. + // Allocates an additional block of memory if needed. + // Only fails (returns false) if memory allocation fails. + AE_FORCEINLINE bool enqueue(T&& element) AE_NO_TSAN { + if (inner.enqueue(std::forward(element))) { + sema->signal(); + return true; + } + return false; + } + +#if MOODYCAMEL_HAS_EMPLACE + // Like enqueue() but with emplace semantics (i.e. construct-in-place). + template + AE_FORCEINLINE bool emplace(Args&&... args) AE_NO_TSAN { + if (inner.emplace(std::forward(args)...)) { + sema->signal(); + return true; + } + return false; + } +#endif + + // Attempts to dequeue an element; if the queue is empty, + // returns false instead. If the queue has at least one element, + // moves front to result using operator=, then returns true. + template + bool try_dequeue(U& result) AE_NO_TSAN { + if (sema->tryWait()) { + bool success = inner.try_dequeue(result); + assert(success); + AE_UNUSED(success); + return true; + } + return false; + } + + // Attempts to dequeue an element; if the queue is empty, + // waits until an element is available, then dequeues it. + template + void wait_dequeue(U& result) AE_NO_TSAN { + while (!sema->wait()) + ; + bool success = inner.try_dequeue(result); + AE_UNUSED(result); + assert(success); + AE_UNUSED(success); + } + + // Attempts to dequeue an element; if the queue is empty, + // waits until an element is available up to the specified timeout, + // then dequeues it and returns true, or returns false if the timeout + // expires before an element can be dequeued. + // Using a negative timeout indicates an indefinite timeout, + // and is thus functionally equivalent to calling wait_dequeue. + template + bool wait_dequeue_timed(U& result, std::int64_t timeout_usecs) AE_NO_TSAN { + if (!sema->wait(timeout_usecs)) { + return false; + } + bool success = inner.try_dequeue(result); + AE_UNUSED(result); + assert(success); + AE_UNUSED(success); + return true; + } + +#if __cplusplus > 199711L || _MSC_VER >= 1700 + // Attempts to dequeue an element; if the queue is empty, + // waits until an element is available up to the specified timeout, + // then dequeues it and returns true, or returns false if the timeout + // expires before an element can be dequeued. + // Using a negative timeout indicates an indefinite timeout, + // and is thus functionally equivalent to calling wait_dequeue. + template + inline bool wait_dequeue_timed(U& result, + std::chrono::duration const& timeout) AE_NO_TSAN { + return wait_dequeue_timed( + result, std::chrono::duration_cast(timeout).count()); + } +#endif + + // Returns a pointer to the front element in the queue (the one that + // would be removed next by a call to `try_dequeue` or `pop`). If the + // queue appears empty at the time the method is called, nullptr is + // returned instead. + // Must be called only from the consumer thread. + AE_FORCEINLINE T* peek() const AE_NO_TSAN { + return inner.peek(); + } + + // Removes the front element from the queue, if any, without returning it. + // Returns true on success, or false if the queue appeared empty at the time + // `pop` was called. + AE_FORCEINLINE bool pop() AE_NO_TSAN { + if (sema->tryWait()) { + bool result = inner.pop(); + assert(result); + AE_UNUSED(result); + return true; + } + return false; + } + + // Returns the approximate number of items currently in the queue. + // Safe to call from both the producer and consumer threads. + AE_FORCEINLINE size_t size_approx() const AE_NO_TSAN { + return sema->availableApprox(); + } + + // Returns the total number of items that could be enqueued without incurring + // an allocation when this queue is empty. + // Safe to call from both the producer and consumer threads. + // + // NOTE: The actual capacity during usage may be different depending on the consumer. + // If the consumer is removing elements concurrently, the producer cannot add to + // the block the consumer is removing from until it's completely empty, except in + // the case where the producer was writing to the same block the consumer was + // reading from the whole time. + AE_FORCEINLINE size_t max_capacity() const { + return inner.max_capacity(); + } + +private: + // Disable copying & assignment + BlockingReaderWriterQueue(BlockingReaderWriterQueue const&) {} + BlockingReaderWriterQueue& operator=(BlockingReaderWriterQueue const&) {} + +private: + ReaderWriterQueue inner; + std::unique_ptr sema; +}; + +} // namespace Common + +#ifdef AE_VCPP +#pragma warning(pop) +#endif diff --git a/src/common/settings.cpp b/src/common/settings.cpp index 751549583..9ffff8684 100755 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -62,7 +62,8 @@ void LogSettings() { log_setting("Renderer_UseAsynchronousShaders", values.use_asynchronous_shaders.GetValue()); log_setting("Renderer_AnisotropicFilteringLevel", values.max_anisotropy.GetValue()); log_setting("Audio_OutputEngine", values.sink_id.GetValue()); - log_setting("Audio_OutputDevice", values.audio_device_id.GetValue()); + log_setting("Audio_OutputDevice", values.audio_output_device_id.GetValue()); + log_setting("Audio_InputDevice", values.audio_input_device_id.GetValue()); log_setting("DataStorage_UseVirtualSd", values.use_virtual_sd.GetValue()); log_path("DataStorage_CacheDir", Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir)); log_path("DataStorage_ConfigDir", Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir)); diff --git a/src/common/settings.h b/src/common/settings.h index 3583a2e70..eb1a02f4b 100755 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -371,10 +371,12 @@ struct TouchFromButtonMap { struct Values { // Audio - Setting audio_device_id{"auto", "output_device"}; Setting sink_id{"auto", "output_engine"}; + Setting audio_output_device_id{"auto", "output_device"}; + Setting audio_input_device_id{"auto", "input_device"}; Setting audio_muted{false, "audio_muted"}; SwitchableSetting volume{100, 0, 100, "volume"}; + Setting dump_audio_commands{false, "dump_audio_commands"}; // Core SwitchableSetting use_multi_core{true, "use_multi_core"}; diff --git a/src/core/core.cpp b/src/core/core.cpp index bad7c6dcf..9709131cc 100755 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -8,6 +8,7 @@ #include #include +#include "audio_core/audio_core.h" #include "common/fs/fs.h" #include "common/logging/log.h" #include "common/microprofile.h" @@ -141,6 +142,8 @@ struct System::Impl { cpu_manager.Pause(false); is_paused = false; + audio_core->PauseSinks(false); + return status; } @@ -148,6 +151,8 @@ struct System::Impl { std::unique_lock lk(suspend_guard); status = SystemResultStatus::Success; + audio_core->PauseSinks(true); + core_timing.SyncPause(true); kernel.Suspend(true); cpu_manager.Pause(true); @@ -156,6 +161,11 @@ struct System::Impl { return status; } + bool IsPaused() const { + std::unique_lock lk(suspend_guard); + return is_paused; + } + std::unique_lock StallProcesses() { std::unique_lock lk(suspend_guard); kernel.Suspend(true); @@ -219,6 +229,8 @@ struct System::Impl { return SystemResultStatus::ErrorVideoCore; } + audio_core = std::make_unique(system); + service_manager = std::make_shared(kernel); services = std::make_unique(service_manager, system); @@ -294,7 +306,7 @@ struct System::Impl { if (Settings::values.gamecard_current_game) { fs_controller.SetGameCard(GetGameFileFromPath(virtual_filesystem, filepath)); } else if (!Settings::values.gamecard_path.GetValue().empty()) { - const auto gamecard_path = Settings::values.gamecard_path.GetValue(); + const auto& gamecard_path = Settings::values.gamecard_path.GetValue(); fs_controller.SetGameCard(GetGameFileFromPath(virtual_filesystem, gamecard_path)); } } @@ -312,6 +324,8 @@ struct System::Impl { } void Shutdown() { + SetShuttingDown(true); + // Log last frame performance stats if game was loded if (perf_stats) { const auto perf_results = GetAndResetPerfStats(); @@ -335,6 +349,7 @@ struct System::Impl { } debugger.reset(); + kernel.CloseServices(); services.reset(); service_manager.reset(); cheat_engine.reset(); @@ -343,6 +358,7 @@ struct System::Impl { time_manager.Shutdown(); core_timing.Shutdown(); app_loader.reset(); + audio_core.reset(); gpu_core.reset(); host1x_core.reset(); perf_stats.reset(); @@ -353,6 +369,14 @@ struct System::Impl { LOG_DEBUG(Core, "Shutdown OK"); } + bool IsShuttingDown() const { + return is_shutting_down; + } + + void SetShuttingDown(bool shutting_down) { + is_shutting_down = shutting_down; + } + Loader::ResultStatus GetGameName(std::string& out) const { if (app_loader == nullptr) return Loader::ResultStatus::ErrorNotInitialized; @@ -395,8 +419,9 @@ struct System::Impl { return perf_stats->GetAndResetStats(core_timing.GetGlobalTimeUs()); } - std::mutex suspend_guard; + mutable std::mutex suspend_guard; bool is_paused{}; + std::atomic is_shutting_down{}; Timing::CoreTiming core_timing; Kernel::KernelCore kernel; @@ -406,10 +431,11 @@ struct System::Impl { std::unique_ptr content_provider; Service::FileSystem::FileSystemController fs_controller; /// AppLoader used to load the current executing application + std::unique_ptr host1x_core; std::unique_ptr app_loader; std::unique_ptr gpu_core; - std::unique_ptr host1x_core; std::unique_ptr device_memory; + std::unique_ptr audio_core; Core::Memory::Memory memory; Core::HID::HIDCore hid_core; CpuManager cpu_manager; @@ -482,6 +508,10 @@ SystemResultStatus System::Pause() { return impl->Pause(); } +bool System::IsPaused() const { + return impl->IsPaused(); +} + void System::InvalidateCpuInstructionCaches() { impl->kernel.InvalidateAllInstructionCaches(); } @@ -494,6 +524,14 @@ void System::Shutdown() { impl->Shutdown(); } +bool System::IsShuttingDown() const { + return impl->IsShuttingDown(); +} + +void System::SetShuttingDown(bool shutting_down) { + impl->SetShuttingDown(shutting_down); +} + void System::DetachDebugger() { if (impl->debugger) { impl->debugger->NotifyShutdown(); @@ -603,14 +641,6 @@ const Core::Memory::Memory& System::Memory() const { return impl->memory; } -Tegra::GPU& System::GPU() { - return *impl->gpu_core; -} - -const Tegra::GPU& System::GPU() const { - return *impl->gpu_core; -} - Tegra::Host1x::Host1x& System::Host1x() { return *impl->host1x_core; } @@ -619,6 +649,14 @@ const Tegra::Host1x::Host1x& System::Host1x() const { return *impl->host1x_core; } +Tegra::GPU& System::GPU() { + return *impl->gpu_core; +} + +const Tegra::GPU& System::GPU() const { + return *impl->gpu_core; +} + VideoCore::RendererBase& System::Renderer() { return impl->gpu_core->Renderer(); } @@ -643,6 +681,14 @@ const HID::HIDCore& System::HIDCore() const { return impl->hid_core; } +AudioCore::AudioCore& System::AudioCore() { + return *impl->audio_core; +} + +const AudioCore::AudioCore& System::AudioCore() const { + return *impl->audio_core; +} + Timing::CoreTiming& System::CoreTiming() { return impl->core_timing; } diff --git a/src/core/core.h b/src/core/core.h index 8859f43c7..48dbe40fa 100755 --- a/src/core/core.h +++ b/src/core/core.h @@ -84,6 +84,10 @@ namespace VideoCore { class RendererBase; } // namespace VideoCore +namespace AudioCore { +class AudioCore; +} // namespace AudioCore + namespace Core::Timing { class CoreTiming; } @@ -147,6 +151,9 @@ public: */ [[nodiscard]] SystemResultStatus Pause(); + /// Check if the core is currently paused. + [[nodiscard]] bool IsPaused() const; + /** * Invalidate the CPU instruction caches * This function should only be used by GDB Stub to support breakpoints, memory updates and @@ -159,6 +166,12 @@ public: /// Shutdown the emulated system. void Shutdown(); + /// Check if the core is shutting down. + [[nodiscard]] bool IsShuttingDown() const; + + /// Set the shutting down state. + void SetShuttingDown(bool shutting_down); + /// Forcibly detach the debugger if it is running. void DetachDebugger(); @@ -255,6 +268,12 @@ public: /// Gets an immutable reference to the renderer. [[nodiscard]] const VideoCore::RendererBase& Renderer() const; + /// Gets a mutable reference to the audio interface + [[nodiscard]] AudioCore::AudioCore& AudioCore(); + + /// Gets an immutable reference to the audio interface. + [[nodiscard]] const AudioCore::AudioCore& AudioCore() const; + /// Gets the global scheduler [[nodiscard]] Kernel::GlobalSchedulerContext& GlobalSchedulerContext(); diff --git a/src/core/core_timing.cpp b/src/core/core_timing.cpp index 07b8e356b..d394573bb 100755 --- a/src/core/core_timing.cpp +++ b/src/core/core_timing.cpp @@ -26,6 +26,7 @@ struct CoreTiming::Event { u64 fifo_order; std::uintptr_t user_data; std::weak_ptr type; + u64 reschedule_time; // Sort by time, unless the times are the same, in which case sort by // the order added to the queue @@ -134,7 +135,40 @@ void CoreTiming::ScheduleEvent(std::chrono::nanoseconds ns_into_future, std::unique_lock main_lock(event_mutex); const u64 timeout = static_cast((GetGlobalTimeNs() + ns_into_future).count()); - event_queue.emplace_back(Event{timeout, event_fifo_id++, user_data, event_type}); + event_queue.emplace_back(Event{timeout, event_fifo_id++, user_data, event_type, 0}); + pending_events.fetch_add(1, std::memory_order_relaxed); + + std::push_heap(event_queue.begin(), event_queue.end(), std::greater<>()); + + if (is_multicore) { + event_cv.notify_one(); + } +} + +void CoreTiming::ScheduleEventAt(std::chrono::nanoseconds next_time, + const std::shared_ptr& event_type, + std::uintptr_t user_data) { + std::unique_lock main_lock(event_mutex); + const u64 timeout = static_cast(next_time.count()); + + event_queue.emplace_back(Event{timeout, event_fifo_id++, user_data, event_type, 0}); + pending_events.fetch_add(1, std::memory_order_relaxed); + + std::push_heap(event_queue.begin(), event_queue.end(), std::greater<>()); + + if (is_multicore) { + event_cv.notify_one(); + } +} + +void CoreTiming::ScheduleLoopingEvent(std::chrono::nanoseconds time, + const std::shared_ptr& event_type, + std::uintptr_t user_data) { + std::unique_lock main_lock(event_mutex); + const u64 timeout = static_cast((GetGlobalTimeNs() + time).count()); + + event_queue.emplace_back( + Event{timeout, event_fifo_id++, user_data, event_type, static_cast(time.count())}); pending_events.fetch_add(1, std::memory_order_relaxed); std::push_heap(event_queue.begin(), event_queue.end(), std::greater<>()); @@ -233,6 +267,13 @@ std::optional CoreTiming::Advance() { pending_events.fetch_sub(1, std::memory_order_relaxed); } + if (evt.reschedule_time != 0) { + event_queue.emplace_back( + Event{global_timer + evt.reschedule_time - (global_timer - evt.time), + event_fifo_id++, evt.user_data, evt.type, evt.reschedule_time}); + std::push_heap(event_queue.begin(), event_queue.end(), std::greater<>()); + } + global_timer = GetGlobalTimeNs().count(); } diff --git a/src/core/core_timing.h b/src/core/core_timing.h index c52bffb3b..4500dccdf 100755 --- a/src/core/core_timing.h +++ b/src/core/core_timing.h @@ -95,6 +95,17 @@ public: void ScheduleEvent(std::chrono::nanoseconds ns_into_future, const std::shared_ptr& event_type, std::uintptr_t user_data = 0); + /// Schedules an event with an absolute time in core timing + void ScheduleEventAt(std::chrono::nanoseconds next_time, + const std::shared_ptr& event_type, + std::uintptr_t user_data = 0); + + /// Schedules an event which will automatically re-schedule itself with the given time, until + /// unscheduled + void ScheduleLoopingEvent(std::chrono::nanoseconds time, + const std::shared_ptr& event_type, + std::uintptr_t user_data = 0); + void UnscheduleEvent(const std::shared_ptr& event_type, std::uintptr_t user_data); /// We only permit one event of each type in the queue at a time. diff --git a/src/core/hle/kernel/hle_ipc.cpp b/src/core/hle/kernel/hle_ipc.cpp index 45135a07f..5b3feec66 100755 --- a/src/core/hle/kernel/hle_ipc.cpp +++ b/src/core/hle/kernel/hle_ipc.cpp @@ -287,18 +287,52 @@ std::size_t HLERequestContext::WriteBuffer(const void* buffer, std::size_t size, BufferDescriptorB().size() > buffer_index && BufferDescriptorB()[buffer_index].Size() >= size, { return 0; }, "BufferDescriptorB is invalid, index={}, size={}", buffer_index, size); - memory.WriteBlock(BufferDescriptorB()[buffer_index].Address(), buffer, size); + WriteBufferB(buffer, size, buffer_index); } else { ASSERT_OR_EXECUTE_MSG( BufferDescriptorC().size() > buffer_index && BufferDescriptorC()[buffer_index].Size() >= size, { return 0; }, "BufferDescriptorC is invalid, index={}, size={}", buffer_index, size); - memory.WriteBlock(BufferDescriptorC()[buffer_index].Address(), buffer, size); + WriteBufferC(buffer, size, buffer_index); } return size; } +std::size_t HLERequestContext::WriteBufferB(const void* buffer, std::size_t size, + std::size_t buffer_index) const { + if (buffer_index >= BufferDescriptorB().size() || size == 0) { + return 0; + } + + const auto buffer_size{BufferDescriptorB()[buffer_index].Size()}; + if (size > buffer_size) { + LOG_CRITICAL(Core, "size ({:016X}) is greater than buffer_size ({:016X})", size, + buffer_size); + size = buffer_size; // TODO(bunnei): This needs to be HW tested + } + + memory.WriteBlock(BufferDescriptorB()[buffer_index].Address(), buffer, size); + return size; +} + +std::size_t HLERequestContext::WriteBufferC(const void* buffer, std::size_t size, + std::size_t buffer_index) const { + if (buffer_index >= BufferDescriptorC().size() || size == 0) { + return 0; + } + + const auto buffer_size{BufferDescriptorC()[buffer_index].Size()}; + if (size > buffer_size) { + LOG_CRITICAL(Core, "size ({:016X}) is greater than buffer_size ({:016X})", size, + buffer_size); + size = buffer_size; // TODO(bunnei): This needs to be HW tested + } + + memory.WriteBlock(BufferDescriptorC()[buffer_index].Address(), buffer, size); + return size; +} + std::size_t HLERequestContext::GetReadBufferSize(std::size_t buffer_index) const { const bool is_buffer_a{BufferDescriptorA().size() > buffer_index && BufferDescriptorA()[buffer_index].Size()}; diff --git a/src/core/hle/kernel/hle_ipc.h b/src/core/hle/kernel/hle_ipc.h index d3abeee85..99265ce90 100755 --- a/src/core/hle/kernel/hle_ipc.h +++ b/src/core/hle/kernel/hle_ipc.h @@ -277,6 +277,14 @@ public: std::size_t WriteBuffer(const void* buffer, std::size_t size, std::size_t buffer_index = 0) const; + /// Helper function to write buffer B + std::size_t WriteBufferB(const void* buffer, std::size_t size, + std::size_t buffer_index = 0) const; + + /// Helper function to write buffer C + std::size_t WriteBufferC(const void* buffer, std::size_t size, + std::size_t buffer_index = 0) const; + /* Helper function to write a buffer using the appropriate buffer descriptor * * @tparam T an arbitrary container that satisfies the diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp index 0eb0b98b5..42f321e2b 100755 --- a/src/core/hle/kernel/kernel.cpp +++ b/src/core/hle/kernel/kernel.cpp @@ -95,19 +95,7 @@ struct KernelCore::Impl { process_list.clear(); - // Close all open server sessions and ports. - std::unordered_set server_objects_; - { - std::scoped_lock lk(server_objects_lock); - server_objects_ = server_objects; - server_objects.clear(); - } - for (auto* server_object : server_objects_) { - server_object->Close(); - } - - // Ensures all service threads gracefully shutdown. - ClearServiceThreads(); + CloseServices(); next_object_id = 0; next_kernel_process_id = KProcess::InitialKIPIDMin; @@ -191,6 +179,22 @@ struct KernelCore::Impl { global_object_list_container.reset(); } + void CloseServices() { + // Close all open server sessions and ports. + std::unordered_set server_objects_; + { + std::scoped_lock lk(server_objects_lock); + server_objects_ = server_objects; + server_objects.clear(); + } + for (auto* server_object : server_objects_) { + server_object->Close(); + } + + // Ensures all service threads gracefully shutdown. + ClearServiceThreads(); + } + void InitializePhysicalCores() { exclusive_monitor = Core::MakeExclusiveMonitor(system.Memory(), Core::Hardware::NUM_CPU_CORES); @@ -812,6 +816,10 @@ void KernelCore::Shutdown() { impl->Shutdown(); } +void KernelCore::CloseServices() { + impl->CloseServices(); +} + const KResourceLimit* KernelCore::GetSystemResourceLimit() const { return impl->system_resource_limit; } diff --git a/src/core/hle/kernel/kernel.h b/src/core/hle/kernel/kernel.h index cae98e2d8..7ecb708f4 100755 --- a/src/core/hle/kernel/kernel.h +++ b/src/core/hle/kernel/kernel.h @@ -109,6 +109,9 @@ public: /// Clears all resources in use by the kernel instance. void Shutdown(); + /// Close all active services in use by the kernel instance. + void CloseServices(); + /// Retrieves a shared pointer to the system resource limit instance. const KResourceLimit* GetSystemResourceLimit() const; diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index d35644e73..769320212 100755 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -5,7 +5,6 @@ #include #include #include -#include "audio_core/audio_renderer.h" #include "common/settings.h" #include "core/core.h" #include "core/file_sys/control_metadata.h" @@ -285,7 +284,7 @@ ISelfController::ISelfController(Core::System& system_, NVFlinger::NVFlinger& nv {62, &ISelfController::SetIdleTimeDetectionExtension, "SetIdleTimeDetectionExtension"}, {63, &ISelfController::GetIdleTimeDetectionExtension, "GetIdleTimeDetectionExtension"}, {64, nullptr, "SetInputDetectionSourceSet"}, - {65, nullptr, "ReportUserIsActive"}, + {65, &ISelfController::ReportUserIsActive, "ReportUserIsActive"}, {66, nullptr, "GetCurrentIlluminance"}, {67, nullptr, "IsIlluminanceAvailable"}, {68, &ISelfController::SetAutoSleepDisabled, "SetAutoSleepDisabled"}, @@ -517,6 +516,13 @@ void ISelfController::GetIdleTimeDetectionExtension(Kernel::HLERequestContext& c rb.Push(idle_time_detection_extension); } +void ISelfController::ReportUserIsActive(Kernel::HLERequestContext& ctx) { + LOG_WARNING(Service_AM, "(STUBBED) called"); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); +} + void ISelfController::SetAutoSleepDisabled(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; is_auto_sleep_disabled = rp.Pop(); diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h index 988ead215..03a7bf47f 100755 --- a/src/core/hle/service/am/am.h +++ b/src/core/hle/service/am/am.h @@ -174,6 +174,7 @@ private: void SetHandlesRequestToDisplay(Kernel::HLERequestContext& ctx); void SetIdleTimeDetectionExtension(Kernel::HLERequestContext& ctx); void GetIdleTimeDetectionExtension(Kernel::HLERequestContext& ctx); + void ReportUserIsActive(Kernel::HLERequestContext& ctx); void SetAutoSleepDisabled(Kernel::HLERequestContext& ctx); void IsAutoSleepDisabled(Kernel::HLERequestContext& ctx); void GetAccumulatedSuspendedTickValue(Kernel::HLERequestContext& ctx); diff --git a/src/core/hle/service/audio/audin_u.cpp b/src/core/hle/service/audio/audin_u.cpp index 18d3ae682..48a9a73a0 100755 --- a/src/core/hle/service/audio/audin_u.cpp +++ b/src/core/hle/service/audio/audin_u.cpp @@ -1,68 +1,211 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include "audio_core/in/audio_in_system.h" +#include "audio_core/renderer/audio_device.h" +#include "common/common_funcs.h" #include "common/logging/log.h" +#include "common/string_util.h" #include "core/core.h" #include "core/hle/ipc_helpers.h" #include "core/hle/kernel/k_event.h" #include "core/hle/service/audio/audin_u.h" namespace Service::Audio { +using namespace AudioCore::AudioIn; -IAudioIn::IAudioIn(Core::System& system_) - : ServiceFramework{system_, "IAudioIn"}, service_context{system_, "IAudioIn"} { - // clang-format off - static const FunctionInfo functions[] = { - {0, nullptr, "GetAudioInState"}, - {1, &IAudioIn::Start, "Start"}, - {2, nullptr, "Stop"}, - {3, nullptr, "AppendAudioInBuffer"}, - {4, &IAudioIn::RegisterBufferEvent, "RegisterBufferEvent"}, - {5, nullptr, "GetReleasedAudioInBuffer"}, - {6, nullptr, "ContainsAudioInBuffer"}, - {7, nullptr, "AppendUacInBuffer"}, - {8, &IAudioIn::AppendAudioInBufferAuto, "AppendAudioInBufferAuto"}, - {9, nullptr, "GetReleasedAudioInBuffersAuto"}, - {10, nullptr, "AppendUacInBufferAuto"}, - {11, nullptr, "GetAudioInBufferCount"}, - {12, nullptr, "SetDeviceGain"}, - {13, nullptr, "GetDeviceGain"}, - {14, nullptr, "FlushAudioInBuffers"}, - }; - // clang-format on +class IAudioIn final : public ServiceFramework { +public: + explicit IAudioIn(Core::System& system_, Manager& manager, size_t session_id, + std::string& device_name, const AudioInParameter& in_params, u32 handle, + u64 applet_resource_user_id) + : ServiceFramework{system_, "IAudioIn"}, + service_context{system_, "IAudioIn"}, event{service_context.CreateEvent("AudioInEvent")}, + impl{std::make_shared(system_, manager, event, session_id)} { + // clang-format off + static const FunctionInfo functions[] = { + {0, &IAudioIn::GetAudioInState, "GetAudioInState"}, + {1, &IAudioIn::Start, "Start"}, + {2, &IAudioIn::Stop, "Stop"}, + {3, &IAudioIn::AppendAudioInBuffer, "AppendAudioInBuffer"}, + {4, &IAudioIn::RegisterBufferEvent, "RegisterBufferEvent"}, + {5, &IAudioIn::GetReleasedAudioInBuffer, "GetReleasedAudioInBuffer"}, + {6, &IAudioIn::ContainsAudioInBuffer, "ContainsAudioInBuffer"}, + {7, &IAudioIn::AppendAudioInBuffer, "AppendUacInBuffer"}, + {8, &IAudioIn::AppendAudioInBuffer, "AppendAudioInBufferAuto"}, + {9, &IAudioIn::GetReleasedAudioInBuffer, "GetReleasedAudioInBuffersAuto"}, + {10, &IAudioIn::AppendAudioInBuffer, "AppendUacInBufferAuto"}, + {11, &IAudioIn::GetAudioInBufferCount, "GetAudioInBufferCount"}, + {12, &IAudioIn::SetDeviceGain, "SetDeviceGain"}, + {13, &IAudioIn::GetDeviceGain, "GetDeviceGain"}, + {14, &IAudioIn::FlushAudioInBuffers, "FlushAudioInBuffers"}, + }; + // clang-format on - RegisterHandlers(functions); + RegisterHandlers(functions); - buffer_event = service_context.CreateEvent("IAudioIn:BufferEvent"); -} + if (impl->GetSystem() + .Initialize(device_name, in_params, handle, applet_resource_user_id) + .IsError()) { + LOG_ERROR(Service_Audio, "Failed to initialize the AudioIn System!"); + } + } -IAudioIn::~IAudioIn() { - service_context.CloseEvent(buffer_event); -} + ~IAudioIn() override { + impl->Free(); + service_context.CloseEvent(event); + } -void IAudioIn::Start(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); + [[nodiscard]] std::shared_ptr GetImpl() { + return impl; + } - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); -} +private: + void GetAudioInState(Kernel::HLERequestContext& ctx) { + const auto state = static_cast(impl->GetState()); -void IAudioIn::RegisterBufferEvent(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); + LOG_DEBUG(Service_Audio, "called. State={}", state); - IPC::ResponseBuilder rb{ctx, 2, 1}; - rb.Push(ResultSuccess); - rb.PushCopyObjects(buffer_event->GetReadableEvent()); -} + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); + rb.Push(state); + } -void IAudioIn::AppendAudioInBufferAuto(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); + void Start(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_Audio, "called"); - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); -} + auto result = impl->StartSystem(); -AudInU::AudInU(Core::System& system_) : ServiceFramework{system_, "audin:u"} { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + } + + void Stop(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_Audio, "called"); + + auto result = impl->StopSystem(); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + } + + void AppendAudioInBuffer(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + u64 tag = rp.PopRaw(); + + const auto in_buffer_size{ctx.GetReadBufferSize()}; + if (in_buffer_size < sizeof(AudioInBuffer)) { + LOG_ERROR(Service_Audio, "Input buffer is too small for an AudioInBuffer!"); + } + + const auto& in_buffer = ctx.ReadBuffer(); + AudioInBuffer buffer{}; + std::memcpy(&buffer, in_buffer.data(), sizeof(AudioInBuffer)); + + [[maybe_unused]] auto sessionid{impl->GetSystem().GetSessionId()}; + LOG_TRACE(Service_Audio, "called. Session {} Appending buffer {:08X}", sessionid, tag); + + auto result = impl->AppendBuffer(buffer, tag); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + } + + void RegisterBufferEvent(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_Audio, "called"); + + auto& buffer_event = impl->GetBufferEvent(); + + IPC::ResponseBuilder rb{ctx, 2, 1}; + rb.Push(ResultSuccess); + rb.PushCopyObjects(buffer_event); + } + + void GetReleasedAudioInBuffer(Kernel::HLERequestContext& ctx) { + auto write_buffer_size = ctx.GetWriteBufferSize() / sizeof(u64); + std::vector released_buffers(write_buffer_size, 0); + + auto count = impl->GetReleasedBuffers(released_buffers); + + [[maybe_unused]] std::string tags{}; + for (u32 i = 0; i < count; i++) { + tags += fmt::format("{:08X}, ", released_buffers[i]); + } + [[maybe_unused]] auto sessionid{impl->GetSystem().GetSessionId()}; + LOG_TRACE(Service_Audio, "called. Session {} released {} buffers: {}", sessionid, count, + tags); + + ctx.WriteBuffer(released_buffers); + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); + rb.Push(count); + } + + void ContainsAudioInBuffer(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + + const u64 tag{rp.Pop()}; + const auto buffer_queued{impl->ContainsAudioBuffer(tag)}; + + LOG_DEBUG(Service_Audio, "called. Is buffer {:08X} registered? {}", tag, buffer_queued); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); + rb.Push(buffer_queued); + } + + void GetAudioInBufferCount(Kernel::HLERequestContext& ctx) { + const auto buffer_count = impl->GetBufferCount(); + + LOG_DEBUG(Service_Audio, "called. Buffer count={}", buffer_count); + + IPC::ResponseBuilder rb{ctx, 3}; + + rb.Push(ResultSuccess); + rb.Push(buffer_count); + } + + void SetDeviceGain(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + + const auto volume{rp.Pop()}; + LOG_DEBUG(Service_Audio, "called. Gain {}", volume); + + impl->SetVolume(volume); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); + } + + void GetDeviceGain(Kernel::HLERequestContext& ctx) { + auto volume{impl->GetVolume()}; + + LOG_DEBUG(Service_Audio, "called. Gain {}", volume); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); + rb.Push(volume); + } + + void FlushAudioInBuffers(Kernel::HLERequestContext& ctx) { + bool flushed{impl->FlushAudioInBuffers()}; + + LOG_DEBUG(Service_Audio, "called. Were any buffers flushed? {}", flushed); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); + rb.Push(flushed); + } + + KernelHelpers::ServiceContext service_context; + Kernel::KEvent* event; + std::shared_ptr impl; +}; + +AudInU::AudInU(Core::System& system_) + : ServiceFramework{system_, "audin:u", ServiceThreadType::CreateNew}, + service_context{system_, "AudInU"}, impl{std::make_unique( + system_)} { // clang-format off static const FunctionInfo functions[] = { {0, &AudInU::ListAudioIns, "ListAudioIns"}, @@ -80,59 +223,152 @@ AudInU::AudInU(Core::System& system_) : ServiceFramework{system_, "audin:u"} { AudInU::~AudInU() = default; void AudInU::ListAudioIns(Kernel::HLERequestContext& ctx) { + using namespace AudioCore::AudioRenderer; + LOG_DEBUG(Service_Audio, "called"); - const std::size_t count = ctx.GetWriteBufferSize() / sizeof(AudioInDeviceName); - const std::size_t device_count = std::min(count, audio_device_names.size()); - std::vector device_names; - device_names.reserve(device_count); + const auto write_count = + static_cast(ctx.GetWriteBufferSize() / sizeof(AudioDevice::AudioDeviceName)); + std::vector device_names{}; - for (std::size_t i = 0; i < device_count; i++) { - const auto& device_name = audio_device_names[i]; - auto& entry = device_names.emplace_back(); - device_name.copy(entry.data(), device_name.size()); + u32 out_count{0}; + if (write_count > 0) { + out_count = impl->GetDeviceNames(device_names, write_count, false); + ctx.WriteBuffer(device_names); } - ctx.WriteBuffer(device_names); - IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(static_cast(device_names.size())); + rb.Push(out_count); } void AudInU::ListAudioInsAutoFiltered(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); - constexpr u32 device_count = 0; + using namespace AudioCore::AudioRenderer; - // Since we don't actually use any other audio input devices, we return 0 devices. Filtered - // device listing just omits the default input device + LOG_DEBUG(Service_Audio, "called"); + + const auto write_count = + static_cast(ctx.GetWriteBufferSize() / sizeof(AudioDevice::AudioDeviceName)); + std::vector device_names{}; + + u32 out_count{0}; + if (write_count > 0) { + out_count = impl->GetDeviceNames(device_names, write_count, true); + ctx.WriteBuffer(device_names); + } IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(static_cast(device_count)); -} - -void AudInU::OpenInOutImpl(Kernel::HLERequestContext& ctx) { - AudInOutParams params{}; - params.channel_count = 2; - params.sample_format = SampleFormat::PCM16; - params.sample_rate = 48000; - params.state = State::Started; - - IPC::ResponseBuilder rb{ctx, 6, 0, 1}; - rb.Push(ResultSuccess); - rb.PushRaw(params); - rb.PushIpcInterface(system); + rb.Push(out_count); } void AudInU::OpenAudioIn(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); - OpenInOutImpl(ctx); + IPC::RequestParser rp{ctx}; + auto in_params{rp.PopRaw()}; + auto applet_resource_user_id{rp.PopRaw()}; + const auto device_name_data{ctx.ReadBuffer()}; + auto device_name = Common::StringFromBuffer(device_name_data); + auto handle{ctx.GetCopyHandle(0)}; + + std::scoped_lock l{impl->mutex}; + auto link{impl->LinkToManager()}; + if (link.IsError()) { + LOG_ERROR(Service_Audio, "Failed to link Audio In to Audio Manager"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(link); + return; + } + + size_t new_session_id{}; + auto result{impl->AcquireSessionId(new_session_id)}; + if (result.IsError()) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + return; + } + + LOG_DEBUG(Service_Audio, "Opening new AudioIn, sessionid={}, free sessions={}", new_session_id, + impl->num_free_sessions); + + auto audio_in = std::make_shared(system, *impl, new_session_id, device_name, + in_params, handle, applet_resource_user_id); + impl->sessions[new_session_id] = audio_in->GetImpl(); + impl->applet_resource_user_ids[new_session_id] = applet_resource_user_id; + + auto& out_system = impl->sessions[new_session_id]->GetSystem(); + AudioInParameterInternal out_params{.sample_rate = out_system.GetSampleRate(), + .channel_count = out_system.GetChannelCount(), + .sample_format = + static_cast(out_system.GetSampleFormat()), + .state = static_cast(out_system.GetState())}; + + IPC::ResponseBuilder rb{ctx, 6, 0, 1}; + + std::string out_name{out_system.GetName()}; + ctx.WriteBuffer(out_name); + + rb.Push(ResultSuccess); + rb.PushRaw(out_params); + rb.PushIpcInterface(audio_in); } void AudInU::OpenAudioInProtocolSpecified(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); - OpenInOutImpl(ctx); + IPC::RequestParser rp{ctx}; + auto protocol_specified{rp.PopRaw()}; + auto in_params{rp.PopRaw()}; + auto applet_resource_user_id{rp.PopRaw()}; + const auto device_name_data{ctx.ReadBuffer()}; + auto device_name = Common::StringFromBuffer(device_name_data); + auto handle{ctx.GetCopyHandle(0)}; + + std::scoped_lock l{impl->mutex}; + auto link{impl->LinkToManager()}; + if (link.IsError()) { + LOG_ERROR(Service_Audio, "Failed to link Audio In to Audio Manager"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(link); + return; + } + + size_t new_session_id{}; + auto result{impl->AcquireSessionId(new_session_id)}; + if (result.IsError()) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + return; + } + + LOG_DEBUG(Service_Audio, "Opening new AudioIn, sessionid={}, free sessions={}", new_session_id, + impl->num_free_sessions); + + auto audio_in = std::make_shared(system, *impl, new_session_id, device_name, + in_params, handle, applet_resource_user_id); + impl->sessions[new_session_id] = audio_in->GetImpl(); + impl->applet_resource_user_ids[new_session_id] = applet_resource_user_id; + + auto& out_system = impl->sessions[new_session_id]->GetSystem(); + AudioInParameterInternal out_params{.sample_rate = out_system.GetSampleRate(), + .channel_count = out_system.GetChannelCount(), + .sample_format = + static_cast(out_system.GetSampleFormat()), + .state = static_cast(out_system.GetState())}; + + IPC::ResponseBuilder rb{ctx, 6, 0, 1}; + + std::string out_name{out_system.GetName()}; + if (protocol_specified == 0) { + if (out_system.IsUac()) { + out_name = "UacIn"; + } else { + out_name = "DeviceIn"; + } + } + + ctx.WriteBuffer(out_name); + + rb.Push(ResultSuccess); + rb.PushRaw(out_params); + rb.PushIpcInterface(audio_in); } } // namespace Service::Audio diff --git a/src/core/hle/service/audio/audin_u.h b/src/core/hle/service/audio/audin_u.h index 2bfa38ecc..b45fda78a 100755 --- a/src/core/hle/service/audio/audin_u.h +++ b/src/core/hle/service/audio/audin_u.h @@ -3,6 +3,8 @@ #pragma once +#include "audio_core/audio_in_manager.h" +#include "audio_core/in/audio_in.h" #include "core/hle/service/kernel_helpers.h" #include "core/hle/service/service.h" @@ -14,56 +16,27 @@ namespace Kernel { class HLERequestContext; } +namespace AudioCore::AudioOut { +class Manager; +class In; +} // namespace AudioCore::AudioOut + namespace Service::Audio { -class IAudioIn final : public ServiceFramework { -public: - explicit IAudioIn(Core::System& system_); - ~IAudioIn() override; - -private: - void Start(Kernel::HLERequestContext& ctx); - void RegisterBufferEvent(Kernel::HLERequestContext& ctx); - void AppendAudioInBufferAuto(Kernel::HLERequestContext& ctx); - - KernelHelpers::ServiceContext service_context; - - Kernel::KEvent* buffer_event; -}; - class AudInU final : public ServiceFramework { public: explicit AudInU(Core::System& system_); ~AudInU() override; private: - enum class SampleFormat : u32_le { - PCM16 = 2, - }; - - enum class State : u32_le { - Started = 0, - Stopped = 1, - }; - - struct AudInOutParams { - u32_le sample_rate{}; - u32_le channel_count{}; - SampleFormat sample_format{}; - State state{}; - }; - static_assert(sizeof(AudInOutParams) == 0x10, "AudInOutParams is an invalid size"); - - using AudioInDeviceName = std::array; - static constexpr std::array audio_device_names{{ - "BuiltInHeadset", - }}; - void ListAudioIns(Kernel::HLERequestContext& ctx); void ListAudioInsAutoFiltered(Kernel::HLERequestContext& ctx); void OpenInOutImpl(Kernel::HLERequestContext& ctx); void OpenAudioIn(Kernel::HLERequestContext& ctx); void OpenAudioInProtocolSpecified(Kernel::HLERequestContext& ctx); + + KernelHelpers::ServiceContext service_context; + std::unique_ptr impl; }; } // namespace Service::Audio diff --git a/src/core/hle/service/audio/audout_u.cpp b/src/core/hle/service/audio/audout_u.cpp index b0dad6053..a44dd842a 100755 --- a/src/core/hle/service/audio/audout_u.cpp +++ b/src/core/hle/service/audio/audout_u.cpp @@ -5,56 +5,43 @@ #include #include -#include "audio_core/audio_out.h" -#include "audio_core/codec.h" +#include "audio_core/out/audio_out_system.h" +#include "audio_core/renderer/audio_device.h" #include "common/common_funcs.h" #include "common/logging/log.h" +#include "common/string_util.h" #include "common/swap.h" #include "core/core.h" #include "core/hle/ipc_helpers.h" #include "core/hle/kernel/k_event.h" #include "core/hle/service/audio/audout_u.h" #include "core/hle/service/audio/errors.h" -#include "core/hle/service/kernel_helpers.h" #include "core/memory.h" namespace Service::Audio { - -constexpr std::array DefaultDevice{{"DeviceOut"}}; -constexpr int DefaultSampleRate{48000}; - -struct AudoutParams { - s32_le sample_rate; - u16_le channel_count; - INSERT_PADDING_BYTES_NOINIT(2); -}; -static_assert(sizeof(AudoutParams) == 0x8, "AudoutParams is an invalid size"); - -enum class AudioState : u32 { - Started, - Stopped, -}; +using namespace AudioCore::AudioOut; class IAudioOut final : public ServiceFramework { public: - explicit IAudioOut(Core::System& system_, AudoutParams audio_params_, - AudioCore::AudioOut& audio_core_, std::string&& device_name_, - std::string&& unique_name) + explicit IAudioOut(Core::System& system_, AudioCore::AudioOut::Manager& manager, + size_t session_id, std::string& device_name, + const AudioOutParameter& in_params, u32 handle, u64 applet_resource_user_id) : ServiceFramework{system_, "IAudioOut", ServiceThreadType::CreateNew}, - audio_core{audio_core_}, device_name{std::move(device_name_)}, - audio_params{audio_params_}, main_memory{system.Memory()}, service_context{system_, - "IAudioOut"} { + service_context{system_, "IAudioOut"}, event{service_context.CreateEvent( + "AudioOutEvent")}, + impl{std::make_shared(system_, manager, event, session_id)} { + // clang-format off static const FunctionInfo functions[] = { {0, &IAudioOut::GetAudioOutState, "GetAudioOutState"}, - {1, &IAudioOut::StartAudioOut, "Start"}, - {2, &IAudioOut::StopAudioOut, "Stop"}, - {3, &IAudioOut::AppendAudioOutBufferImpl, "AppendAudioOutBuffer"}, + {1, &IAudioOut::Start, "Start"}, + {2, &IAudioOut::Stop, "Stop"}, + {3, &IAudioOut::AppendAudioOutBuffer, "AppendAudioOutBuffer"}, {4, &IAudioOut::RegisterBufferEvent, "RegisterBufferEvent"}, - {5, &IAudioOut::GetReleasedAudioOutBufferImpl, "GetReleasedAudioOutBuffers"}, + {5, &IAudioOut::GetReleasedAudioOutBuffers, "GetReleasedAudioOutBuffers"}, {6, &IAudioOut::ContainsAudioOutBuffer, "ContainsAudioOutBuffer"}, - {7, &IAudioOut::AppendAudioOutBufferImpl, "AppendAudioOutBufferAuto"}, - {8, &IAudioOut::GetReleasedAudioOutBufferImpl, "GetReleasedAudioOutBufferAuto"}, + {7, &IAudioOut::AppendAudioOutBuffer, "AppendAudioOutBufferAuto"}, + {8, &IAudioOut::GetReleasedAudioOutBuffers, "GetReleasedAudioOutBuffersAuto"}, {9, &IAudioOut::GetAudioOutBufferCount, "GetAudioOutBufferCount"}, {10, &IAudioOut::GetAudioOutPlayedSampleCount, "GetAudioOutPlayedSampleCount"}, {11, &IAudioOut::FlushAudioOutBuffers, "FlushAudioOutBuffers"}, @@ -64,241 +51,263 @@ public: // clang-format on RegisterHandlers(functions); - // This is the event handle used to check if the audio buffer was released - buffer_event = service_context.CreateEvent("IAudioOutBufferReleased"); - - stream = audio_core.OpenStream(system.CoreTiming(), audio_params.sample_rate, - audio_params.channel_count, std::move(unique_name), [this] { - const auto guard = LockService(); - buffer_event->GetWritableEvent().Signal(); - }); + if (impl->GetSystem() + .Initialize(device_name, in_params, handle, applet_resource_user_id) + .IsError()) { + LOG_ERROR(Service_Audio, "Failed to initialize the AudioOut System!"); + } } ~IAudioOut() override { - service_context.CloseEvent(buffer_event); + impl->Free(); + service_context.CloseEvent(event); + } + + [[nodiscard]] std::shared_ptr GetImpl() { + return impl; } private: - struct AudioBuffer { - u64_le next; - u64_le buffer; - u64_le buffer_capacity; - u64_le buffer_size; - u64_le offset; - }; - static_assert(sizeof(AudioBuffer) == 0x28, "AudioBuffer is an invalid size"); - void GetAudioOutState(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); + const auto state = static_cast(impl->GetState()); + + LOG_DEBUG(Service_Audio, "called. State={}", state); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(static_cast(stream->IsPlaying() ? AudioState::Started : AudioState::Stopped)); + rb.Push(state); } - void StartAudioOut(Kernel::HLERequestContext& ctx) { + void Start(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_Audio, "called"); - if (stream->IsPlaying()) { - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ERR_OPERATION_FAILED); - return; - } - - audio_core.StartStream(stream); + auto result = impl->StartSystem(); IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); + rb.Push(result); } - void StopAudioOut(Kernel::HLERequestContext& ctx) { + void Stop(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_Audio, "called"); - if (stream->IsPlaying()) { - audio_core.StopStream(stream); - } + auto result = impl->StopSystem(); IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); + rb.Push(result); + } + + void AppendAudioOutBuffer(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + u64 tag = rp.PopRaw(); + + const auto in_buffer_size{ctx.GetReadBufferSize()}; + if (in_buffer_size < sizeof(AudioOutBuffer)) { + LOG_ERROR(Service_Audio, "Input buffer is too small for an AudioOutBuffer!"); + } + + const auto& in_buffer = ctx.ReadBuffer(); + AudioOutBuffer buffer{}; + std::memcpy(&buffer, in_buffer.data(), sizeof(AudioOutBuffer)); + + [[maybe_unused]] auto sessionid{impl->GetSystem().GetSessionId()}; + LOG_TRACE(Service_Audio, "called. Session {} Appending buffer {:08X}", sessionid, tag); + + auto result = impl->AppendBuffer(buffer, tag); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); } void RegisterBufferEvent(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_Audio, "called"); + auto& buffer_event = impl->GetBufferEvent(); + IPC::ResponseBuilder rb{ctx, 2, 1}; rb.Push(ResultSuccess); - rb.PushCopyObjects(buffer_event->GetReadableEvent()); + rb.PushCopyObjects(buffer_event); } - void AppendAudioOutBufferImpl(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "(STUBBED) called {}", ctx.Description()); - IPC::RequestParser rp{ctx}; + void GetReleasedAudioOutBuffers(Kernel::HLERequestContext& ctx) { + auto write_buffer_size = ctx.GetWriteBufferSize() / sizeof(u64); + std::vector released_buffers(write_buffer_size, 0); - const auto& input_buffer{ctx.ReadBuffer()}; - ASSERT_MSG(input_buffer.size() == sizeof(AudioBuffer), - "AudioBuffer input is an invalid size!"); - AudioBuffer audio_buffer{}; - std::memcpy(&audio_buffer, input_buffer.data(), sizeof(AudioBuffer)); - const u64 tag{rp.Pop()}; + auto count = impl->GetReleasedBuffers(released_buffers); - std::vector samples(audio_buffer.buffer_size / sizeof(s16)); - main_memory.ReadBlock(audio_buffer.buffer, samples.data(), audio_buffer.buffer_size); - - if (!audio_core.QueueBuffer(stream, tag, std::move(samples))) { - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ERR_BUFFER_COUNT_EXCEEDED); - return; + [[maybe_unused]] std::string tags{}; + for (u32 i = 0; i < count; i++) { + tags += fmt::format("{:08X}, ", released_buffers[i]); } + [[maybe_unused]] auto sessionid{impl->GetSystem().GetSessionId()}; + LOG_TRACE(Service_Audio, "called. Session {} released {} buffers: {}", sessionid, count, + tags); - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); - } - - void GetReleasedAudioOutBufferImpl(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called {}", ctx.Description()); - - const u64 max_count{ctx.GetWriteBufferSize() / sizeof(u64)}; - const auto released_buffers{audio_core.GetTagsAndReleaseBuffers(stream, max_count)}; - - std::vector tags{released_buffers}; - tags.resize(max_count); - ctx.WriteBuffer(tags); - + ctx.WriteBuffer(released_buffers); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(static_cast(released_buffers.size())); + rb.Push(count); } void ContainsAudioOutBuffer(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); - IPC::RequestParser rp{ctx}; + const u64 tag{rp.Pop()}; + const auto buffer_queued{impl->ContainsAudioBuffer(tag)}; + + LOG_DEBUG(Service_Audio, "called. Is buffer {:08X} registered? {}", tag, buffer_queued); + IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(stream->ContainsBuffer(tag)); + rb.Push(buffer_queued); } void GetAudioOutBufferCount(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); + const auto buffer_count = impl->GetBufferCount(); + + LOG_DEBUG(Service_Audio, "called. Buffer count={}", buffer_count); IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); - rb.Push(static_cast(stream->GetQueueSize())); + rb.Push(buffer_count); } void GetAudioOutPlayedSampleCount(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); + const auto samples_played = impl->GetPlayedSampleCount(); + + LOG_DEBUG(Service_Audio, "called. Played samples={}", samples_played); IPC::ResponseBuilder rb{ctx, 4}; + rb.Push(ResultSuccess); - rb.Push(stream->GetPlayedSampleCount()); + rb.Push(samples_played); } void FlushAudioOutBuffers(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); + bool flushed{impl->FlushAudioOutBuffers()}; + + LOG_DEBUG(Service_Audio, "called. Were any buffers flushed? {}", flushed); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(stream->Flush()); + rb.Push(flushed); } void SetAudioOutVolume(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; - const float volume = rp.Pop(); - LOG_DEBUG(Service_Audio, "called, volume={}", volume); + const auto volume = rp.Pop(); - stream->SetVolume(volume); + LOG_DEBUG(Service_Audio, "called. Volume={}", volume); + + impl->SetVolume(volume); IPC::ResponseBuilder rb{ctx, 2}; rb.Push(ResultSuccess); } void GetAudioOutVolume(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); + const auto volume = impl->GetVolume(); + + LOG_DEBUG(Service_Audio, "called. Volume={}", volume); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(stream->GetVolume()); + rb.Push(volume); } - AudioCore::AudioOut& audio_core; - AudioCore::StreamPtr stream; - std::string device_name; - - [[maybe_unused]] AudoutParams audio_params{}; - - Core::Memory::Memory& main_memory; - KernelHelpers::ServiceContext service_context; - - /// This is the event handle used to check if the audio buffer was released - Kernel::KEvent* buffer_event; + Kernel::KEvent* event; + std::shared_ptr impl; }; -AudOutU::AudOutU(Core::System& system_) : ServiceFramework{system_, "audout:u"} { +AudOutU::AudOutU(Core::System& system_) + : ServiceFramework{system_, "audout:u", ServiceThreadType::CreateNew}, + service_context{system_, "AudOutU"}, impl{std::make_unique( + system_)} { // clang-format off static const FunctionInfo functions[] = { - {0, &AudOutU::ListAudioOutsImpl, "ListAudioOuts"}, - {1, &AudOutU::OpenAudioOutImpl, "OpenAudioOut"}, - {2, &AudOutU::ListAudioOutsImpl, "ListAudioOutsAuto"}, - {3, &AudOutU::OpenAudioOutImpl, "OpenAudioOutAuto"}, + {0, &AudOutU::ListAudioOuts, "ListAudioOuts"}, + {1, &AudOutU::OpenAudioOut, "OpenAudioOut"}, + {2, &AudOutU::ListAudioOuts, "ListAudioOutsAuto"}, + {3, &AudOutU::OpenAudioOut, "OpenAudioOutAuto"}, }; // clang-format on RegisterHandlers(functions); - audio_core = std::make_unique(); } AudOutU::~AudOutU() = default; -void AudOutU::ListAudioOutsImpl(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); +void AudOutU::ListAudioOuts(Kernel::HLERequestContext& ctx) { + using namespace AudioCore::AudioRenderer; - ctx.WriteBuffer(DefaultDevice); + std::scoped_lock l{impl->mutex}; + + const auto write_count = + static_cast(ctx.GetWriteBufferSize() / sizeof(AudioDevice::AudioDeviceName)); + std::vector device_names{}; + std::string print_names{}; + if (write_count > 0) { + device_names.push_back(AudioDevice::AudioDeviceName("DeviceOut")); + LOG_DEBUG(Service_Audio, "called. \nName=DeviceOut"); + } else { + LOG_DEBUG(Service_Audio, "called. Empty buffer passed in."); + } + + ctx.WriteBuffer(device_names); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(1); // Amount of audio devices + rb.Push(static_cast(device_names.size())); } -void AudOutU::OpenAudioOutImpl(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); - - const auto device_name_data{ctx.ReadBuffer()}; - std::string device_name; - if (device_name_data[0] != '\0') { - device_name.assign(device_name_data.begin(), device_name_data.end()); - } else { - device_name.assign(DefaultDevice.begin(), DefaultDevice.end()); - } - ctx.WriteBuffer(device_name); - +void AudOutU::OpenAudioOut(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; - auto params{rp.PopRaw()}; - if (params.channel_count <= 2) { - // Mono does not exist for audout - params.channel_count = 2; - } else { - params.channel_count = 6; - } - if (!params.sample_rate) { - params.sample_rate = DefaultSampleRate; + auto in_params{rp.PopRaw()}; + auto applet_resource_user_id{rp.PopRaw()}; + const auto device_name_data{ctx.ReadBuffer()}; + auto device_name = Common::StringFromBuffer(device_name_data); + auto handle{ctx.GetCopyHandle(0)}; + + auto link{impl->LinkToManager()}; + if (link.IsError()) { + LOG_ERROR(Service_Audio, "Failed to link Audio Out to Audio Manager"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(link); + return; } - std::string unique_name{fmt::format("{}-{}", device_name, audio_out_interfaces.size())}; - auto audio_out_interface = std::make_shared( - system, params, *audio_core, std::move(device_name), std::move(unique_name)); + size_t new_session_id{}; + auto result{impl->AcquireSessionId(new_session_id)}; + if (result.IsError()) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + return; + } + + LOG_DEBUG(Service_Audio, "Opening new AudioOut, sessionid={}, free sessions={}", new_session_id, + impl->num_free_sessions); + + auto audio_out = std::make_shared(system, *impl, new_session_id, device_name, + in_params, handle, applet_resource_user_id); + + impl->sessions[new_session_id] = audio_out->GetImpl(); + impl->applet_resource_user_ids[new_session_id] = applet_resource_user_id; + + auto& out_system = impl->sessions[new_session_id]->GetSystem(); + AudioOutParameterInternal out_params{.sample_rate = out_system.GetSampleRate(), + .channel_count = out_system.GetChannelCount(), + .sample_format = + static_cast(out_system.GetSampleFormat()), + .state = static_cast(out_system.GetState())}; IPC::ResponseBuilder rb{ctx, 6, 0, 1}; - rb.Push(ResultSuccess); - rb.Push(DefaultSampleRate); - rb.Push(params.channel_count); - rb.Push(static_cast(AudioCore::Codec::PcmFormat::Int16)); - rb.Push(static_cast(AudioState::Stopped)); - rb.PushIpcInterface(audio_out_interface); - audio_out_interfaces.push_back(std::move(audio_out_interface)); + ctx.WriteBuffer(out_system.GetName()); + + rb.Push(ResultSuccess); + rb.PushRaw(out_params); + rb.PushIpcInterface(audio_out); } } // namespace Service::Audio diff --git a/src/core/hle/service/audio/audout_u.h b/src/core/hle/service/audio/audout_u.h index d82004c2e..fdc0ee754 100755 --- a/src/core/hle/service/audio/audout_u.h +++ b/src/core/hle/service/audio/audout_u.h @@ -3,13 +3,11 @@ #pragma once -#include +#include "audio_core/audio_out_manager.h" +#include "audio_core/out/audio_out.h" +#include "core/hle/service/kernel_helpers.h" #include "core/hle/service/service.h" -namespace AudioCore { -class AudioOut; -} - namespace Core { class System; } @@ -18,6 +16,11 @@ namespace Kernel { class HLERequestContext; } +namespace AudioCore::AudioOut { +class Manager; +class Out; +} // namespace AudioCore::AudioOut + namespace Service::Audio { class IAudioOut; @@ -28,11 +31,11 @@ public: ~AudOutU() override; private: - void ListAudioOutsImpl(Kernel::HLERequestContext& ctx); - void OpenAudioOutImpl(Kernel::HLERequestContext& ctx); + void ListAudioOuts(Kernel::HLERequestContext& ctx); + void OpenAudioOut(Kernel::HLERequestContext& ctx); - std::vector> audio_out_interfaces; - std::unique_ptr audio_core; + KernelHelpers::ServiceContext service_context; + std::unique_ptr impl; }; } // namespace Service::Audio diff --git a/src/core/hle/service/audio/audren_u.cpp b/src/core/hle/service/audio/audren_u.cpp index 2ce63c004..aca8c0590 100755 --- a/src/core/hle/service/audio/audren_u.cpp +++ b/src/core/hle/service/audio/audren_u.cpp @@ -4,7 +4,12 @@ #include #include -#include "audio_core/audio_renderer.h" +#include "audio_core/audio_core.h" +#include "audio_core/common/audio_renderer_parameter.h" +#include "audio_core/common/feature_support.h" +#include "audio_core/renderer/audio_device.h" +#include "audio_core/renderer/audio_renderer.h" +#include "audio_core/renderer/voice/voice_info.h" #include "common/alignment.h" #include "common/bit_util.h" #include "common/common_funcs.h" @@ -13,91 +18,112 @@ #include "core/core.h" #include "core/hle/ipc_helpers.h" #include "core/hle/kernel/k_event.h" +#include "core/hle/kernel/k_process.h" +#include "core/hle/kernel/k_transfer_memory.h" #include "core/hle/service/audio/audren_u.h" #include "core/hle/service/audio/errors.h" +#include "core/memory.h" + +using namespace AudioCore::AudioRenderer; namespace Service::Audio { class IAudioRenderer final : public ServiceFramework { public: - explicit IAudioRenderer(Core::System& system_, - const AudioCommon::AudioRendererParameter& audren_params, - const std::size_t instance_number) + explicit IAudioRenderer(Core::System& system_, Manager& manager_, + AudioCore::AudioRendererParameterInternal& params, + Kernel::KTransferMemory* transfer_memory, u64 transfer_memory_size, + u32 process_handle, u64 applet_resource_user_id, s32 session_id) : ServiceFramework{system_, "IAudioRenderer", ServiceThreadType::CreateNew}, - service_context{system_, "IAudioRenderer"} { + service_context{system_, "IAudioRenderer"}, rendered_event{service_context.CreateEvent( + "IAudioRendererEvent")}, + manager{manager_}, impl{std::make_unique(system_, manager, rendered_event)} { // clang-format off static const FunctionInfo functions[] = { {0, &IAudioRenderer::GetSampleRate, "GetSampleRate"}, {1, &IAudioRenderer::GetSampleCount, "GetSampleCount"}, {2, &IAudioRenderer::GetMixBufferCount, "GetMixBufferCount"}, {3, &IAudioRenderer::GetState, "GetState"}, - {4, &IAudioRenderer::RequestUpdateImpl, "RequestUpdate"}, + {4, &IAudioRenderer::RequestUpdate, "RequestUpdate"}, {5, &IAudioRenderer::Start, "Start"}, {6, &IAudioRenderer::Stop, "Stop"}, {7, &IAudioRenderer::QuerySystemEvent, "QuerySystemEvent"}, {8, &IAudioRenderer::SetRenderingTimeLimit, "SetRenderingTimeLimit"}, {9, &IAudioRenderer::GetRenderingTimeLimit, "GetRenderingTimeLimit"}, - {10, &IAudioRenderer::RequestUpdateImpl, "RequestUpdateAuto"}, - {11, &IAudioRenderer::ExecuteAudioRendererRendering, "ExecuteAudioRendererRendering"}, + {10, nullptr, "RequestUpdateAuto"}, + {11, nullptr, "ExecuteAudioRendererRendering"}, }; // clang-format on RegisterHandlers(functions); - system_event = service_context.CreateEvent("IAudioRenderer:SystemEvent"); - renderer = std::make_unique( - system.CoreTiming(), system.Memory(), audren_params, - [this]() { - const auto guard = LockService(); - system_event->GetWritableEvent().Signal(); - }, - instance_number); + impl->Initialize(params, transfer_memory, transfer_memory_size, process_handle, + applet_resource_user_id, session_id); } ~IAudioRenderer() override { - service_context.CloseEvent(system_event); + impl->Finalize(); + service_context.CloseEvent(rendered_event); } private: void GetSampleRate(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); + const auto sample_rate{impl->GetSystem().GetSampleRate()}; + + LOG_DEBUG(Service_Audio, "called. Sample rate {}", sample_rate); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(renderer->GetSampleRate()); + rb.Push(sample_rate); } void GetSampleCount(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); + const auto sample_count{impl->GetSystem().GetSampleCount()}; + + LOG_DEBUG(Service_Audio, "called. Sample count {}", sample_count); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(renderer->GetSampleCount()); + rb.Push(sample_count); } void GetState(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); + const u32 state{!impl->GetSystem().IsActive()}; + + LOG_DEBUG(Service_Audio, "called, state {}", state); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(static_cast(renderer->GetStreamState())); + rb.Push(state); } void GetMixBufferCount(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_Audio, "called"); + const auto buffer_count{impl->GetSystem().GetMixBufferCount()}; + IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(renderer->GetMixBufferCount()); + rb.Push(buffer_count); } - void RequestUpdateImpl(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "(STUBBED) called"); + void RequestUpdate(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_Audio, "called"); - std::vector output_params(ctx.GetWriteBufferSize(), 0); - auto result = renderer->UpdateAudioRenderer(ctx.ReadBuffer(), output_params); + std::vector input{ctx.ReadBuffer(0)}; + + // These buffers are written manually to avoid an issue with WriteBuffer throwing errors for + // checking size 0. Performance size is 0 for most games. + const auto buffers{ctx.BufferDescriptorB()}; + std::vector output(buffers[0].Size(), 0); + std::vector performance(buffers[1].Size(), 0); + + auto result = impl->RequestUpdate(input, performance, output); if (result.IsSuccess()) { - ctx.WriteBuffer(output_params); + ctx.WriteBufferB(output.data(), output.size(), 0); + ctx.WriteBufferB(performance.data(), performance.size(), 1); + } else { + LOG_ERROR(Service_Audio, "RequestUpdate failed error 0x{:02X}!", result.description); } IPC::ResponseBuilder rb{ctx, 2}; @@ -105,38 +131,45 @@ private: } void Start(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); + LOG_DEBUG(Service_Audio, "called"); - const auto result = renderer->Start(); + impl->Start(); IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(result); + rb.Push(ResultSuccess); } void Stop(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); + LOG_DEBUG(Service_Audio, "called"); - const auto result = renderer->Stop(); + impl->Stop(); IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(result); + rb.Push(ResultSuccess); } void QuerySystemEvent(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); + LOG_DEBUG(Service_Audio, "called"); + + if (impl->GetSystem().GetExecutionMode() == AudioCore::ExecutionMode::Manual) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERR_NOT_SUPPORTED); + return; + } IPC::ResponseBuilder rb{ctx, 2, 1}; rb.Push(ResultSuccess); - rb.PushCopyObjects(system_event->GetReadableEvent()); + rb.PushCopyObjects(rendered_event->GetReadableEvent()); } void SetRenderingTimeLimit(Kernel::HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - rendering_time_limit_percent = rp.Pop(); - LOG_DEBUG(Service_Audio, "called. rendering_time_limit_percent={}", - rendering_time_limit_percent); + LOG_DEBUG(Service_Audio, "called"); - ASSERT(rendering_time_limit_percent <= 100); + IPC::RequestParser rp{ctx}; + auto limit = rp.PopRaw(); + + auto& system_ = impl->GetSystem(); + system_.SetRenderingTimeLimit(limit); IPC::ResponseBuilder rb{ctx, 2}; rb.Push(ResultSuccess); @@ -145,34 +178,34 @@ private: void GetRenderingTimeLimit(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_Audio, "called"); + auto& system_ = impl->GetSystem(); + auto time = system_.GetRenderingTimeLimit(); + IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(rendering_time_limit_percent); + rb.Push(time); } void ExecuteAudioRendererRendering(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_Audio, "called"); - - // This service command currently only reports an unsupported operation - // error code, or aborts. Given that, we just always return an error - // code in this case. - - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ERR_NOT_SUPPORTED); } KernelHelpers::ServiceContext service_context; - - Kernel::KEvent* system_event; - std::unique_ptr renderer; - u32 rendering_time_limit_percent = 100; + Kernel::KEvent* rendered_event; + Manager& manager; + std::unique_ptr impl; }; class IAudioDevice final : public ServiceFramework { + public: - explicit IAudioDevice(Core::System& system_, Kernel::KEvent* buffer_event_, u32_le revision_) - : ServiceFramework{system_, "IAudioDevice"}, buffer_event{buffer_event_}, revision{ - revision_} { + explicit IAudioDevice(Core::System& system_, u64 applet_resource_user_id, u32 revision, + u32 device_num) + : ServiceFramework{system_, "IAudioDevice", ServiceThreadType::CreateNew}, + service_context{system_, "IAudioDevice"}, impl{std::make_unique( + system_, applet_resource_user_id, + revision)}, + event{service_context.CreateEvent(fmt::format("IAudioDeviceEvent-{}", device_num))} { static const FunctionInfo functions[] = { {0, &IAudioDevice::ListAudioDeviceName, "ListAudioDeviceName"}, {1, &IAudioDevice::SetAudioDeviceOutputVolume, "SetAudioDeviceOutputVolume"}, @@ -186,54 +219,45 @@ public: {10, &IAudioDevice::GetActiveAudioDeviceName, "GetActiveAudioDeviceNameAuto"}, {11, &IAudioDevice::QueryAudioDeviceInputEvent, "QueryAudioDeviceInputEvent"}, {12, &IAudioDevice::QueryAudioDeviceOutputEvent, "QueryAudioDeviceOutputEvent"}, - {13, nullptr, "GetActiveAudioOutputDeviceName"}, - {14, nullptr, "ListAudioOutputDeviceName"}, + {13, &IAudioDevice::GetActiveAudioDeviceName, "GetActiveAudioOutputDeviceName"}, + {14, &IAudioDevice::ListAudioOutputDeviceName, "ListAudioOutputDeviceName"}, }; RegisterHandlers(functions); + + event->GetWritableEvent().Signal(); + } + + ~IAudioDevice() override { + service_context.CloseEvent(event); } private: - using AudioDeviceName = std::array; - static constexpr std::array audio_device_names{{ - "AudioStereoJackOutput", - "AudioBuiltInSpeakerOutput", - "AudioTvOutput", - "AudioUsbDeviceOutput", - }}; - enum class DeviceType { - AHUBHeadphones, - AHUBSpeakers, - HDA, - USBOutput, - }; - void ListAudioDeviceName(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); + const size_t in_count = ctx.GetWriteBufferSize() / sizeof(AudioDevice::AudioDeviceName); - const bool usb_output_supported = - IsFeatureSupported(AudioFeatures::AudioUSBDeviceOutput, revision); - const std::size_t count = ctx.GetWriteBufferSize() / sizeof(AudioDeviceName); + std::vector out_names{}; - std::vector name_buffer; - name_buffer.reserve(audio_device_names.size()); + u32 out_count = impl->ListAudioDeviceName(out_names, in_count); - for (std::size_t i = 0; i < count && i < audio_device_names.size(); i++) { - const auto type = static_cast(i); - - if (!usb_output_supported && type == DeviceType::USBOutput) { - continue; + std::string out{}; + for (u32 i = 0; i < out_count; i++) { + std::string a{}; + u32 j = 0; + while (out_names[i].name[j] != '\0') { + a += out_names[i].name[j]; + j++; } - - const auto& device_name = audio_device_names[i]; - auto& entry = name_buffer.emplace_back(); - device_name.copy(entry.data(), device_name.size()); + out += "\n\t" + a; } - ctx.WriteBuffer(name_buffer); + LOG_DEBUG(Service_Audio, "called.\nNames={}", out); IPC::ResponseBuilder rb{ctx, 3}; + + ctx.WriteBuffer(out_names); + rb.Push(ResultSuccess); - rb.Push(static_cast(name_buffer.size())); + rb.Push(out_count); } void SetAudioDeviceOutputVolume(Kernel::HLERequestContext& ctx) { @@ -243,7 +267,11 @@ private: const auto device_name_buffer = ctx.ReadBuffer(); const std::string name = Common::StringFromBuffer(device_name_buffer); - LOG_WARNING(Service_Audio, "(STUBBED) called. name={}, volume={}", name, volume); + LOG_DEBUG(Service_Audio, "called. name={}, volume={}", name, volume); + + if (name == "AudioTvOutput") { + impl->SetDeviceVolumes(volume); + } IPC::ResponseBuilder rb{ctx, 2}; rb.Push(ResultSuccess); @@ -253,53 +281,60 @@ private: const auto device_name_buffer = ctx.ReadBuffer(); const std::string name = Common::StringFromBuffer(device_name_buffer); - LOG_WARNING(Service_Audio, "(STUBBED) called. name={}", name); + LOG_DEBUG(Service_Audio, "called. Name={}", name); + + f32 volume{1.0f}; + if (name == "AudioTvOutput") { + volume = impl->GetDeviceVolume(name); + } IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(1.0f); + rb.Push(volume); } void GetActiveAudioDeviceName(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); + const auto write_size = ctx.GetWriteBufferSize() / sizeof(char); + std::string out_name{"AudioTvOutput"}; - // Currently set to always be TV audio output. - const auto& device_name = audio_device_names[2]; + LOG_DEBUG(Service_Audio, "(STUBBED) called. Name={}", out_name); - AudioDeviceName out_device_name{}; - device_name.copy(out_device_name.data(), device_name.size()); + out_name.resize(write_size); - ctx.WriteBuffer(out_device_name); + ctx.WriteBuffer(out_name); IPC::ResponseBuilder rb{ctx, 2}; rb.Push(ResultSuccess); } void QueryAudioDeviceSystemEvent(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); + LOG_DEBUG(Service_Audio, "(STUBBED) called"); - buffer_event->GetWritableEvent().Signal(); + event->GetWritableEvent().Signal(); IPC::ResponseBuilder rb{ctx, 2, 1}; rb.Push(ResultSuccess); - rb.PushCopyObjects(buffer_event->GetReadableEvent()); + rb.PushCopyObjects(event->GetReadableEvent()); } void GetActiveChannelCount(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); + const auto& sink{system.AudioCore().GetOutputSink()}; + u32 channel_count{sink.GetDeviceChannels()}; + + LOG_DEBUG(Service_Audio, "(STUBBED) called. Channels={}", channel_count); IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); - rb.Push(2); + rb.Push(channel_count); } - // Should be similar to QueryAudioDeviceOutputEvent void QueryAudioDeviceInputEvent(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); + LOG_DEBUG(Service_Audio, "(STUBBED) called"); IPC::ResponseBuilder rb{ctx, 2, 1}; rb.Push(ResultSuccess); - rb.PushCopyObjects(buffer_event->GetReadableEvent()); + rb.PushCopyObjects(event->GetReadableEvent()); } void QueryAudioDeviceOutputEvent(Kernel::HLERequestContext& ctx) { @@ -307,402 +342,167 @@ private: IPC::ResponseBuilder rb{ctx, 2, 1}; rb.Push(ResultSuccess); - rb.PushCopyObjects(buffer_event->GetReadableEvent()); + rb.PushCopyObjects(event->GetReadableEvent()); } - Kernel::KEvent* buffer_event; - u32_le revision = 0; + void ListAudioOutputDeviceName(Kernel::HLERequestContext& ctx) { + const size_t in_count = ctx.GetWriteBufferSize() / sizeof(AudioDevice::AudioDeviceName); + + std::vector out_names{}; + + u32 out_count = impl->ListAudioOutputDeviceName(out_names, in_count); + + std::string out{}; + for (u32 i = 0; i < out_count; i++) { + std::string a{}; + u32 j = 0; + while (out_names[i].name[j] != '\0') { + a += out_names[i].name[j]; + j++; + } + out += "\n\t" + a; + } + + LOG_DEBUG(Service_Audio, "called.\nNames={}", out); + + IPC::ResponseBuilder rb{ctx, 3}; + + ctx.WriteBuffer(out_names); + + rb.Push(ResultSuccess); + rb.Push(out_count); + } + + KernelHelpers::ServiceContext service_context; + std::unique_ptr impl; + Kernel::KEvent* event; }; AudRenU::AudRenU(Core::System& system_) - : ServiceFramework{system_, "audren:u"}, service_context{system_, "audren:u"} { + : ServiceFramework{system_, "audren:u", ServiceThreadType::CreateNew}, + service_context{system_, "audren:u"}, impl{std::make_unique(system_)} { // clang-format off static const FunctionInfo functions[] = { {0, &AudRenU::OpenAudioRenderer, "OpenAudioRenderer"}, - {1, &AudRenU::GetAudioRendererWorkBufferSize, "GetWorkBufferSize"}, + {1, &AudRenU::GetWorkBufferSize, "GetWorkBufferSize"}, {2, &AudRenU::GetAudioDeviceService, "GetAudioDeviceService"}, - {3, &AudRenU::OpenAudioRendererForManualExecution, "OpenAudioRendererForManualExecution"}, + {3, nullptr, "OpenAudioRendererForManualExecution"}, {4, &AudRenU::GetAudioDeviceServiceWithRevisionInfo, "GetAudioDeviceServiceWithRevisionInfo"}, }; // clang-format on RegisterHandlers(functions); - - buffer_event = service_context.CreateEvent("IAudioOutBufferReleasedEvent"); } -AudRenU::~AudRenU() { - service_context.CloseEvent(buffer_event); -} +AudRenU::~AudRenU() = default; void AudRenU::OpenAudioRenderer(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); + IPC::RequestParser rp{ctx}; - OpenAudioRendererImpl(ctx); + AudioCore::AudioRendererParameterInternal params; + rp.PopRaw(params); + auto transfer_memory_handle = ctx.GetCopyHandle(0); + auto process_handle = ctx.GetCopyHandle(1); + auto transfer_memory_size = rp.Pop(); + auto applet_resource_user_id = rp.Pop(); + + if (impl->GetSessionCount() + 1 > AudioCore::MaxRendererSessions) { + LOG_ERROR(Service_Audio, "Too many AudioRenderer sessions open!"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERR_MAXIMUM_SESSIONS_REACHED); + return; + } + + const auto& handle_table{system.CurrentProcess()->GetHandleTable()}; + auto process{handle_table.GetObject(process_handle)}; + auto transfer_memory{ + process->GetHandleTable().GetObject(transfer_memory_handle)}; + + const auto session_id{impl->GetSessionId()}; + if (session_id == -1) { + LOG_ERROR(Service_Audio, "Tried to open a session that's already in use!"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERR_MAXIMUM_SESSIONS_REACHED); + return; + } + + LOG_DEBUG(Service_Audio, "Opened new AudioRenderer session {} sessions open {}", session_id, + impl->GetSessionCount()); + + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(ResultSuccess); + rb.PushIpcInterface(system, *impl, params, transfer_memory.GetPointerUnsafe(), + transfer_memory_size, process_handle, + applet_resource_user_id, session_id); } -static u64 CalculateNumPerformanceEntries(const AudioCommon::AudioRendererParameter& params) { - // +1 represents the final mix. - return u64{params.effect_count} + params.submix_count + params.sink_count + params.voice_count + - 1; -} - -void AudRenU::GetAudioRendererWorkBufferSize(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); - - // Several calculations below align the sizes being calculated - // onto a 64 byte boundary. - static constexpr u64 buffer_alignment_size = 64; - - // Some calculations that calculate portions of the buffer - // that will contain information, on the other hand, align - // the result of some of their calcularions on a 16 byte boundary. - static constexpr u64 info_field_alignment_size = 16; - - // Maximum detail entries that may exist at one time for performance - // frame statistics. - static constexpr u64 max_perf_detail_entries = 100; - - // Size of the data structure representing the bulk of the voice-related state. - static constexpr u64 voice_state_size_bytes = 0x100; - - // Size of the upsampler manager data structure - constexpr u64 upsampler_manager_size = 0x48; - - // Calculates the part of the size that relates to mix buffers. - const auto calculate_mix_buffer_sizes = [](const AudioCommon::AudioRendererParameter& params) { - // As of 8.0.0 this is the maximum on voice channels. - constexpr u64 max_voice_channels = 6; - - // The service expects the sample_count member of the parameters to either be - // a value of 160 or 240, so the maximum sample count is assumed in order - // to adequately handle all values at runtime. - constexpr u64 default_max_sample_count = 240; - - const u64 total_mix_buffers = params.mix_buffer_count + max_voice_channels; - - u64 size = 0; - size += total_mix_buffers * (sizeof(s32) * params.sample_count); - size += total_mix_buffers * (sizeof(s32) * default_max_sample_count); - size += u64{params.submix_count} + params.sink_count; - size = Common::AlignUp(size, buffer_alignment_size); - size += Common::AlignUp(params.unknown_30, buffer_alignment_size); - size += Common::AlignUp(sizeof(s32) * params.mix_buffer_count, buffer_alignment_size); - return size; - }; - - // Calculates the portion of the size related to the mix data (and the sorting thereof). - const auto calculate_mix_info_size = [](const AudioCommon::AudioRendererParameter& params) { - // The size of the mixing info data structure. - constexpr u64 mix_info_size = 0x940; - - // Consists of total submixes with the final mix included. - const u64 total_mix_count = u64{params.submix_count} + 1; - - // The total number of effects that may be available to the audio renderer at any time. - constexpr u64 max_effects = 256; - - // Calculates the part of the size related to the audio node state. - // This will only be used if the audio revision supports the splitter. - const auto calculate_node_state_size = [](std::size_t num_nodes) { - // Internally within a nodestate, it appears to use a data structure - // similar to a std::bitset<64> twice. - constexpr u64 bit_size = Common::BitSize(); - constexpr u64 num_bitsets = 2; - - // Node state instances have three states internally for performing - // depth-first searches of nodes. Initialized, Found, and Done Sorting. - constexpr u64 num_states = 3; - - u64 size = 0; - size += (num_nodes * num_nodes) * sizeof(s32); - size += num_states * (num_nodes * sizeof(s32)); - size += num_bitsets * (Common::AlignUp(num_nodes, bit_size) / Common::BitSize()); - return size; - }; - - // Calculates the part of the size related to the adjacency (aka edge) matrix. - const auto calculate_edge_matrix_size = [](std::size_t num_nodes) { - return (num_nodes * num_nodes) * sizeof(s32); - }; - - u64 size = 0; - size += Common::AlignUp(sizeof(void*) * total_mix_count, info_field_alignment_size); - size += Common::AlignUp(mix_info_size * total_mix_count, info_field_alignment_size); - size += Common::AlignUp(sizeof(s32) * max_effects * params.submix_count, - info_field_alignment_size); - - if (IsFeatureSupported(AudioFeatures::Splitter, params.revision)) { - size += Common::AlignUp(calculate_node_state_size(total_mix_count) + - calculate_edge_matrix_size(total_mix_count), - info_field_alignment_size); - } - - return size; - }; - - // Calculates the part of the size related to voice channel info. - const auto calculate_voice_info_size = [](const AudioCommon::AudioRendererParameter& params) { - constexpr u64 voice_info_size = 0x220; - constexpr u64 voice_resource_size = 0xD0; - - u64 size = 0; - size += Common::AlignUp(sizeof(void*) * params.voice_count, info_field_alignment_size); - size += Common::AlignUp(voice_info_size * params.voice_count, info_field_alignment_size); - size += - Common::AlignUp(voice_resource_size * params.voice_count, info_field_alignment_size); - size += - Common::AlignUp(voice_state_size_bytes * params.voice_count, info_field_alignment_size); - return size; - }; - - // Calculates the part of the size related to memory pools. - const auto calculate_memory_pools_size = [](const AudioCommon::AudioRendererParameter& params) { - const u64 num_memory_pools = sizeof(s32) * (u64{params.effect_count} + params.voice_count); - const u64 memory_pool_info_size = 0x20; - return Common::AlignUp(num_memory_pools * memory_pool_info_size, info_field_alignment_size); - }; - - // Calculates the part of the size related to the splitter context. - const auto calculate_splitter_context_size = - [](const AudioCommon::AudioRendererParameter& params) -> u64 { - if (!IsFeatureSupported(AudioFeatures::Splitter, params.revision)) { - return 0; - } - - constexpr u64 splitter_info_size = 0x20; - constexpr u64 splitter_destination_data_size = 0xE0; - - u64 size = 0; - size += params.num_splitter_send_channels; - size += - Common::AlignUp(splitter_info_size * params.splitter_count, info_field_alignment_size); - size += Common::AlignUp(splitter_destination_data_size * params.num_splitter_send_channels, - info_field_alignment_size); - - return size; - }; - - // Calculates the part of the size related to the upsampler info. - const auto calculate_upsampler_info_size = - [](const AudioCommon::AudioRendererParameter& params) { - constexpr u64 upsampler_info_size = 0x280; - // Yes, using the buffer size over info alignment size is intentional here. - return Common::AlignUp(upsampler_info_size * - (u64{params.submix_count} + params.sink_count), - buffer_alignment_size); - }; - - // Calculates the part of the size related to effect info. - const auto calculate_effect_info_size = [](const AudioCommon::AudioRendererParameter& params) { - constexpr u64 effect_info_size = 0x2B0; - return Common::AlignUp(effect_info_size * params.effect_count, info_field_alignment_size); - }; - - // Calculates the part of the size related to audio sink info. - const auto calculate_sink_info_size = [](const AudioCommon::AudioRendererParameter& params) { - const u64 sink_info_size = 0x170; - return Common::AlignUp(sink_info_size * params.sink_count, info_field_alignment_size); - }; - - // Calculates the part of the size related to voice state info. - const auto calculate_voice_state_size = [](const AudioCommon::AudioRendererParameter& params) { - const u64 voice_state_size = 0x100; - const u64 additional_size = buffer_alignment_size - 1; - return Common::AlignUp(voice_state_size * params.voice_count + additional_size, - info_field_alignment_size); - }; - - // Calculates the part of the size related to performance statistics. - const auto calculate_perf_size = [](const AudioCommon::AudioRendererParameter& params) { - // Extra size value appended to the end of the calculation. - constexpr u64 appended = 128; - - // Whether or not we assume the newer version of performance metrics data structures. - const bool is_v2 = - IsFeatureSupported(AudioFeatures::PerformanceMetricsVersion2, params.revision); - - // Data structure sizes - constexpr u64 perf_statistics_size = 0x0C; - const u64 header_size = is_v2 ? 0x30 : 0x18; - const u64 entry_size = is_v2 ? 0x18 : 0x10; - const u64 detail_size = is_v2 ? 0x18 : 0x10; - - const u64 entry_count = CalculateNumPerformanceEntries(params); - const u64 size_per_frame = - header_size + (entry_size * entry_count) + (detail_size * max_perf_detail_entries); - - u64 size = 0; - size += Common::AlignUp(size_per_frame * params.performance_frame_count + 1, - buffer_alignment_size); - size += Common::AlignUp(perf_statistics_size, buffer_alignment_size); - size += appended; - return size; - }; - - // Calculates the part of the size that relates to the audio command buffer. - const auto calculate_command_buffer_size = - [](const AudioCommon::AudioRendererParameter& params) { - constexpr u64 alignment = (buffer_alignment_size - 1) * 2; - - if (!IsFeatureSupported(AudioFeatures::VariadicCommandBuffer, params.revision)) { - constexpr u64 command_buffer_size = 0x18000; - - return command_buffer_size + alignment; - } - - // When the variadic command buffer is supported, this means - // the command generator for the audio renderer can issue commands - // that are (as one would expect), variable in size. So what we need to do - // is determine the maximum possible size for a few command data structures - // then multiply them by the amount of present commands indicated by the given - // respective audio parameters. - - constexpr u64 max_biquad_filters = 2; - constexpr u64 max_mix_buffers = 24; - - constexpr u64 biquad_filter_command_size = 0x2C; - - constexpr u64 depop_mix_command_size = 0x24; - constexpr u64 depop_setup_command_size = 0x50; - - constexpr u64 effect_command_max_size = 0x540; - - constexpr u64 mix_command_size = 0x1C; - constexpr u64 mix_ramp_command_size = 0x24; - constexpr u64 mix_ramp_grouped_command_size = 0x13C; - - constexpr u64 perf_command_size = 0x28; - - constexpr u64 sink_command_size = 0x130; - - constexpr u64 submix_command_max_size = - depop_mix_command_size + (mix_command_size * max_mix_buffers) * max_mix_buffers; - - constexpr u64 volume_command_size = 0x1C; - constexpr u64 volume_ramp_command_size = 0x20; - - constexpr u64 voice_biquad_filter_command_size = - biquad_filter_command_size * max_biquad_filters; - constexpr u64 voice_data_command_size = 0x9C; - const u64 voice_command_max_size = - (params.splitter_count * depop_setup_command_size) + - (voice_data_command_size + voice_biquad_filter_command_size + - volume_ramp_command_size + mix_ramp_grouped_command_size); - - // Now calculate the individual elements that comprise the size and add them together. - const u64 effect_commands_size = params.effect_count * effect_command_max_size; - - const u64 final_mix_commands_size = - depop_mix_command_size + volume_command_size * max_mix_buffers; - - const u64 perf_commands_size = - perf_command_size * - (CalculateNumPerformanceEntries(params) + max_perf_detail_entries); - - const u64 sink_commands_size = params.sink_count * sink_command_size; - - const u64 splitter_commands_size = - params.num_splitter_send_channels * max_mix_buffers * mix_ramp_command_size; - - const u64 submix_commands_size = params.submix_count * submix_command_max_size; - - const u64 voice_commands_size = params.voice_count * voice_command_max_size; - - return effect_commands_size + final_mix_commands_size + perf_commands_size + - sink_commands_size + splitter_commands_size + submix_commands_size + - voice_commands_size + alignment; - }; +void AudRenU::GetWorkBufferSize(Kernel::HLERequestContext& ctx) { + AudioCore::AudioRendererParameterInternal params; IPC::RequestParser rp{ctx}; - const auto params = rp.PopRaw(); + rp.PopRaw(params); - u64 size = 0; - size += calculate_mix_buffer_sizes(params); - size += calculate_mix_info_size(params); - size += calculate_voice_info_size(params); - size += upsampler_manager_size; - size += calculate_memory_pools_size(params); - size += calculate_splitter_context_size(params); + u64 size{0}; + auto result = impl->GetWorkBufferSize(params, size); - size = Common::AlignUp(size, buffer_alignment_size); + std::string output_info{}; + output_info += fmt::format("\tRevision {}", AudioCore::GetRevisionNum(params.revision)); + output_info += + fmt::format("\n\tSample Rate {}, Sample Count {}", params.sample_rate, params.sample_count); + output_info += fmt::format("\n\tExecution Mode {}, Voice Drop Enabled {}", + static_cast(params.execution_mode), params.voice_drop_enabled); + output_info += fmt::format( + "\n\tSizes: Effects {:04X}, Mixes {:04X}, Sinks {:04X}, Submixes {:04X}, Splitter Infos " + "{:04X}, Splitter Destinations {:04X}, Voices {:04X}, Performance Frames {:04X} External " + "Context {:04X}", + params.effects, params.mixes, params.sinks, params.sub_mixes, params.splitter_infos, + params.splitter_destinations, params.voices, params.perf_frames, + params.external_context_size); - size += calculate_upsampler_info_size(params); - size += calculate_effect_info_size(params); - size += calculate_sink_info_size(params); - size += calculate_voice_state_size(params); - size += calculate_perf_size(params); - size += calculate_command_buffer_size(params); - - // finally, 4KB page align the size, and we're done. - size = Common::AlignUp(size, 4096); + LOG_DEBUG(Service_Audio, "called.\nInput params:\n{}\nOutput params:\n\tWorkbuffer size {:08X}", + output_info, size); IPC::ResponseBuilder rb{ctx, 4}; - rb.Push(ResultSuccess); + rb.Push(result); rb.Push(size); - - LOG_DEBUG(Service_Audio, "buffer_size=0x{:X}", size); } void AudRenU::GetAudioDeviceService(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; - const u64 aruid = rp.Pop(); - LOG_DEBUG(Service_Audio, "called. aruid={:016X}", aruid); + const auto applet_resource_user_id = rp.Pop(); + + LOG_DEBUG(Service_Audio, "called. Applet resource id {}", applet_resource_user_id); - // Revisionless variant of GetAudioDeviceServiceWithRevisionInfo that - // always assumes the initial release revision (REV1). IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(ResultSuccess); - rb.PushIpcInterface(system, buffer_event, Common::MakeMagic('R', 'E', 'V', '1')); + rb.PushIpcInterface(system, applet_resource_user_id, + ::Common::MakeMagic('R', 'E', 'V', '1'), num_audio_devices++); } void AudRenU::OpenAudioRendererForManualExecution(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_Audio, "called"); - - OpenAudioRendererImpl(ctx); } void AudRenU::GetAudioDeviceServiceWithRevisionInfo(Kernel::HLERequestContext& ctx) { struct Parameters { u32 revision; - u64 aruid; + u64 applet_resource_user_id; }; IPC::RequestParser rp{ctx}; - const auto [revision, aruid] = rp.PopRaw(); - LOG_DEBUG(Service_Audio, "called. revision={:08X}, aruid={:016X}", revision, aruid); + const auto [revision, applet_resource_user_id] = rp.PopRaw(); - IPC::ResponseBuilder rb{ctx, 2, 0, 1}; - rb.Push(ResultSuccess); - rb.PushIpcInterface(system, buffer_event, revision); -} + LOG_DEBUG(Service_Audio, "called. Revision {} Applet resource id {}", + AudioCore::GetRevisionNum(revision), applet_resource_user_id); -void AudRenU::OpenAudioRendererImpl(Kernel::HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const auto params = rp.PopRaw(); IPC::ResponseBuilder rb{ctx, 2, 0, 1}; rb.Push(ResultSuccess); - rb.PushIpcInterface(system, params, audren_instance_count++); -} - -bool IsFeatureSupported(AudioFeatures feature, u32_le revision) { - // Byte swap - const u32_be version_num = revision - Common::MakeMagic('R', 'E', 'V', '0'); - - switch (feature) { - case AudioFeatures::AudioUSBDeviceOutput: - return version_num >= 4U; - case AudioFeatures::Splitter: - return version_num >= 2U; - case AudioFeatures::PerformanceMetricsVersion2: - case AudioFeatures::VariadicCommandBuffer: - return version_num >= 5U; - default: - return false; - } + rb.PushIpcInterface(system, applet_resource_user_id, revision, + num_audio_devices++); } } // namespace Service::Audio diff --git a/src/core/hle/service/audio/audren_u.h b/src/core/hle/service/audio/audren_u.h index 869d39002..4384a9b3c 100755 --- a/src/core/hle/service/audio/audren_u.h +++ b/src/core/hle/service/audio/audren_u.h @@ -3,6 +3,7 @@ #pragma once +#include "audio_core/audio_render_manager.h" #include "core/hle/service/kernel_helpers.h" #include "core/hle/service/service.h" @@ -15,6 +16,7 @@ class HLERequestContext; } namespace Service::Audio { +class IAudioRenderer; class AudRenU final : public ServiceFramework { public: @@ -23,28 +25,14 @@ public: private: void OpenAudioRenderer(Kernel::HLERequestContext& ctx); - void GetAudioRendererWorkBufferSize(Kernel::HLERequestContext& ctx); + void GetWorkBufferSize(Kernel::HLERequestContext& ctx); void GetAudioDeviceService(Kernel::HLERequestContext& ctx); void OpenAudioRendererForManualExecution(Kernel::HLERequestContext& ctx); void GetAudioDeviceServiceWithRevisionInfo(Kernel::HLERequestContext& ctx); - void OpenAudioRendererImpl(Kernel::HLERequestContext& ctx); - KernelHelpers::ServiceContext service_context; - - std::size_t audren_instance_count = 0; - Kernel::KEvent* buffer_event; + std::unique_ptr impl; + u32 num_audio_devices{0}; }; -// Describes a particular audio feature that may be supported in a particular revision. -enum class AudioFeatures : u32 { - AudioUSBDeviceOutput, - Splitter, - PerformanceMetricsVersion2, - VariadicCommandBuffer, -}; - -// Tests if a particular audio feature is supported with a given audio revision. -bool IsFeatureSupported(AudioFeatures feature, u32_le revision); - } // namespace Service::Audio diff --git a/src/core/hle/service/audio/errors.h b/src/core/hle/service/audio/errors.h index ac6c514af..d706978cb 100755 --- a/src/core/hle/service/audio/errors.h +++ b/src/core/hle/service/audio/errors.h @@ -7,8 +7,17 @@ namespace Service::Audio { +constexpr Result ERR_INVALID_DEVICE_NAME{ErrorModule::Audio, 1}; constexpr Result ERR_OPERATION_FAILED{ErrorModule::Audio, 2}; +constexpr Result ERR_INVALID_SAMPLE_RATE{ErrorModule::Audio, 3}; +constexpr Result ERR_INSUFFICIENT_BUFFER_SIZE{ErrorModule::Audio, 4}; +constexpr Result ERR_MAXIMUM_SESSIONS_REACHED{ErrorModule::Audio, 5}; constexpr Result ERR_BUFFER_COUNT_EXCEEDED{ErrorModule::Audio, 8}; +constexpr Result ERR_INVALID_CHANNEL_COUNT{ErrorModule::Audio, 10}; +constexpr Result ERR_INVALID_UPDATE_DATA{ErrorModule::Audio, 41}; +constexpr Result ERR_POOL_MAPPING_FAILED{ErrorModule::Audio, 42}; constexpr Result ERR_NOT_SUPPORTED{ErrorModule::Audio, 513}; +constexpr Result ERR_INVALID_PROCESS_HANDLE{ErrorModule::Audio, 1536}; +constexpr Result ERR_INVALID_REVISION{ErrorModule::Audio, 1537}; } // namespace Service::Audio diff --git a/src/core/memory.cpp b/src/core/memory.cpp index a64273af5..8e5775e3f 100755 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -511,7 +511,7 @@ struct Memory::Impl { [[nodiscard]] u8* GetPointerImpl(VAddr vaddr, auto on_unmapped, auto on_rasterizer) const { // AARCH64 masks the upper 16 bit of all memory accesses - vaddr &= 0xffffffffffffLL; + vaddr &= 0xffffffffffffULL; if (vaddr >= 1uLL << current_page_table->GetAddressSpaceBits()) { on_unmapped(); @@ -785,6 +785,10 @@ void Memory::CopyBlock(const Kernel::KProcess& process, VAddr dest_addr, VAddr s impl->CopyBlock(process, dest_addr, src_addr, size); } +void Memory::ZeroBlock(const Kernel::KProcess& process, VAddr dest_addr, const std::size_t size) { + impl->ZeroBlock(process, dest_addr, size); +} + void Memory::RasterizerMarkRegionCached(VAddr vaddr, u64 size, bool cached) { impl->RasterizerMarkRegionCached(vaddr, size, cached); } diff --git a/src/core/memory.h b/src/core/memory.h index 9a08a1597..2122e6692 100755 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -437,6 +437,19 @@ public: void CopyBlock(const Kernel::KProcess& process, VAddr dest_addr, VAddr src_addr, std::size_t size); + /** + * Zeros a range of bytes within the current process' address space at the specified + * virtual address. + * + * @param process The process that will have data zeroed within its address space. + * @param dest_addr The destination virtual address to zero the data from. + * @param size The size of the range to zero out, in bytes. + * + * @post The range [dest_addr, size) within the process' address space contains the + * value 0. + */ + void ZeroBlock(const Kernel::KProcess& process, VAddr dest_addr, std::size_t size); + /** * Marks each page within the specified address range as cached or uncached. * diff --git a/src/video_core/texture_cache/util.cpp b/src/video_core/texture_cache/util.cpp index e0fbd0159..1223df5a0 100755 --- a/src/video_core/texture_cache/util.cpp +++ b/src/video_core/texture_cache/util.cpp @@ -510,18 +510,21 @@ void SwizzleBlockLinearImage(Tegra::MemoryManager& gpu_memory, GPUVAddr gpu_addr const LevelInfo level_info = MakeLevelInfo(info); const Extent2D tile_size = DefaultBlockSize(info.format); const u32 bytes_per_block = BytesPerBlock(info.format); - const u32 bpp_log2 = BytesPerBlockLog2(info.format); const s32 level = copy.image_subresource.base_level; const Extent3D level_size = AdjustMipSize(size, level); const u32 num_blocks_per_layer = NumBlocks(level_size, tile_size); const u32 host_bytes_per_layer = num_blocks_per_layer * bytes_per_block; + UNIMPLEMENTED_IF(info.tile_width_spacing > 0); UNIMPLEMENTED_IF(copy.image_offset.x != 0); UNIMPLEMENTED_IF(copy.image_offset.y != 0); UNIMPLEMENTED_IF(copy.image_offset.z != 0); UNIMPLEMENTED_IF(copy.image_extent != level_size); + const Extent3D num_tiles = AdjustTileSize(level_size, tile_size); + const Extent3D block = AdjustMipBlockSize(num_tiles, level_info.block, level); + size_t host_offset = copy.buffer_offset; const u32 num_levels = info.resources.levels; @@ -532,12 +535,6 @@ void SwizzleBlockLinearImage(Tegra::MemoryManager& gpu_memory, GPUVAddr gpu_addr tile_size.height, info.tile_width_spacing); const size_t subresource_size = sizes[level]; - const Extent2D gob = GobSize(bpp_log2, level_info.block.height, info.tile_width_spacing); - - const Extent3D num_tiles = AdjustTileSize(level_size, tile_size); - const Extent3D block = AdjustMipBlockSize(num_tiles, level_info.block, level); - const u32 stride_alignment = StrideAlignment(num_tiles, level_info.block, gob, bpp_log2); - const auto dst_data = std::make_unique(subresource_size); const std::span dst(dst_data.get(), subresource_size); @@ -546,7 +543,7 @@ void SwizzleBlockLinearImage(Tegra::MemoryManager& gpu_memory, GPUVAddr gpu_addr gpu_memory.ReadBlockUnsafe(gpu_addr + guest_offset, dst.data(), dst.size_bytes()); SwizzleTexture(dst, src, bytes_per_block, num_tiles.width, num_tiles.height, - num_tiles.depth, block.height, block.depth, stride_alignment); + num_tiles.depth, block.height, block.depth); gpu_memory.WriteBlockUnsafe(gpu_addr + guest_offset, dst.data(), dst.size_bytes()); diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index 9686412d0..ccafc9fa3 100755 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -372,8 +372,9 @@ void Config::ReadAudioValues() { qt_config->beginGroup(QStringLiteral("Audio")); if (global) { - ReadBasicSetting(Settings::values.audio_device_id); ReadBasicSetting(Settings::values.sink_id); + ReadBasicSetting(Settings::values.audio_output_device_id); + ReadBasicSetting(Settings::values.audio_input_device_id); } ReadGlobalSetting(Settings::values.volume); @@ -1028,7 +1029,8 @@ void Config::SaveAudioValues() { if (global) { WriteBasicSetting(Settings::values.sink_id); - WriteBasicSetting(Settings::values.audio_device_id); + WriteBasicSetting(Settings::values.audio_output_device_id); + WriteBasicSetting(Settings::values.audio_input_device_id); } WriteGlobalSetting(Settings::values.volume); diff --git a/src/yuzu/configuration/configure_audio.cpp b/src/yuzu/configuration/configure_audio.cpp index 512bdfc22..19b8b15ef 100755 --- a/src/yuzu/configuration/configure_audio.cpp +++ b/src/yuzu/configuration/configure_audio.cpp @@ -3,8 +3,8 @@ #include -#include "audio_core/sink.h" -#include "audio_core/sink_details.h" +#include "audio_core/sink/sink.h" +#include "audio_core/sink/sink_details.h" #include "common/settings.h" #include "core/core.h" #include "ui_configure_audio.h" @@ -15,11 +15,11 @@ ConfigureAudio::ConfigureAudio(const Core::System& system_, QWidget* parent) : QWidget(parent), ui(std::make_unique()), system{system_} { ui->setupUi(this); - InitializeAudioOutputSinkComboBox(); + InitializeAudioSinkComboBox(); connect(ui->volume_slider, &QSlider::valueChanged, this, &ConfigureAudio::SetVolumeIndicatorText); - connect(ui->output_sink_combo_box, qOverload(&QComboBox::currentIndexChanged), this, + connect(ui->sink_combo_box, qOverload(&QComboBox::currentIndexChanged), this, &ConfigureAudio::UpdateAudioDevices); ui->volume_label->setVisible(Settings::IsConfiguringGlobal()); @@ -30,8 +30,9 @@ ConfigureAudio::ConfigureAudio(const Core::System& system_, QWidget* parent) SetConfiguration(); const bool is_powered_on = system_.IsPoweredOn(); - ui->output_sink_combo_box->setEnabled(!is_powered_on); - ui->audio_device_combo_box->setEnabled(!is_powered_on); + ui->sink_combo_box->setEnabled(!is_powered_on); + ui->output_combo_box->setEnabled(!is_powered_on); + ui->input_combo_box->setEnabled(!is_powered_on); } ConfigureAudio::~ConfigureAudio() = default; @@ -40,9 +41,9 @@ void ConfigureAudio::SetConfiguration() { SetOutputSinkFromSinkID(); // The device list cannot be pre-populated (nor listed) until the output sink is known. - UpdateAudioDevices(ui->output_sink_combo_box->currentIndex()); + UpdateAudioDevices(ui->sink_combo_box->currentIndex()); - SetAudioDeviceFromDeviceID(); + SetAudioDevicesFromDeviceID(); const auto volume_value = static_cast(Settings::values.volume.GetValue()); ui->volume_slider->setValue(volume_value); @@ -62,32 +63,45 @@ void ConfigureAudio::SetConfiguration() { } void ConfigureAudio::SetOutputSinkFromSinkID() { - [[maybe_unused]] const QSignalBlocker blocker(ui->output_sink_combo_box); + [[maybe_unused]] const QSignalBlocker blocker(ui->sink_combo_box); int new_sink_index = 0; const QString sink_id = QString::fromStdString(Settings::values.sink_id.GetValue()); - for (int index = 0; index < ui->output_sink_combo_box->count(); index++) { - if (ui->output_sink_combo_box->itemText(index) == sink_id) { + for (int index = 0; index < ui->sink_combo_box->count(); index++) { + if (ui->sink_combo_box->itemText(index) == sink_id) { new_sink_index = index; break; } } - ui->output_sink_combo_box->setCurrentIndex(new_sink_index); + ui->sink_combo_box->setCurrentIndex(new_sink_index); } -void ConfigureAudio::SetAudioDeviceFromDeviceID() { +void ConfigureAudio::SetAudioDevicesFromDeviceID() { int new_device_index = -1; - const QString device_id = QString::fromStdString(Settings::values.audio_device_id.GetValue()); - for (int index = 0; index < ui->audio_device_combo_box->count(); index++) { - if (ui->audio_device_combo_box->itemText(index) == device_id) { + const QString output_device_id = + QString::fromStdString(Settings::values.audio_output_device_id.GetValue()); + for (int index = 0; index < ui->output_combo_box->count(); index++) { + if (ui->output_combo_box->itemText(index) == output_device_id) { new_device_index = index; break; } } - ui->audio_device_combo_box->setCurrentIndex(new_device_index); + ui->output_combo_box->setCurrentIndex(new_device_index); + + new_device_index = -1; + const QString input_device_id = + QString::fromStdString(Settings::values.audio_input_device_id.GetValue()); + for (int index = 0; index < ui->input_combo_box->count(); index++) { + if (ui->input_combo_box->itemText(index) == input_device_id) { + new_device_index = index; + break; + } + } + + ui->input_combo_box->setCurrentIndex(new_device_index); } void ConfigureAudio::SetVolumeIndicatorText(int percentage) { @@ -95,14 +109,13 @@ void ConfigureAudio::SetVolumeIndicatorText(int percentage) { } void ConfigureAudio::ApplyConfiguration() { - if (Settings::IsConfiguringGlobal()) { Settings::values.sink_id = - ui->output_sink_combo_box->itemText(ui->output_sink_combo_box->currentIndex()) - .toStdString(); - Settings::values.audio_device_id.SetValue( - ui->audio_device_combo_box->itemText(ui->audio_device_combo_box->currentIndex()) - .toStdString()); + ui->sink_combo_box->itemText(ui->sink_combo_box->currentIndex()).toStdString(); + Settings::values.audio_output_device_id.SetValue( + ui->output_combo_box->itemText(ui->output_combo_box->currentIndex()).toStdString()); + Settings::values.audio_input_device_id.SetValue( + ui->input_combo_box->itemText(ui->input_combo_box->currentIndex()).toStdString()); // Guard if during game and set to game-specific value if (Settings::values.volume.UsingGlobal()) { @@ -129,21 +142,27 @@ void ConfigureAudio::changeEvent(QEvent* event) { } void ConfigureAudio::UpdateAudioDevices(int sink_index) { - ui->audio_device_combo_box->clear(); - ui->audio_device_combo_box->addItem(QString::fromUtf8(AudioCore::auto_device_name)); + ui->output_combo_box->clear(); + ui->output_combo_box->addItem(QString::fromUtf8(AudioCore::Sink::auto_device_name)); - const std::string sink_id = ui->output_sink_combo_box->itemText(sink_index).toStdString(); - for (const auto& device : AudioCore::GetDeviceListForSink(sink_id)) { - ui->audio_device_combo_box->addItem(QString::fromStdString(device)); + const std::string sink_id = ui->sink_combo_box->itemText(sink_index).toStdString(); + for (const auto& device : AudioCore::Sink::GetDeviceListForSink(sink_id, false)) { + ui->output_combo_box->addItem(QString::fromStdString(device)); + } + + ui->input_combo_box->clear(); + ui->input_combo_box->addItem(QString::fromUtf8(AudioCore::Sink::auto_device_name)); + for (const auto& device : AudioCore::Sink::GetDeviceListForSink(sink_id, true)) { + ui->input_combo_box->addItem(QString::fromStdString(device)); } } -void ConfigureAudio::InitializeAudioOutputSinkComboBox() { - ui->output_sink_combo_box->clear(); - ui->output_sink_combo_box->addItem(QString::fromUtf8(AudioCore::auto_device_name)); +void ConfigureAudio::InitializeAudioSinkComboBox() { + ui->sink_combo_box->clear(); + ui->sink_combo_box->addItem(QString::fromUtf8(AudioCore::Sink::auto_device_name)); - for (const char* id : AudioCore::GetSinkIDs()) { - ui->output_sink_combo_box->addItem(QString::fromUtf8(id)); + for (const char* id : AudioCore::Sink::GetSinkIDs()) { + ui->sink_combo_box->addItem(QString::fromUtf8(id)); } } @@ -164,8 +183,10 @@ void ConfigureAudio::SetupPerGameUI() { ConfigurationShared::SetHighlight(ui->volume_layout, index == 1); }); - ui->output_sink_combo_box->setVisible(false); - ui->output_sink_label->setVisible(false); - ui->audio_device_combo_box->setVisible(false); - ui->audio_device_label->setVisible(false); + ui->sink_combo_box->setVisible(false); + ui->sink_label->setVisible(false); + ui->output_combo_box->setVisible(false); + ui->output_label->setVisible(false); + ui->input_combo_box->setVisible(false); + ui->input_label->setVisible(false); } diff --git a/src/yuzu/configuration/configure_audio.h b/src/yuzu/configuration/configure_audio.h index 08c278eeb..0d03aae1d 100755 --- a/src/yuzu/configuration/configure_audio.h +++ b/src/yuzu/configuration/configure_audio.h @@ -31,14 +31,14 @@ public: private: void changeEvent(QEvent* event) override; - void InitializeAudioOutputSinkComboBox(); + void InitializeAudioSinkComboBox(); void RetranslateUI(); void UpdateAudioDevices(int sink_index); void SetOutputSinkFromSinkID(); - void SetAudioDeviceFromDeviceID(); + void SetAudioDevicesFromDeviceID(); void SetVolumeIndicatorText(int percentage); void SetupPerGameUI(); diff --git a/src/yuzu/configuration/configure_audio.ui b/src/yuzu/configuration/configure_audio.ui index d1ac8ad02..a5bcee415 100755 --- a/src/yuzu/configuration/configure_audio.ui +++ b/src/yuzu/configuration/configure_audio.ui @@ -21,30 +21,44 @@ - + - + Output Engine: - + - + - + - Audio Device: + Output Device - + + + + + + + + + + Input Device + + + + + diff --git a/src/yuzu/configuration/configure_debug.cpp b/src/yuzu/configuration/configure_debug.cpp index 343d2aee1..84808f678 100755 --- a/src/yuzu/configuration/configure_debug.cpp +++ b/src/yuzu/configuration/configure_debug.cpp @@ -44,6 +44,7 @@ void ConfigureDebug::SetConfiguration() { ui->fs_access_log->setEnabled(runtime_lock); ui->fs_access_log->setChecked(Settings::values.enable_fs_access_log.GetValue()); ui->reporting_services->setChecked(Settings::values.reporting_services.GetValue()); + ui->dump_audio_commands->setChecked(Settings::values.dump_audio_commands.GetValue()); ui->quest_flag->setChecked(Settings::values.quest_flag.GetValue()); ui->use_debug_asserts->setChecked(Settings::values.use_debug_asserts.GetValue()); ui->use_auto_stub->setChecked(Settings::values.use_auto_stub.GetValue()); @@ -83,6 +84,7 @@ void ConfigureDebug::ApplyConfiguration() { Settings::values.program_args = ui->homebrew_args_edit->text().toStdString(); Settings::values.enable_fs_access_log = ui->fs_access_log->isChecked(); Settings::values.reporting_services = ui->reporting_services->isChecked(); + Settings::values.dump_audio_commands = ui->dump_audio_commands->isChecked(); Settings::values.quest_flag = ui->quest_flag->isChecked(); Settings::values.use_debug_asserts = ui->use_debug_asserts->isChecked(); Settings::values.use_auto_stub = ui->use_auto_stub->isChecked(); diff --git a/src/yuzu/configuration/configure_debug.ui b/src/yuzu/configuration/configure_debug.ui index 1152fa6c6..4c16274fc 100755 --- a/src/yuzu/configuration/configure_debug.ui +++ b/src/yuzu/configuration/configure_debug.ui @@ -235,6 +235,16 @@ + + + Dump Audio Commands To Console** + + + Enable this to output the latest generated audio command list to the console. Only affects games using the audio renderer. + + + + Enable Verbose Reporting Services** @@ -325,6 +335,7 @@ disable_loop_safety_checks fs_access_log reporting_services + dump_audio_commands quest_flag enable_cpu_debugging use_debug_asserts diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index fbba811bc..6f57861ad 100755 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -1501,6 +1501,8 @@ void GMainWindow::BootGame(const QString& filename, u64 program_id, std::size_t if (!LoadROM(filename, program_id, program_index)) return; + system->SetShuttingDown(false); + // Create and start the emulation thread emu_thread = std::make_unique(*system); emit EmulationStarting(emu_thread.get()); @@ -1591,6 +1593,7 @@ void GMainWindow::ShutdownGame() { AllowOSSleep(); + system->SetShuttingDown(true); system->DetachDebugger(); discord_rpc->Pause(); emu_thread->RequestStop(); diff --git a/src/yuzu_cmd/config.cpp b/src/yuzu_cmd/config.cpp index 903e02297..ad6d4804e 100755 --- a/src/yuzu_cmd/config.cpp +++ b/src/yuzu_cmd/config.cpp @@ -324,7 +324,7 @@ void Config::ReadValues() { // Audio ReadSetting("Audio", Settings::values.sink_id); - ReadSetting("Audio", Settings::values.audio_device_id); + ReadSetting("Audio", Settings::values.audio_output_device_id); ReadSetting("Audio", Settings::values.volume); // Miscellaneous