From 6542c606023164d5c9c028bb2a2409d91b3ecb9e Mon Sep 17 00:00:00 2001
From: MerryMage <MerryMage@users.noreply.github.com>
Date: Wed, 27 Apr 2016 07:22:39 +0100
Subject: [PATCH] DSP/HLE: Implement mixer processing

---
 src/audio_core/CMakeLists.txt |   2 +
 src/audio_core/hle/dsp.cpp    |  54 +++++++--
 src/audio_core/hle/dsp.h      |   2 +-
 src/audio_core/hle/mixers.cpp | 201 ++++++++++++++++++++++++++++++++++
 src/audio_core/hle/mixers.h   |  63 +++++++++++
 5 files changed, 311 insertions(+), 11 deletions(-)
 create mode 100644 src/audio_core/hle/mixers.cpp
 create mode 100644 src/audio_core/hle/mixers.h

diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt
index eba0a56978..a72a907ef3 100644
--- a/src/audio_core/CMakeLists.txt
+++ b/src/audio_core/CMakeLists.txt
@@ -3,6 +3,7 @@ set(SRCS
             codec.cpp
             hle/dsp.cpp
             hle/filter.cpp
+            hle/mixers.cpp
             hle/pipe.cpp
             hle/source.cpp
             interpolate.cpp
@@ -16,6 +17,7 @@ set(HEADERS
             hle/common.h
             hle/dsp.h
             hle/filter.h
+            hle/mixers.h
             hle/pipe.h
             hle/source.h
             interpolate.h
diff --git a/src/audio_core/hle/dsp.cpp b/src/audio_core/hle/dsp.cpp
index 5113ad8cab..1c00ed4727 100644
--- a/src/audio_core/hle/dsp.cpp
+++ b/src/audio_core/hle/dsp.cpp
@@ -6,6 +6,7 @@
 #include <memory>
 
 #include "audio_core/hle/dsp.h"
+#include "audio_core/hle/mixers.h"
 #include "audio_core/hle/pipe.h"
 #include "audio_core/hle/source.h"
 #include "audio_core/sink.h"
@@ -14,6 +15,8 @@
 namespace DSP {
 namespace HLE {
 
+// Region management
+
 std::array<SharedMemory, 2> g_regions;
 
 static size_t CurrentRegionIndex() {
@@ -41,16 +44,52 @@ static SharedMemory& WriteRegion() {
     return g_regions[1 - CurrentRegionIndex()];
 }
 
+// Audio processing and mixing
+
 static std::array<Source, num_sources> sources = {
     Source(0), Source(1), Source(2), Source(3), Source(4), Source(5),
     Source(6), Source(7), Source(8), Source(9), Source(10), Source(11),
     Source(12), Source(13), Source(14), Source(15), Source(16), Source(17),
     Source(18), Source(19), Source(20), Source(21), Source(22), Source(23)
 };
+static Mixers mixers;
+
+static StereoFrame16 GenerateCurrentFrame() {
+    SharedMemory& read = ReadRegion();
+    SharedMemory& write = WriteRegion();
+
+    std::array<QuadFrame32, 3> intermediate_mixes = {};
+
+    // Generate intermediate mixes
+    for (size_t i = 0; i < num_sources; i++) {
+        write.source_statuses.status[i] = sources[i].Tick(read.source_configurations.config[i], read.adpcm_coefficients.coeff[i]);
+        for (size_t mix = 0; mix < 3; mix++) {
+            sources[i].MixInto(intermediate_mixes[mix], mix);
+        }
+    }
+
+    // Generate final mix
+    write.dsp_status = mixers.Tick(read.dsp_configuration, read.intermediate_mix_samples, write.intermediate_mix_samples, intermediate_mixes);
+
+    StereoFrame16 output_frame = mixers.GetOutput();
+
+    // Write current output frame to the shared memory region
+    for (size_t samplei = 0; samplei < output_frame.size(); samplei++) {
+        for (size_t channeli = 0; channeli < output_frame[0].size(); channeli++) {
+            write.final_samples.pcm16[samplei][channeli] = s16_le(output_frame[samplei][channeli]);
+        }
+    }
+
+    return output_frame;
+}
+
+// Audio output
 
 static std::unique_ptr<AudioCore::Sink> sink;
 static AudioCore::TimeStretcher time_stretcher;
 
+// Public Interface
+
 void Init() {
     DSP::HLE::ResetPipes();
 
@@ -58,6 +97,8 @@ void Init() {
         source.Reset();
     }
 
+    mixers.Reset();
+
     time_stretcher.Reset();
     if (sink) {
         time_stretcher.SetOutputSampleRate(sink->GetNativeSampleRate());
@@ -75,17 +116,10 @@ void Shutdown() {
 }
 
 bool Tick() {
-    SharedMemory& read = ReadRegion();
-    SharedMemory& write = WriteRegion();
+    StereoFrame16 current_frame = {};
 
-    std::array<QuadFrame32, 3> intermediate_mixes = {};
-
-    for (size_t i = 0; i < num_sources; i++) {
-        write.source_statuses.status[i] = sources[i].Tick(read.source_configurations.config[i], read.adpcm_coefficients.coeff[i]);
-        for (size_t mix = 0; mix < 3; mix++) {
-            sources[i].MixInto(intermediate_mixes[mix], mix);
-        }
-    }
+    // TODO: Check dsp::DSP semaphore (which indicates emulated application has finished writing to shared memory region)
+    current_frame = GenerateCurrentFrame();
 
     return true;
 }
diff --git a/src/audio_core/hle/dsp.h b/src/audio_core/hle/dsp.h
index f6e53f68f0..9275cd7dee 100644
--- a/src/audio_core/hle/dsp.h
+++ b/src/audio_core/hle/dsp.h
@@ -428,7 +428,7 @@ ASSERT_DSP_STRUCT(DspStatus, 32);
 /// Final mixed output in PCM16 stereo format, what you hear out of the speakers.
 /// When the application writes to this region it has no effect.
 struct FinalMixSamples {
-    s16_le pcm16[2 * samples_per_frame];
+    s16_le pcm16[samples_per_frame][2];
 };
 ASSERT_DSP_STRUCT(FinalMixSamples, 640);
 
diff --git a/src/audio_core/hle/mixers.cpp b/src/audio_core/hle/mixers.cpp
new file mode 100644
index 0000000000..18335f7f07
--- /dev/null
+++ b/src/audio_core/hle/mixers.cpp
@@ -0,0 +1,201 @@
+// Copyright 2016 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <cstddef>
+
+#include "audio_core/hle/common.h"
+#include "audio_core/hle/dsp.h"
+#include "audio_core/hle/mixers.h"
+
+#include "common/assert.h"
+#include "common/logging/log.h"
+#include "common/math_util.h"
+
+namespace DSP {
+namespace HLE {
+
+void Mixers::Reset() {
+    current_frame.fill({});
+    state = {};
+}
+
+DspStatus Mixers::Tick(DspConfiguration& config,
+        const IntermediateMixSamples& read_samples,
+        IntermediateMixSamples& write_samples,
+        const std::array<QuadFrame32, 3>& input)
+{
+    ParseConfig(config);
+
+    AuxReturn(read_samples);
+    AuxSend(write_samples, input);
+
+    MixCurrentFrame();
+
+    return GetCurrentStatus();
+}
+
+void Mixers::ParseConfig(DspConfiguration& config) {
+    if (!config.dirty_raw) {
+        return;
+    }
+
+    if (config.mixer1_enabled_dirty) {
+        config.mixer1_enabled_dirty.Assign(0);
+        state.mixer1_enabled = config.mixer1_enabled != 0;
+        LOG_TRACE(Audio_DSP, "mixers mixer1_enabled = %hu", config.mixer1_enabled);
+    }
+
+    if (config.mixer2_enabled_dirty) {
+        config.mixer2_enabled_dirty.Assign(0);
+        state.mixer2_enabled = config.mixer2_enabled != 0;
+        LOG_TRACE(Audio_DSP, "mixers mixer2_enabled = %hu", config.mixer2_enabled);
+    }
+
+    if (config.volume_0_dirty) {
+        config.volume_0_dirty.Assign(0);
+        state.intermediate_mixer_volume[0] = config.volume[0];
+        LOG_TRACE(Audio_DSP, "mixers volume[0] = %f", config.volume[0]);
+    }
+
+    if (config.volume_1_dirty) {
+        config.volume_1_dirty.Assign(0);
+        state.intermediate_mixer_volume[1] = config.volume[1];
+        LOG_TRACE(Audio_DSP, "mixers volume[1] = %f", config.volume[1]);
+    }
+
+    if (config.volume_2_dirty) {
+        config.volume_2_dirty.Assign(0);
+        state.intermediate_mixer_volume[2] = config.volume[2];
+        LOG_TRACE(Audio_DSP, "mixers volume[2] = %f", config.volume[2]);
+    }
+
+    if (config.output_format_dirty) {
+        config.output_format_dirty.Assign(0);
+        state.output_format = config.output_format;
+        LOG_TRACE(Audio_DSP, "mixers output_format = %zu", static_cast<size_t>(config.output_format));
+    }
+
+    if (config.headphones_connected_dirty) {
+        config.headphones_connected_dirty.Assign(0);
+        // Do nothing.
+        // (Note: Whether headphones are connected does affect coefficients used for surround sound.)
+        LOG_TRACE(Audio_DSP, "mixers headphones_connected=%hu", config.headphones_connected);
+    }
+
+    if (config.dirty_raw) {
+        LOG_DEBUG(Audio_DSP, "mixers remaining_dirty=%x", config.dirty_raw);
+    }
+
+    config.dirty_raw = 0;
+}
+
+static s16 ClampToS16(s32 value) {
+    return static_cast<s16>(MathUtil::Clamp(value, -32768, 32767));
+}
+
+static std::array<s16, 2> AddAndClampToS16(const std::array<s16, 2>& a, const std::array<s16, 2>& b) {
+    return {
+        ClampToS16(static_cast<s32>(a[0]) + static_cast<s32>(b[0])),
+        ClampToS16(static_cast<s32>(a[1]) + static_cast<s32>(b[1]))
+    };
+}
+
+void Mixers::DownmixAndMixIntoCurrentFrame(float gain, const QuadFrame32& samples) {
+    // TODO(merry): Limiter. (Currently we're performing final mixing assuming a disabled limiter.)
+
+    switch (state.output_format) {
+    case OutputFormat::Mono:
+        std::transform(current_frame.begin(), current_frame.end(), samples.begin(), current_frame.begin(),
+            [gain](const std::array<s16, 2>& accumulator, const std::array<s32, 4>& sample) -> std::array<s16, 2> {
+                // Downmix to mono
+                s16 mono = ClampToS16(static_cast<s32>((gain * sample[0] + gain * sample[1] + gain * sample[2] + gain * sample[3]) / 2));
+                // Mix into current frame
+                return AddAndClampToS16(accumulator, { mono, mono });
+            });
+        return;
+
+    case OutputFormat::Surround:
+        // TODO(merry): Implement surround sound.
+        // fallthrough
+
+    case OutputFormat::Stereo:
+        std::transform(current_frame.begin(), current_frame.end(), samples.begin(), current_frame.begin(),
+            [gain](const std::array<s16, 2>& accumulator, const std::array<s32, 4>& sample) -> std::array<s16, 2> {
+                // Downmix to stereo
+                s16 left = ClampToS16(static_cast<s32>(gain * sample[0] + gain * sample[2]));
+                s16 right = ClampToS16(static_cast<s32>(gain * sample[1] + gain * sample[3]));
+                // Mix into current frame
+                return AddAndClampToS16(accumulator, { left, right });
+            });
+        return;
+    }
+
+    UNREACHABLE_MSG("Invalid output_format %zu", static_cast<size_t>(state.output_format));
+}
+
+void Mixers::AuxReturn(const IntermediateMixSamples& read_samples) {
+    // NOTE: read_samples.mix{1,2}.pcm32 annoyingly have their dimensions in reverse order to QuadFrame32.
+
+    if (state.mixer1_enabled) {
+        for (size_t sample = 0; sample < samples_per_frame; sample++) {
+            for (size_t channel = 0; channel < 4; channel++) {
+                state.intermediate_mix_buffer[1][sample][channel] = read_samples.mix1.pcm32[channel][sample];
+            }
+        }
+    }
+
+    if (state.mixer2_enabled) {
+        for (size_t sample = 0; sample < samples_per_frame; sample++) {
+            for (size_t channel = 0; channel < 4; channel++) {
+                state.intermediate_mix_buffer[2][sample][channel] = read_samples.mix2.pcm32[channel][sample];
+            }
+        }
+    }
+}
+
+void Mixers::AuxSend(IntermediateMixSamples& write_samples, const std::array<QuadFrame32, 3>& input) {
+    // NOTE: read_samples.mix{1,2}.pcm32 annoyingly have their dimensions in reverse order to QuadFrame32.
+
+    state.intermediate_mix_buffer[0] = input[0];
+
+    if (state.mixer1_enabled) {
+        for (size_t sample = 0; sample < samples_per_frame; sample++) {
+            for (size_t channel = 0; channel < 4; channel++) {
+                write_samples.mix1.pcm32[channel][sample] = input[1][sample][channel];
+            }
+        }
+    } else {
+        state.intermediate_mix_buffer[1] = input[1];
+    }
+
+    if (state.mixer2_enabled) {
+        for (size_t sample = 0; sample < samples_per_frame; sample++) {
+            for (size_t channel = 0; channel < 4; channel++) {
+                write_samples.mix2.pcm32[channel][sample] = input[2][sample][channel];
+            }
+        }
+    } else {
+        state.intermediate_mix_buffer[2] = input[2];
+    }
+}
+
+void Mixers::MixCurrentFrame() {
+    current_frame.fill({});
+
+    for (size_t mix = 0; mix < 3; mix++) {
+        DownmixAndMixIntoCurrentFrame(state.intermediate_mixer_volume[mix], state.intermediate_mix_buffer[mix]);
+    }
+
+    // TODO(merry): Compressor. (We currently assume a disabled compressor.)
+}
+
+DspStatus Mixers::GetCurrentStatus() const {
+    DspStatus status;
+    status.unknown = 0;
+    status.dropped_frames = 0;
+    return status;
+}
+
+} // namespace HLE
+} // namespace DSP
diff --git a/src/audio_core/hle/mixers.h b/src/audio_core/hle/mixers.h
new file mode 100644
index 0000000000..b52952eb50
--- /dev/null
+++ b/src/audio_core/hle/mixers.h
@@ -0,0 +1,63 @@
+// Copyright 2016 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <array>
+
+#include "audio_core/hle/common.h"
+#include "audio_core/hle/dsp.h"
+
+namespace DSP {
+namespace HLE {
+
+class Mixers final {
+public:
+    Mixers() {
+        Reset();
+    }
+
+    void Reset();
+
+    DspStatus Tick(DspConfiguration& config,
+                   const IntermediateMixSamples& read_samples,
+                   IntermediateMixSamples& write_samples,
+                   const std::array<QuadFrame32, 3>& input);
+
+    StereoFrame16 GetOutput() const {
+        return current_frame;
+    }
+
+private:
+    StereoFrame16 current_frame = {};
+
+    using OutputFormat = DspConfiguration::OutputFormat;
+
+    struct {
+        std::array<float, 3> intermediate_mixer_volume = {};
+
+        bool mixer1_enabled = false;
+        bool mixer2_enabled = false;
+        std::array<QuadFrame32, 3> intermediate_mix_buffer = {};
+
+        OutputFormat output_format = OutputFormat::Stereo;
+
+    } state;
+
+    /// INTERNAL: Update our internal state based on the current config.
+    void ParseConfig(DspConfiguration& config);
+    /// INTERNAL: Read samples from shared memory that have been modified by the ARM11.
+    void AuxReturn(const IntermediateMixSamples& read_samples);
+    /// INTERNAL: Write samples to shared memory for the ARM11 to modify.
+    void AuxSend(IntermediateMixSamples& write_samples, const std::array<QuadFrame32, 3>& input);
+    /// INTERNAL: Mix current_frame.
+    void MixCurrentFrame();
+    /// INTERNAL: Downmix from quadraphonic to stereo based on status.output_format and accumulate into current_frame.
+    void DownmixAndMixIntoCurrentFrame(float gain, const QuadFrame32& samples);
+    /// INTERNAL: Generate DspStatus based on internal state.
+    DspStatus GetCurrentStatus() const;
+};
+
+} // namespace HLE
+} // namespace DSP
-- 
GitLab