mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-09-08 18:42:27 -04:00
Implement surround panner
This commit is contained in:
+214
-22
@@ -8,6 +8,7 @@
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <span>
|
||||
#include <numbers>
|
||||
|
||||
#include "Adpcm.hpp"
|
||||
#include "dusk/audio/DuskAudioSystem.h"
|
||||
@@ -142,6 +143,12 @@ static void MixSubframe(DspSubframe& dst, const DspSubframe& src) {
|
||||
}
|
||||
}
|
||||
|
||||
static void MixOutputSubframe(OutputSubframe& dst, const OutputSubframe& src) {
|
||||
for (int i = 0; i < OutChannelCount; i++) {
|
||||
MixSubframe(dst.channels[i], src.channels[i]);
|
||||
}
|
||||
}
|
||||
|
||||
enum class OscType : u16 {
|
||||
SQUARE_WAVE_PW_50 = 0,
|
||||
SAW_WAVE = 1,
|
||||
@@ -470,6 +477,8 @@ struct VolumeValue {
|
||||
f32 Init;
|
||||
};
|
||||
|
||||
using VolumeArray = std::array<VolumeValue, OutputSubframe::NUM_CHANNELS>;
|
||||
|
||||
static void ApplyVolume(
|
||||
std::span<f32> dst,
|
||||
std::span<const f32> src,
|
||||
@@ -489,23 +498,135 @@ static void ApplyVolume(
|
||||
}
|
||||
}
|
||||
|
||||
struct SpeakerPlacement {
|
||||
OutputChannel channel;
|
||||
f32 angle;
|
||||
};
|
||||
|
||||
struct SpeakerPair {
|
||||
OutputChannel first;
|
||||
OutputChannel second;
|
||||
f32 lo;
|
||||
f32 span;
|
||||
};
|
||||
|
||||
template <std::size_t N>
|
||||
constexpr auto BuildSpeakerPairs(const std::array<SpeakerPlacement, N>& config) {
|
||||
std::array<SpeakerPair, N> pairs = {};
|
||||
constexpr f32 kDegToRad = std::numbers::pi_v<f32> / 180.0f;
|
||||
|
||||
for (std::size_t i = 0; i < N; i++) {
|
||||
std::size_t j = (i + 1) % N;
|
||||
float lo = config[i].angle;
|
||||
float hi = config[j].angle;
|
||||
float span = hi - lo;
|
||||
if (span < 0) span += 360.0f;
|
||||
|
||||
pairs[i] = {config[i].channel, config[j].channel, lo * kDegToRad, span * kDegToRad};
|
||||
}
|
||||
|
||||
return pairs;
|
||||
}
|
||||
|
||||
constexpr auto Placement6ch = std::to_array<SpeakerPlacement>({
|
||||
// the "rear" channels are actually surround left/right,
|
||||
// changed to match SDL order
|
||||
{OutputChannel::FRONT_RIGHT, -30},
|
||||
{OutputChannel::FRONT_CENTER, 0},
|
||||
{OutputChannel::FRONT_LEFT, 30},
|
||||
{OutputChannel::REAR_LEFT, 120},
|
||||
{OutputChannel::REAR_RIGHT, -120},
|
||||
});
|
||||
|
||||
constexpr auto Placement8ch = std::to_array<SpeakerPlacement>({
|
||||
{OutputChannel::SURROUND_RIGHT, -90},
|
||||
{OutputChannel::FRONT_RIGHT, -30},
|
||||
{OutputChannel::FRONT_CENTER, 0},
|
||||
{OutputChannel::FRONT_LEFT, 30},
|
||||
{OutputChannel::SURROUND_LEFT, 90},
|
||||
{OutputChannel::REAR_LEFT, 150},
|
||||
{OutputChannel::REAR_RIGHT, -150},
|
||||
});
|
||||
|
||||
constexpr auto Pairs6ch = BuildSpeakerPairs(Placement6ch);
|
||||
constexpr auto Pairs8ch = BuildSpeakerPairs(Placement8ch);
|
||||
|
||||
static void CalcStereoChannelVolumes(
|
||||
const JASDsp::TChannel& voice,
|
||||
VolumeArray& volumes)
|
||||
{
|
||||
const auto volume = VolumeFromU16(voice.mAutoMixerVolume);
|
||||
const auto initVolume = VolumeFromU16(voice.mAutoMixerInitVolume);
|
||||
|
||||
const auto right = static_cast<f32>(voice.mAutoMixerPanDolby >> 8) / 127.0f;
|
||||
const auto left = 1.0f - right;
|
||||
|
||||
volumes[0] = {left * volume, left * initVolume};
|
||||
volumes[1] = {right * volume, right * initVolume};
|
||||
}
|
||||
|
||||
static void CalcSurroundChannelVolumes(
|
||||
const JASDsp::TChannel& voice,
|
||||
VolumeArray& volumes)
|
||||
{
|
||||
constexpr f32 kTurn = 2.0f * std::numbers::pi_v<f32>;
|
||||
|
||||
const auto omniGain = 1.0f / static_cast<f32>(OutChannelCount - 1);
|
||||
const auto pan = static_cast<f32>(voice.mAutoMixerPanDolby >> 8) / 63.5f - 1.0f;
|
||||
const auto dolby = static_cast<f32>(voice.mAutoMixerPanDolby & 0xFF) / 63.5f - 1.0f;
|
||||
const auto focus = std::min(std::sqrt(pan * pan + dolby * dolby), 1.0f);
|
||||
|
||||
f32 angle = std::atan2(-pan, -dolby);
|
||||
angle = std::fmod(angle, kTurn);
|
||||
if (angle < 0) angle += kTurn;
|
||||
|
||||
std::array<f32, OutputSubframe::NUM_CHANNELS> gains = {};
|
||||
gains.fill(omniGain * (1.0f - focus));
|
||||
|
||||
using Pairs = std::span<const SpeakerPair>;
|
||||
const auto pairs = OutChannelCount == 6 ? Pairs{Pairs6ch} : Pairs{Pairs8ch};
|
||||
|
||||
for (const auto& pair : pairs) {
|
||||
const auto offset = std::fmod(angle - pair.lo + kTurn, kTurn);
|
||||
|
||||
if (offset <= pair.span) {
|
||||
const auto first = static_cast<size_t>(pair.first);
|
||||
const auto second = static_cast<size_t>(pair.second);
|
||||
const auto t = std::clamp(offset / pair.span, 0.0f, 1.0f);
|
||||
|
||||
const auto firstGain = std::cos(t * std::numbers::pi_v<f32> / 2.0f);
|
||||
const auto secondGain = std::sin(t * std::numbers::pi_v<f32> / 2.0f);
|
||||
|
||||
gains[first] += focus * firstGain;
|
||||
gains[second] += focus * secondGain;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const auto volume = VolumeFromU16(voice.mAutoMixerVolume);
|
||||
const auto initVolume = VolumeFromU16(voice.mAutoMixerInitVolume);
|
||||
|
||||
for (size_t i = 0; i < OutChannelCount; i++) {
|
||||
volumes[i].Target = volume * gains[i];
|
||||
volumes[i].Init = initVolume * gains[i];
|
||||
}
|
||||
}
|
||||
|
||||
static void ApplyPanning(
|
||||
const JASDsp::TChannel& voice,
|
||||
ChannelAuxData& aux,
|
||||
const DspSubframe& input,
|
||||
OutputSubframe& output)
|
||||
{
|
||||
std::array<VolumeValue, OutputSubframe::NUM_CHANNELS> volumes = {};
|
||||
VolumeArray volumes = {};
|
||||
|
||||
if (voice.mAutoMixerBeenSet) {
|
||||
const auto volume = VolumeFromU16(voice.mAutoMixerVolume);
|
||||
const auto initVolume = VolumeFromU16(voice.mAutoMixerInitVolume);
|
||||
|
||||
const auto right = static_cast<f32>(voice.mAutoMixerPanDolby >> 8) / 127.0f;
|
||||
const auto left = 1.0f - right;
|
||||
|
||||
volumes[0] = {left * volume, left * initVolume};
|
||||
volumes[1] = {right * volume, right * initVolume};
|
||||
if (OutChannelCount > 2) {
|
||||
CalcSurroundChannelVolumes(voice, volumes);
|
||||
} else {
|
||||
CalcStereoChannelVolumes(voice, volumes);
|
||||
}
|
||||
} else {
|
||||
for (const auto& outChannel : voice.mOutputChannels) {
|
||||
std::optional<OutputChannel> ch;
|
||||
@@ -529,8 +650,12 @@ static void ApplyPanning(
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < OutputSubframe::NUM_CHANNELS; i++) {
|
||||
for (size_t i = 0; i < OutChannelCount; i++) {
|
||||
const auto ch = static_cast<OutputChannel>(i);
|
||||
if (ch == OutputChannel::LFE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto& volume = volumes[i];
|
||||
|
||||
const f32 targetVolume = volume.Target;
|
||||
@@ -550,6 +675,59 @@ static void ApplyPanning(
|
||||
}
|
||||
}
|
||||
|
||||
static void DownmixSurroundToStereo(
|
||||
const OutputSubframe& input,
|
||||
OutputSubframe& output)
|
||||
{
|
||||
auto& left = output.channels[0];
|
||||
auto& right = output.channels[1];
|
||||
for (int i = 0; i < DSP_SUBFRAME_SIZE; i++) {
|
||||
const auto fc = input.channels[2][i] * 0.5f;
|
||||
left[i] = input.channels[0][i] + fc + input.channels[4][i];
|
||||
right[i] = input.channels[1][i] + fc + input.channels[5][i];
|
||||
if (OutChannelCount > 6) {
|
||||
left[i] += input.channels[6][i];
|
||||
right[i] += input.channels[7][i];
|
||||
left[i] /= 3.5f;
|
||||
right[i] /= 3.5f;
|
||||
} else {
|
||||
left[i] /= 2.5f;
|
||||
right[i] /= 2.5f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void UpmixStereoToSurroundInplace(OutputSubframe& buf) {
|
||||
// pseudoinverse of downmix matrix
|
||||
const auto w = OutChannelCount > 6 ? 1.0f / 12.0f : 1.0f / 8.0f;
|
||||
for (int i = 0; i < DSP_SUBFRAME_SIZE; i++) {
|
||||
const auto le = buf.channels[0][i] * (1.0f + w) + buf.channels[1][i] * -w;
|
||||
const auto re = buf.channels[1][i] * (1.0f + w) + buf.channels[0][i] * -w;
|
||||
const auto c = buf.channels[0][i] * 0.5f + buf.channels[1][i] * 0.5f;
|
||||
/* FL */ buf.channels[0][i] = le;
|
||||
/* FR */ buf.channels[1][i] = re;
|
||||
/* FC */ buf.channels[2][i] = c;
|
||||
/* LFE */ buf.channels[3][i] = 0.0f;
|
||||
/* BL */ buf.channels[4][i] = le;
|
||||
/* BR */ buf.channels[5][i] = re;
|
||||
if (OutChannelCount > 6) {
|
||||
/* SL */ buf.channels[6][i] = le;
|
||||
/* SR */ buf.channels[7][i] = re;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void AccumulateReverbInput(
|
||||
DspSubframe& dstL, DspSubframe& dstR,
|
||||
const DspSubframe& srcL, const DspSubframe& srcR,
|
||||
f32 gain)
|
||||
{
|
||||
for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) {
|
||||
dstL[j] += srcL[j] * gain;
|
||||
dstR[j] += srcR[j] * gain;
|
||||
}
|
||||
}
|
||||
|
||||
void dusk::audio::DspInit() {
|
||||
SharedReverb.setwet(1.0f);
|
||||
SharedReverb.setdry(0.0f);
|
||||
@@ -619,9 +797,12 @@ void dusk::audio::DspRender(OutputSubframe& subframe) {
|
||||
f32 inputGain = (voice.mAutoMixerFxMix >> 8) / 600.0f;
|
||||
if (inputGain > 0) {
|
||||
anyReverbInput = true;
|
||||
for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) {
|
||||
reverbInputL[j] += buf.channels[0][j] * inputGain;
|
||||
reverbInputR[j] += buf.channels[1][j] * inputGain;
|
||||
if (OutChannelCount > 2) {
|
||||
OutputSubframe downmix;
|
||||
DownmixSurroundToStereo(buf, downmix);
|
||||
AccumulateReverbInput(reverbInputL, reverbInputR, downmix.channels[0], downmix.channels[1], inputGain);
|
||||
} else {
|
||||
AccumulateReverbInput(reverbInputL, reverbInputR, buf.channels[0], buf.channels[1], inputGain);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -650,19 +831,29 @@ void dusk::audio::DspRender(OutputSubframe& subframe) {
|
||||
fwrite(interleaved, sizeof(f32), DSP_SUBFRAME_SIZE * 2, sChannelDumpFiles[i]);
|
||||
}
|
||||
|
||||
for (int o = 0; o < subframe.channels.size(); o++) {
|
||||
MixSubframe(subframe.channels[o], buf.channels[o]);
|
||||
}
|
||||
MixOutputSubframe(subframe, buf);
|
||||
}
|
||||
|
||||
if (EnableReverb && (anyReverbInput || ReverbHasTail)) {
|
||||
// Equivalent to -80 dBFS: rms = 1e-4, rms^2 = 1e-8, sumSq = 2 * N * 1e-8
|
||||
constexpr f32 REVERB_ENERGY_EPSILON = 2.0f * DSP_SUBFRAME_SIZE * 1e-8f;
|
||||
f32 wetEnergy = SharedReverb.processmix(
|
||||
reverbInputL.data(), reverbInputR.data(),
|
||||
subframe.channels[0].data(), subframe.channels[1].data(),
|
||||
DSP_SUBFRAME_SIZE, 1, 1.0f
|
||||
);
|
||||
f32 wetEnergy = 0.0f;
|
||||
if (OutChannelCount > 2) {
|
||||
OutputSubframe reverbOut;
|
||||
wetEnergy = SharedReverb.processreplace(
|
||||
reverbInputL.data(), reverbInputR.data(),
|
||||
reverbOut.channels[0].data(), reverbOut.channels[1].data(),
|
||||
DSP_SUBFRAME_SIZE, 1, 1.0f
|
||||
);
|
||||
UpmixStereoToSurroundInplace(reverbOut);
|
||||
MixOutputSubframe(subframe, reverbOut);
|
||||
} else {
|
||||
wetEnergy = SharedReverb.processmix(
|
||||
reverbInputL.data(), reverbInputR.data(),
|
||||
subframe.channels[0].data(), subframe.channels[1].data(),
|
||||
DSP_SUBFRAME_SIZE, 1, 1.0f
|
||||
);
|
||||
}
|
||||
ReverbHasTail = wetEnergy >= REVERB_ENERGY_EPSILON;
|
||||
}
|
||||
|
||||
@@ -688,7 +879,8 @@ void dusk::audio::DspRender(OutputSubframe& subframe) {
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& channel : subframe.channels) {
|
||||
for (int i = 0; i < OutChannelCount; i++) {
|
||||
auto& channel = subframe.channels[i];
|
||||
ApplyVolume(channel, channel, PrevMasterVolume, MasterVolume);
|
||||
}
|
||||
PrevMasterVolume = MasterVolume;
|
||||
|
||||
Reference in New Issue
Block a user