summaryrefslogtreecommitdiffstats
path: root/src/audio_core/renderer/command/effect
diff options
context:
space:
mode:
Diffstat (limited to 'src/audio_core/renderer/command/effect')
-rw-r--r--src/audio_core/renderer/command/effect/aux_.cpp207
-rw-r--r--src/audio_core/renderer/command/effect/aux_.h66
-rw-r--r--src/audio_core/renderer/command/effect/biquad_filter.cpp118
-rw-r--r--src/audio_core/renderer/command/effect/biquad_filter.h74
-rw-r--r--src/audio_core/renderer/command/effect/capture.cpp142
-rw-r--r--src/audio_core/renderer/command/effect/capture.h62
-rw-r--r--src/audio_core/renderer/command/effect/compressor.cpp156
-rw-r--r--src/audio_core/renderer/command/effect/compressor.h60
-rw-r--r--src/audio_core/renderer/command/effect/delay.cpp238
-rw-r--r--src/audio_core/renderer/command/effect/delay.h60
-rw-r--r--src/audio_core/renderer/command/effect/i3dl2_reverb.cpp437
-rw-r--r--src/audio_core/renderer/command/effect/i3dl2_reverb.h60
-rw-r--r--src/audio_core/renderer/command/effect/light_limiter.cpp222
-rw-r--r--src/audio_core/renderer/command/effect/light_limiter.h103
-rw-r--r--src/audio_core/renderer/command/effect/multi_tap_biquad_filter.cpp45
-rw-r--r--src/audio_core/renderer/command/effect/multi_tap_biquad_filter.h59
-rw-r--r--src/audio_core/renderer/command/effect/reverb.cpp440
-rw-r--r--src/audio_core/renderer/command/effect/reverb.h62
18 files changed, 2611 insertions, 0 deletions
diff --git a/src/audio_core/renderer/command/effect/aux_.cpp b/src/audio_core/renderer/command/effect/aux_.cpp
new file mode 100644
index 000000000..e76db893f
--- /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/aux_.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<AuxInfo::AuxInfoDsp*>(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<const s32> 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<s32> 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 100644
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 <string>
+
+#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 100644
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<s32> output, std::span<const s32> input,
+ std::array<s16, 3>& b_, std::array<s16, 2>& a_,
+ VoiceState::BiquadFilterState& state, const u32 sample_count) {
+ constexpr s64 min{std::numeric_limits<s32>::min()};
+ constexpr s64 max{std::numeric_limits<s32>::max()};
+ std::array<f64, 3> 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<f64, 2> a{Common::FixedPoint<50, 14>::from_base(a_[0]).to_double(),
+ Common::FixedPoint<50, 14>::from_base(a_[1]).to_double()};
+ std::array<f64, 4> 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<f64>(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<s32>(std::clamp(static_cast<s64>(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<s32> output, std::span<const s32> input,
+ std::array<s16, 3>& b_, std::array<s16, 2>& a_,
+ VoiceState::BiquadFilterState& state, const u32 sample_count) {
+ constexpr s64 min{std::numeric_limits<s32>::min()};
+ constexpr s64 max{std::numeric_limits<s32>::max()};
+ std::array<Common::FixedPoint<50, 14>, 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<Common::FixedPoint<50, 14>, 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<s32>(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<VoiceState::BiquadFilterState*>(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 100644
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 <string>
+
+#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<s32> output, std::span<const s32> input,
+ std::array<s16, 3>& b, std::array<s16, 2>& 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 100644
index 000000000..042fd286e
--- /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/aux_.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<const s32> 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 100644
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 <string>
+
+#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/compressor.cpp b/src/audio_core/renderer/command/effect/compressor.cpp
new file mode 100644
index 000000000..2ebc140f1
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/compressor.cpp
@@ -0,0 +1,156 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <cmath>
+#include <span>
+#include <vector>
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/effect/compressor.h"
+#include "audio_core/renderer/effect/compressor.h"
+
+namespace AudioCore::AudioRenderer {
+
+static void SetCompressorEffectParameter(CompressorInfo::ParameterVersion2& params,
+ CompressorInfo::State& state) {
+ const auto ratio{1.0f / params.compressor_ratio};
+ auto makeup_gain{0.0f};
+ if (params.makeup_gain_enabled) {
+ makeup_gain = (params.threshold * 0.5f) * (ratio - 1.0f) - 3.0f;
+ }
+ state.makeup_gain = makeup_gain;
+ state.unk_18 = params.unk_28;
+
+ const auto a{(params.out_gain + makeup_gain) / 20.0f * 3.3219f};
+ const auto b{(a - std::trunc(a)) * 0.69315f};
+ const auto c{std::pow(2.0f, b)};
+
+ state.unk_0C = (1.0f - ratio) / 6.0f;
+ state.unk_14 = params.threshold + 1.5f;
+ state.unk_10 = params.threshold - 1.5f;
+ state.unk_20 = c;
+}
+
+static void InitializeCompressorEffect(CompressorInfo::ParameterVersion2& params,
+ CompressorInfo::State& state) {
+ std::memset(&state, 0, sizeof(CompressorInfo::State));
+
+ state.unk_00 = 0;
+ state.unk_04 = 1.0f;
+ state.unk_08 = 1.0f;
+
+ SetCompressorEffectParameter(params, state);
+}
+
+static void ApplyCompressorEffect(CompressorInfo::ParameterVersion2& params,
+ CompressorInfo::State& state, bool enabled,
+ std::vector<std::span<const s32>> input_buffers,
+ std::vector<std::span<s32>> output_buffers, u32 sample_count) {
+ if (enabled) {
+ auto state_00{state.unk_00};
+ auto state_04{state.unk_04};
+ auto state_08{state.unk_08};
+ auto state_18{state.unk_18};
+
+ for (u32 i = 0; i < sample_count; i++) {
+ auto a{0.0f};
+ for (s16 channel = 0; channel < params.channel_count; channel++) {
+ const auto input_sample{Common::FixedPoint<49, 15>(input_buffers[channel][i])};
+ a += (input_sample * input_sample).to_float();
+ }
+
+ state_00 += params.unk_24 * ((a / params.channel_count) - state.unk_00);
+
+ auto b{-100.0f};
+ auto c{0.0f};
+ if (state_00 >= 1.0e-10) {
+ b = std::log10(state_00) * 10.0f;
+ c = 1.0f;
+ }
+
+ if (b >= state.unk_10) {
+ const auto d{b >= state.unk_14
+ ? ((1.0f / params.compressor_ratio) - 1.0f) *
+ (b - params.threshold)
+ : (b - state.unk_10) * (b - state.unk_10) * -state.unk_0C};
+ const auto e{d / 20.0f * 3.3219f};
+ const auto f{(e - std::trunc(e)) * 0.69315f};
+ c = std::pow(2.0f, f);
+ }
+
+ state_18 = params.unk_28;
+ auto tmp{c};
+ if ((state_04 - c) <= 0.08f) {
+ state_18 = params.unk_2C;
+ if (((state_04 - c) >= -0.08f) && (std::abs(state_08 - c) >= 0.001f)) {
+ tmp = state_04;
+ }
+ }
+
+ state_04 = tmp;
+ state_08 += (c - state_08) * state_18;
+
+ for (s16 channel = 0; channel < params.channel_count; channel++) {
+ output_buffers[channel][i] = static_cast<s32>(
+ static_cast<f32>(input_buffers[channel][i]) * state_08 * state.unk_20);
+ }
+ }
+
+ state.unk_00 = state_00;
+ state.unk_04 = state_04;
+ state.unk_08 = state_08;
+ state.unk_18 = state_18;
+ } else {
+ for (s16 channel = 0; channel < params.channel_count; channel++) {
+ if (params.inputs[channel] != params.outputs[channel]) {
+ std::memcpy((char*)output_buffers[channel].data(),
+ (char*)input_buffers[channel].data(),
+ output_buffers[channel].size_bytes());
+ }
+ }
+ }
+}
+
+void CompressorCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor,
+ std::string& string) {
+ string += fmt::format("CompressorCommand\n\tenabled {} \n\tinputs: ", effect_enabled);
+ for (s16 i = 0; i < parameter.channel_count; i++) {
+ string += fmt::format("{:02X}, ", inputs[i]);
+ }
+ string += "\n\toutputs: ";
+ for (s16 i = 0; i < parameter.channel_count; i++) {
+ string += fmt::format("{:02X}, ", outputs[i]);
+ }
+ string += "\n";
+}
+
+void CompressorCommand::Process(const ADSP::CommandListProcessor& processor) {
+ std::vector<std::span<const s32>> input_buffers(parameter.channel_count);
+ std::vector<std::span<s32>> 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<CompressorInfo::State*>(state)};
+
+ if (effect_enabled) {
+ if (parameter.state == CompressorInfo::ParameterState::Updating) {
+ SetCompressorEffectParameter(parameter, *state_);
+ } else if (parameter.state == CompressorInfo::ParameterState::Initialized) {
+ InitializeCompressorEffect(parameter, *state_);
+ }
+ }
+
+ ApplyCompressorEffect(parameter, *state_, effect_enabled, input_buffers, output_buffers,
+ processor.sample_count);
+}
+
+bool CompressorCommand::Verify(const ADSP::CommandListProcessor& processor) {
+ return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/effect/compressor.h b/src/audio_core/renderer/command/effect/compressor.h
new file mode 100644
index 000000000..f8e96cb43
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/compressor.h
@@ -0,0 +1,60 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+#include <string>
+
+#include "audio_core/renderer/command/icommand.h"
+#include "audio_core/renderer/effect/compressor.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 CompressorCommand : 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<s16, MaxChannels> inputs;
+ /// Output mix buffer offsets for each channel
+ std::array<s16, MaxChannels> outputs;
+ /// Input parameters
+ CompressorInfo::ParameterVersion2 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/delay.cpp b/src/audio_core/renderer/command/effect/delay.cpp
new file mode 100644
index 000000000..a4e408d40
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/delay.cpp
@@ -0,0 +1,238 @@
+// 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.
+ *
+ * @tparam NumChannels - Number of channels to process. 1-6.
+ * @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 <size_t NumChannels>
+static void ApplyDelay(const DelayInfo::ParameterVersion1& params, DelayInfo::State& state,
+ std::vector<std::span<const s32>>& inputs,
+ std::vector<std::span<s32>>& outputs, const u32 sample_count) {
+ for (u32 sample_index = 0; sample_index < sample_count; sample_index++) {
+ std::array<Common::FixedPoint<50, 14>, NumChannels> input_samples{};
+ for (u32 channel = 0; channel < NumChannels; channel++) {
+ input_samples[channel] = inputs[channel][sample_index] * 64;
+ }
+
+ std::array<Common::FixedPoint<50, 14>, NumChannels> delay_samples{};
+ for (u32 channel = 0; channel < NumChannels; channel++) {
+ delay_samples[channel] = state.delay_lines[channel].Read();
+ }
+
+ // clang-format off
+ std::array<std::array<Common::FixedPoint<18, 14>, NumChannels>, NumChannels> matrix{};
+ if constexpr (NumChannels == 1) {
+ matrix = {{
+ {state.feedback_gain},
+ }};
+ } else if constexpr (NumChannels == 2) {
+ matrix = {{
+ {state.delay_feedback_gain, state.delay_feedback_cross_gain},
+ {state.delay_feedback_cross_gain, state.delay_feedback_gain},
+ }};
+ } else if constexpr (NumChannels == 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 (NumChannels == 6) {
+ matrix = {{
+ {state.delay_feedback_gain, 0.0f, state.delay_feedback_cross_gain, 0.0f, state.delay_feedback_cross_gain, 0.0f},
+ {0.0f, state.delay_feedback_gain, state.delay_feedback_cross_gain, 0.0f, 0.0f, state.delay_feedback_cross_gain},
+ {state.delay_feedback_cross_gain, state.delay_feedback_cross_gain, state.delay_feedback_gain, 0.0f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f, params.feedback_gain, 0.0f, 0.0f},
+ {state.delay_feedback_cross_gain, 0.0f, 0.0f, 0.0f, state.delay_feedback_gain, state.delay_feedback_cross_gain},
+ {0.0f, state.delay_feedback_cross_gain, 0.0f, 0.0f, state.delay_feedback_cross_gain, state.delay_feedback_gain},
+ }};
+ }
+ // clang-format on
+
+ std::array<Common::FixedPoint<50, 14>, NumChannels> gained_samples{};
+ for (u32 channel = 0; channel < NumChannels; channel++) {
+ Common::FixedPoint<50, 14> delay{};
+ for (u32 j = 0; j < NumChannels; j++) {
+ delay += delay_samples[j] * matrix[j][channel];
+ }
+ gained_samples[channel] = input_samples[channel] * params.in_gain + delay;
+ }
+
+ for (u32 channel = 0; channel < NumChannels; 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 < NumChannels; channel++) {
+ outputs[channel][sample_index] = (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<std::span<const s32>>& inputs,
+ std::vector<std::span<s32>>& 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<std::span<const s32>> input_buffers(parameter.channel_count);
+ std::vector<std::span<s32>> 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<DelayInfo::State*>(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 100644
index 000000000..b7a15ae6b
--- /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 <array>
+#include <string>
+
+#include "audio_core/renderer/command/icommand.h"
+#include "audio_core/renderer/effect/delay.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<s16, MaxChannels> inputs;
+ /// Output mix buffer offsets for each channel
+ std::array<s16, MaxChannels> 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 100644
index 000000000..c4bf3943a
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/i3dl2_reverb.cpp
@@ -0,0 +1,437 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <numbers>
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/effect/i3dl2_reverb.h"
+
+namespace AudioCore::AudioRenderer {
+
+constexpr std::array<f32, I3dl2ReverbInfo::MaxDelayLines> MinDelayLineTimes{
+ 5.0f,
+ 6.0f,
+ 13.0f,
+ 14.0f,
+};
+constexpr std::array<f32, I3dl2ReverbInfo::MaxDelayLines> MaxDelayLineTimes{
+ 45.7042007446f,
+ 82.7817001343f,
+ 149.938293457f,
+ 271.575805664f,
+};
+constexpr std::array<f32, I3dl2ReverbInfo::MaxDelayLines> Decay0MaxDelayLineTimes{17.0f, 13.0f,
+ 9.0f, 7.0f};
+constexpr std::array<f32, I3dl2ReverbInfo::MaxDelayLines> Decay1MaxDelayLineTimes{19.0f, 11.0f,
+ 10.0f, 6.0f};
+constexpr std::array<f32, I3dl2ReverbInfo::MaxDelayTaps> 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<f32, I3dl2ReverbInfo::MaxDelayTaps> 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<f32> / 180.0f);
+ };
+ const auto cos = [](f32 degrees) -> f32 {
+ return std::cos(degrees * std::numbers::pi_v<f32> / 180.0f);
+ };
+
+ Common::FixedPoint<50, 14> delay{static_cast<f32>(params.sample_rate) / 1000.0f};
+
+ state.dry_gain = params.dry_gain;
+ 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<f32>(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<f32>(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<f32>(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<f32>(params.sample_rate)) /
+ sin(((params.reference_HF * 0.5f) * 128.0f) / static_cast<f32>(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<f32>(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<std::span<const s32>> inputs,
+ std::span<std::span<s32>> 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 NumChannels - 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 <size_t NumChannels>
+static void ApplyI3dl2ReverbEffect(I3dl2ReverbInfo::State& state,
+ std::span<std::span<const s32>> inputs,
+ std::span<std::span<s32>> outputs, const u32 sample_count) {
+ constexpr std::array<u8, I3dl2ReverbInfo::MaxDelayTaps> OutTapIndexes1Ch{
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ };
+ constexpr std::array<u8, I3dl2ReverbInfo::MaxDelayTaps> OutTapIndexes2Ch{
+ 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1,
+ };
+ constexpr std::array<u8, I3dl2ReverbInfo::MaxDelayTaps> OutTapIndexes4Ch{
+ 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 3, 3, 3,
+ };
+ constexpr std::array<u8, I3dl2ReverbInfo::MaxDelayTaps> OutTapIndexes6Ch{
+ 2, 0, 0, 1, 1, 1, 1, 4, 4, 4, 1, 1, 1, 0, 0, 0, 0, 5, 5, 5,
+ };
+
+ std::span<const u8> tap_indexes{};
+ if constexpr (NumChannels == 1) {
+ tap_indexes = OutTapIndexes1Ch;
+ } else if constexpr (NumChannels == 2) {
+ tap_indexes = OutTapIndexes2Ch;
+ } else if constexpr (NumChannels == 4) {
+ tap_indexes = OutTapIndexes4Ch;
+ } else if constexpr (NumChannels == 6) {
+ tap_indexes = OutTapIndexes6Ch;
+ }
+
+ for (u32 sample_index = 0; sample_index < sample_count; sample_index++) {
+ Common::FixedPoint<50, 14> early_to_late_tap{
+ state.early_delay_line.TapOut(state.early_to_late_taps)};
+ std::array<Common::FixedPoint<50, 14>, NumChannels> 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 (NumChannels == 6) {
+ output_samples[static_cast<u32>(Channels::LFE)] +=
+ 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 < NumChannels; channel++) {
+ current_sample += inputs[channel][sample_index];
+ }
+
+ 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 < NumChannels; channel++) {
+ output_samples[channel] *= state.early_gain;
+ }
+
+ std::array<Common::FixedPoint<50, 14>, 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<Common::FixedPoint<50, 14>, 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<Common::FixedPoint<50, 14>, 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]);
+ }
+
+ if constexpr (NumChannels == 6) {
+ const std::array<Common::FixedPoint<50, 14>, MaxChannels> allpass_outputs{
+ allpass_samples[0], allpass_samples[1], allpass_samples[2] - allpass_samples[3],
+ allpass_samples[3], allpass_samples[2], allpass_samples[3],
+ };
+
+ for (u32 channel = 0; channel < NumChannels; channel++) {
+ Common::FixedPoint<50, 14> allpass{};
+
+ if (channel == static_cast<u32>(Channels::Center)) {
+ allpass = state.center_delay_line.Tick(allpass_outputs[channel] * 0.5f);
+ } else {
+ allpass = allpass_outputs[channel];
+ }
+
+ auto out_sample{output_samples[channel] + allpass +
+ state.dry_gain * static_cast<f32>(inputs[channel][sample_index])};
+
+ outputs[channel][sample_index] =
+ static_cast<s32>(std::clamp(out_sample.to_float(), -8388600.0f, 8388600.0f));
+ }
+ } else {
+ for (u32 channel = 0; channel < NumChannels; channel++) {
+ auto out_sample{output_samples[channel] + allpass_samples[channel] +
+ state.dry_gain * static_cast<f32>(inputs[channel][sample_index])};
+ outputs[channel][sample_index] =
+ static_cast<s32>(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<std::span<const s32>> inputs,
+ std::span<std::span<s32>> 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<std::span<const s32>> input_buffers(parameter.channel_count);
+ std::vector<std::span<s32>> 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<I3dl2ReverbInfo::State*>(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 100644
index 000000000..243877056
--- /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 <array>
+#include <string>
+
+#include "audio_core/renderer/command/icommand.h"
+#include "audio_core/renderer/effect/i3dl2.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<s16, MaxChannels> inputs;
+ /// Output mix buffer offsets for each channel
+ std::array<s16, MaxChannels> 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 100644
index 000000000..e8fb0e2fc
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/light_limiter.cpp
@@ -0,0 +1,222 @@
+// 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<std::span<const s32>>& inputs,
+ std::vector<std::span<s32>>& outputs, const u32 sample_count,
+ LightLimiterInfo::StatisticsInternal* statistics) {
+ constexpr s64 min{std::numeric_limits<s32>::min()};
+ constexpr s64 max{std::numeric_limits<s32>::max()};
+
+ const auto recip_estimate = [](f64 a) -> f64 {
+ s32 q, s;
+ f64 r;
+ q = (s32)(a * 512.0); /* a in units of 1/512 rounded down */
+ r = 1.0 / (((f64)q + 0.5) / 512.0); /* reciprocal r */
+ s = (s32)(256.0 * r + 0.5); /* r in units of 1/256 rounded to nearest */
+ return ((f64)s / 256.0);
+ };
+
+ 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]) /
+ Common::FixedPoint<49, 15>::one) *
+ 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();
+
+ // Reciprocal estimate
+ auto new_average_sample{Common::FixedPoint<49, 15>(
+ recip_estimate(state.samples_average[channel].to_double()))};
+ if (params.processing_mode != LightLimiterInfo::ProcessingMode::Mode1) {
+ // Two Newton-Raphson steps
+ auto temp{2.0 - (state.samples_average[channel] * new_average_sample)};
+ new_average_sample = 2.0 - (state.samples_average[channel] * temp);
+ }
+
+ auto above_threshold{state.samples_average[channel] > params.threshold};
+ auto attenuation{above_threshold ? params.threshold * new_average_sample : 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<s32>(
+ std::clamp((lookahead_sample * state.compression_gain[channel] *
+ params.output_gain * Common::FixedPoint<49, 15>::one)
+ .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<std::span<const s32>> input_buffers(parameter.channel_count);
+ std::vector<std::span<s32>> 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<LightLimiterInfo::State*>(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) {
+ std::vector<std::span<const s32>> input_buffers(parameter.channel_count);
+ std::vector<std::span<s32>> 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<LightLimiterInfo::State*>(state)};
+
+ 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<LightLimiterInfo::StatisticsInternal*>(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 100644
index 000000000..5d98272c7
--- /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 <array>
+#include <string>
+
+#include "audio_core/renderer/command/icommand.h"
+#include "audio_core/renderer/effect/light_limiter.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<s16, MaxChannels> inputs;
+ /// Output mix buffer offsets for each channel
+ std::array<s16, MaxChannels> 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<s16, MaxChannels> inputs;
+ /// Output mix buffer offsets for each channel
+ std::array<s16, MaxChannels> 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 100644
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<VoiceState::BiquadFilterState*>(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 100644
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 <array>
+#include <string>
+
+#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<VoiceInfo::BiquadFilterParameter, MaxBiquadFilters> biquads;
+ /// Biquad states, updated each call
+ std::array<CpuAddr, MaxBiquadFilters> states;
+ /// If each biquad needs initialisation
+ std::array<bool, MaxBiquadFilters> 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 100644
index 000000000..fe2b1eb43
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/reverb.cpp
@@ -0,0 +1,440 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <numbers>
+#include <ranges>
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/effect/reverb.h"
+
+namespace AudioCore::AudioRenderer {
+
+constexpr std::array<f32, ReverbInfo::MaxDelayLines> FdnMaxDelayLineTimes = {
+ 53.9532470703125f,
+ 79.19256591796875f,
+ 116.23876953125f,
+ 170.61529541015625f,
+};
+
+constexpr std::array<f32, ReverbInfo::MaxDelayLines> DecayMaxDelayLineTimes = {
+ 7.0f,
+ 9.0f,
+ 13.0f,
+ 17.0f,
+};
+
+constexpr std::array<std::array<f32, ReverbInfo::MaxDelayTaps + 1>, 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<std::array<f32, ReverbInfo::MaxDelayTaps>, 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<std::array<f32, ReverbInfo::MaxDelayLines>, 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<std::array<f32, ReverbInfo::MaxDelayLines>, 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<f32> / 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<std::span<const s32>> inputs,
+ std::span<std::span<s32>> 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 NumChannels - 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 <size_t NumChannels>
+static void ApplyReverbEffect(const ReverbInfo::ParameterVersion2& params, ReverbInfo::State& state,
+ std::vector<std::span<const s32>>& inputs,
+ std::vector<std::span<s32>>& outputs, const u32 sample_count) {
+ constexpr std::array<u8, ReverbInfo::MaxDelayTaps> OutTapIndexes1Ch{
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ };
+ constexpr std::array<u8, ReverbInfo::MaxDelayTaps> OutTapIndexes2Ch{
+ 0, 0, 1, 1, 0, 1, 0, 0, 1, 1,
+ };
+ constexpr std::array<u8, ReverbInfo::MaxDelayTaps> OutTapIndexes4Ch{
+ 0, 0, 1, 1, 0, 1, 2, 2, 3, 3,
+ };
+ constexpr std::array<u8, ReverbInfo::MaxDelayTaps> OutTapIndexes6Ch{
+ 0, 0, 1, 1, 2, 2, 4, 4, 5, 5,
+ };
+
+ std::span<const u8> tap_indexes{};
+ if constexpr (NumChannels == 1) {
+ tap_indexes = OutTapIndexes1Ch;
+ } else if constexpr (NumChannels == 2) {
+ tap_indexes = OutTapIndexes2Ch;
+ } else if constexpr (NumChannels == 4) {
+ tap_indexes = OutTapIndexes4Ch;
+ } else if constexpr (NumChannels == 6) {
+ tap_indexes = OutTapIndexes6Ch;
+ }
+
+ for (u32 sample_index = 0; sample_index < sample_count; sample_index++) {
+ std::array<Common::FixedPoint<50, 14>, NumChannels> 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 (NumChannels == 6) {
+ output_samples[static_cast<u32>(Channels::LFE)] += sample;
+ }
+ }
+
+ if constexpr (NumChannels == 6) {
+ output_samples[static_cast<u32>(Channels::LFE)] *= 0.2f;
+ }
+
+ Common::FixedPoint<50, 14> input_sample{};
+ for (u32 channel = 0; channel < NumChannels; 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<Common::FixedPoint<50, 14>, 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<Common::FixedPoint<50, 14>, ReverbInfo::MaxDelayLines> allpass_samples{};
+ for (u32 i = 0; i < ReverbInfo::MaxDelayLines; i++) {
+ allpass_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)};
+
+ if constexpr (NumChannels == 6) {
+ const std::array<Common::FixedPoint<50, 14>, MaxChannels> allpass_outputs{
+ allpass_samples[0], allpass_samples[1], allpass_samples[2] - allpass_samples[3],
+ allpass_samples[3], allpass_samples[2], allpass_samples[3],
+ };
+
+ for (u32 channel = 0; channel < NumChannels; channel++) {
+ auto in_sample{inputs[channel][sample_index] * dry_gain};
+
+ Common::FixedPoint<50, 14> allpass{};
+ if (channel == static_cast<u32>(Channels::Center)) {
+ allpass = state.center_delay_line.Tick(allpass_outputs[channel] * 0.5f);
+ } else {
+ allpass = allpass_outputs[channel];
+ }
+
+ auto out_sample{((output_samples[channel] + allpass) * wet_gain) / 64};
+ outputs[channel][sample_index] = (in_sample + out_sample).to_int();
+ }
+ } else {
+ for (u32 channel = 0; channel < NumChannels; channel++) {
+ auto in_sample{inputs[channel][sample_index] * dry_gain};
+ auto out_sample{((output_samples[channel] + allpass_samples[channel]) * wet_gain) /
+ 64};
+ outputs[channel][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<std::span<const s32>>& inputs,
+ std::vector<std::span<s32>>& 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<std::span<const s32>> input_buffers(parameter.channel_count);
+ std::vector<std::span<s32>> 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<ReverbInfo::State*>(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 100644
index 000000000..328756150
--- /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 <array>
+#include <string>
+
+#include "audio_core/renderer/command/icommand.h"
+#include "audio_core/renderer/effect/reverb.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<s16, MaxChannels> inputs;
+ /// Output mix buffer offsets for each channel
+ std::array<s16, MaxChannels> 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