mirror of
https://git.suyu.dev/suyu/suyu.git
synced 2024-11-26 05:16:24 -05:00
Merge pull request #1797 from MerryMage/audio-mixer
DSP/HLE: Implement mixer processing
This commit is contained in:
commit
af258584d9
5 changed files with 317 additions and 10 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,57 @@ 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;
|
||||||
|
|
||||||
|
static void OutputCurrentFrame(const StereoFrame16& frame) {
|
||||||
|
time_stretcher.AddSamples(&frame[0][0], frame.size());
|
||||||
|
sink->EnqueueSamples(time_stretcher.Process(sink->SamplesInQueue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public Interface
|
||||||
|
|
||||||
void Init() {
|
void Init() {
|
||||||
DSP::HLE::ResetPipes();
|
DSP::HLE::ResetPipes();
|
||||||
|
|
||||||
|
@ -58,6 +102,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 +121,12 @@ 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++) {
|
OutputCurrentFrame(current_frame);
|
||||||
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…
Reference in a new issue