mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-30 21:30:04 +00:00 
			
		
		
		
	DSP/HLE: Implement mixer processing
This commit is contained in:
		
							parent
							
								
									6f6af6928f
								
							
						
					
					
						commit
						6542c60602
					
				
					 5 changed files with 311 additions and 11 deletions
				
			
		|  | @ -3,6 +3,7 @@ set(SRCS | ||||||
|             codec.cpp |             codec.cpp | ||||||
|             hle/dsp.cpp |             hle/dsp.cpp | ||||||
|             hle/filter.cpp |             hle/filter.cpp | ||||||
|  |             hle/mixers.cpp | ||||||
|             hle/pipe.cpp |             hle/pipe.cpp | ||||||
|             hle/source.cpp |             hle/source.cpp | ||||||
|             interpolate.cpp |             interpolate.cpp | ||||||
|  | @ -16,6 +17,7 @@ set(HEADERS | ||||||
|             hle/common.h |             hle/common.h | ||||||
|             hle/dsp.h |             hle/dsp.h | ||||||
|             hle/filter.h |             hle/filter.h | ||||||
|  |             hle/mixers.h | ||||||
|             hle/pipe.h |             hle/pipe.h | ||||||
|             hle/source.h |             hle/source.h | ||||||
|             interpolate.h |             interpolate.h | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ | ||||||
| #include <memory> | #include <memory> | ||||||
| 
 | 
 | ||||||
| #include "audio_core/hle/dsp.h" | #include "audio_core/hle/dsp.h" | ||||||
|  | #include "audio_core/hle/mixers.h" | ||||||
| #include "audio_core/hle/pipe.h" | #include "audio_core/hle/pipe.h" | ||||||
| #include "audio_core/hle/source.h" | #include "audio_core/hle/source.h" | ||||||
| #include "audio_core/sink.h" | #include "audio_core/sink.h" | ||||||
|  | @ -14,6 +15,8 @@ | ||||||
| namespace DSP { | namespace DSP { | ||||||
| namespace HLE { | namespace HLE { | ||||||
| 
 | 
 | ||||||
|  | // Region management
 | ||||||
|  | 
 | ||||||
| std::array<SharedMemory, 2> g_regions; | std::array<SharedMemory, 2> g_regions; | ||||||
| 
 | 
 | ||||||
| static size_t CurrentRegionIndex() { | static size_t CurrentRegionIndex() { | ||||||
|  | @ -41,16 +44,52 @@ static SharedMemory& WriteRegion() { | ||||||
|     return g_regions[1 - CurrentRegionIndex()]; |     return g_regions[1 - CurrentRegionIndex()]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // Audio processing and mixing
 | ||||||
|  | 
 | ||||||
| static std::array<Source, num_sources> sources = { | static std::array<Source, num_sources> sources = { | ||||||
|     Source(0), Source(1), Source(2), Source(3), Source(4), Source(5), |     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(6), Source(7), Source(8), Source(9), Source(10), Source(11), | ||||||
|     Source(12), Source(13), Source(14), Source(15), Source(16), Source(17), |     Source(12), Source(13), Source(14), Source(15), Source(16), Source(17), | ||||||
|     Source(18), Source(19), Source(20), Source(21), Source(22), Source(23) |     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 std::unique_ptr<AudioCore::Sink> sink; | ||||||
| static AudioCore::TimeStretcher time_stretcher; | static AudioCore::TimeStretcher time_stretcher; | ||||||
| 
 | 
 | ||||||
|  | // Public Interface
 | ||||||
|  | 
 | ||||||
| void Init() { | void Init() { | ||||||
|     DSP::HLE::ResetPipes(); |     DSP::HLE::ResetPipes(); | ||||||
| 
 | 
 | ||||||
|  | @ -58,6 +97,8 @@ void Init() { | ||||||
|         source.Reset(); |         source.Reset(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     mixers.Reset(); | ||||||
|  | 
 | ||||||
|     time_stretcher.Reset(); |     time_stretcher.Reset(); | ||||||
|     if (sink) { |     if (sink) { | ||||||
|         time_stretcher.SetOutputSampleRate(sink->GetNativeSampleRate()); |         time_stretcher.SetOutputSampleRate(sink->GetNativeSampleRate()); | ||||||
|  | @ -75,17 +116,10 @@ void Shutdown() { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| bool Tick() { | bool Tick() { | ||||||
|     SharedMemory& read = ReadRegion(); |     StereoFrame16 current_frame = {}; | ||||||
|     SharedMemory& write = WriteRegion(); |  | ||||||
| 
 | 
 | ||||||
|     std::array<QuadFrame32, 3> intermediate_mixes = {}; |     // TODO: Check dsp::DSP semaphore (which indicates emulated application has finished writing to shared memory region)
 | ||||||
| 
 |     current_frame = GenerateCurrentFrame(); | ||||||
|     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); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     return true; |     return true; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -428,7 +428,7 @@ ASSERT_DSP_STRUCT(DspStatus, 32); | ||||||
| /// Final mixed output in PCM16 stereo format, what you hear out of the speakers.
 | /// 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.
 | /// When the application writes to this region it has no effect.
 | ||||||
| struct FinalMixSamples { | struct FinalMixSamples { | ||||||
|     s16_le pcm16[2 * samples_per_frame]; |     s16_le pcm16[samples_per_frame][2]; | ||||||
| }; | }; | ||||||
| ASSERT_DSP_STRUCT(FinalMixSamples, 640); | ASSERT_DSP_STRUCT(FinalMixSamples, 640); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										201
									
								
								src/audio_core/hle/mixers.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								src/audio_core/hle/mixers.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -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
 | ||||||
							
								
								
									
										63
									
								
								src/audio_core/hle/mixers.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/audio_core/hle/mixers.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -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
 | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue