mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-09-09 10:54:06 -04:00
Merge pull request #2384 from dooplecks/surround-redux
Basic discrete surround sound support
This commit is contained in:
@@ -173,11 +173,7 @@ void JASChannel::updateEffectorParam(JASDsp::TChannel* i_channel, u16* i_mixerVo
|
||||
|
||||
f32 pan = 0.5f;
|
||||
f32 dolby = 0.0f;
|
||||
#if TARGET_PC
|
||||
u32 effectiveOutputMode = dusk::audio::EnableHrtf ? JAS_OUTPUT_SURROUND : JASDriver::getOutputMode();
|
||||
#else
|
||||
u32 effectiveOutputMode = JASDriver::getOutputMode();
|
||||
#endif
|
||||
switch (effectiveOutputMode) {
|
||||
case JAS_OUTPUT_MONO:
|
||||
break;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "JSystem/JAudio2/JASDriverIF.h"
|
||||
#include "JSystem/JAudio2/JASAiCtrl.h"
|
||||
#include "JSystem/JAudio2/JASDSPInterface.h"
|
||||
#include "dusk/settings.h"
|
||||
#include <os.h>
|
||||
|
||||
void JASDriver::setDSPLevel(f32 param_0) {
|
||||
@@ -30,7 +31,18 @@ void JASDriver::setOutputMode(u32 mode) {
|
||||
}
|
||||
|
||||
u32 JASDriver::getOutputMode() {
|
||||
#ifdef TARGET_PC
|
||||
switch (dusk::getSettings().audio.outputMode) {
|
||||
case dusk::AudioOutputMode::StereoSpeakers:
|
||||
return JAS_OUTPUT_STEREO;
|
||||
case dusk::AudioOutputMode::StereoHeadphones:
|
||||
case dusk::AudioOutputMode::Surround6ch:
|
||||
case dusk::AudioOutputMode::Surround8ch:
|
||||
return JAS_OUTPUT_SURROUND;
|
||||
}
|
||||
#else
|
||||
return JASDriver::JAS_SYSTEM_OUTPUT_MODE;
|
||||
#endif
|
||||
}
|
||||
|
||||
void JASDriver::waitSubFrame() {
|
||||
|
||||
@@ -745,16 +745,26 @@ f32 Z2Audience::calcRelPosPan(const Vec& param_0, int camID) {
|
||||
f32 Z2Audience::calcRelPosDolby(const Vec& param_0, int camID) {
|
||||
f32 fVar1 = param_0.z + mAudioCamera[camID].getDolbyCenterZ();
|
||||
#if TARGET_PC
|
||||
if (dusk::audio::EnableHrtf) {
|
||||
const auto mode = dusk::getSettings().audio.outputMode.getValue();
|
||||
if (mode >= dusk::AudioOutputMode::StereoHeadphones) {
|
||||
// Normalize the direction so result is purely front/back orientation,
|
||||
// independent of how far away the sound is
|
||||
f32 lenSq = param_0.x * param_0.x + param_0.y * param_0.y + param_0.z * param_0.z;
|
||||
f32 lenSq = param_0.x * param_0.x + param_0.z * param_0.z;
|
||||
if (mode == dusk::AudioOutputMode::StereoHeadphones) {
|
||||
// original HRTF math
|
||||
lenSq += param_0.y * param_0.y;
|
||||
}
|
||||
if (lenSq < 0.0001f) {
|
||||
return 0.5f;
|
||||
}
|
||||
f32 zNorm = param_0.z / sqrtf(lenSq);
|
||||
f32 t = (zNorm + 1.0f) * 0.5f;
|
||||
return 0.5f - 0.5f * cosf(t * static_cast<f32>(M_PI));
|
||||
if (mode == dusk::AudioOutputMode::StereoHeadphones) {
|
||||
// original HRTF math
|
||||
return 0.5f - 0.5f * cosf(t * static_cast<f32>(M_PI));
|
||||
} else {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (fVar1 > mSetting.field_0x48) {
|
||||
|
||||
+10
-21
@@ -876,7 +876,7 @@ void dMenu_Option_c::vib_init() {
|
||||
|
||||
void dMenu_Option_c::vib_move() {
|
||||
bool upTrigger = mpStick->checkUpTrigger();
|
||||
bool downTrigger = mpStick->checkDownTrigger();
|
||||
IF_NOT_DUSK(bool downTrigger =) mpStick->checkDownTrigger();
|
||||
bool leftTrigger = checkLeftTrigger();
|
||||
bool rightTrigger = checkRightTrigger();
|
||||
|
||||
@@ -891,10 +891,14 @@ void dMenu_Option_c::vib_move() {
|
||||
field_0x3ef = PROC_ATTEN_e;
|
||||
#endif
|
||||
Z2GetAudioMgr()->seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0);
|
||||
} else if (downTrigger) {
|
||||
}
|
||||
#ifndef TARGET_PC
|
||||
else if (downTrigger) {
|
||||
field_0x3ef = OPTION_SELECT(PROC_SOUND_e);
|
||||
Z2GetAudioMgr()->seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0);
|
||||
} else if (leftTrigger) {
|
||||
}
|
||||
#endif
|
||||
else if (leftTrigger) {
|
||||
if (isRumbleSupported()) {
|
||||
if (field_0x3ea == 0) {
|
||||
field_0x3ea = 1;
|
||||
@@ -1369,8 +1373,7 @@ void dMenu_Option_c::calibration_close2_move() {
|
||||
|
||||
void dMenu_Option_c::menuVisible() {
|
||||
for (int i = 0; i < 6; i++) {
|
||||
if (i < OPTION_SELECT(PROC_CHANGE_MOVE_e))
|
||||
{
|
||||
if (i < OPTION_SELECT(DUSK_IF_ELSE(PROC_SOUND_e, PROC_CHANGE_MOVE_e))) {
|
||||
menuShow(i);
|
||||
} else {
|
||||
menuHide(i);
|
||||
@@ -2478,7 +2481,7 @@ bool dMenu_Option_c::isRumbleSupported() {
|
||||
#if TARGET_PC
|
||||
bool dMenu_Option_c::pointerConfirmSelect() {
|
||||
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Options);
|
||||
for (u8 i = 0; i < (dusk::version::isRegionJpn() ? 4 : 3); ++i) {
|
||||
for (u8 i = 0; i < (dusk::version::isRegionJpn() ? 3 : 2); ++i) {
|
||||
if (dusk::menu_pointer::hit_pane(mpMenuPane[i], 8.0f)) {
|
||||
dusk::menu_pointer::set_hover_target(i);
|
||||
return false;
|
||||
@@ -2502,7 +2505,7 @@ bool dMenu_Option_c::pointerConfirmSelect() {
|
||||
bool dMenu_Option_c::dpdMenuMove() {
|
||||
#if TARGET_PC
|
||||
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Options);
|
||||
for (u8 i = 0; i < (dusk::version::isRegionJpn() ? 4 : 3); ++i) {
|
||||
for (u8 i = 0; i < (dusk::version::isRegionJpn() ? 3 : 2); ++i) {
|
||||
if (!dusk::menu_pointer::hit_pane(mpMenuPane[i], 8.0f)) {
|
||||
continue;
|
||||
}
|
||||
@@ -2582,20 +2585,6 @@ bool dMenu_Option_c::dpdMenuMove() {
|
||||
-1.0f, 0);
|
||||
}
|
||||
return true;
|
||||
case PROC_SOUND_e:
|
||||
if (field_0x3e9 == 0) {
|
||||
field_0x3e9 = 2;
|
||||
} else {
|
||||
field_0x3e9--;
|
||||
}
|
||||
field_0x3da = 5;
|
||||
mDoAud_setOutputMode(dMo_soundMode[field_0x3e9]);
|
||||
setSoundMode(dMo_soundMode[field_0x3e9]);
|
||||
field_0x3ef = OPTION_SELECT(PROC_CHANGE_MOVE_e);
|
||||
field_0x3f5 = OPTION_SELECT(PROC_SOUND_e);
|
||||
Z2GetAudioMgr()->seStart(Z2SE_SY_OPTION_SWITCH, NULL, 0, 0, 1.0f, 1.0f, -1.0f,
|
||||
-1.0f, 0);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
using namespace dusk::audio;
|
||||
|
||||
static OutputSubframe OutBuffer;
|
||||
static std::array<f32, DSP_SUBFRAME_SIZE * OutputSubframe::NUM_CHANNELS> OutInterleaveBuffer;
|
||||
static std::array<f32, DSP_SUBFRAME_SIZE * OutputSubframe::NUM_CHANNELS> OutInterleaveBufferFull;
|
||||
|
||||
static SDL_AudioStream* PlaybackStream;
|
||||
|
||||
@@ -43,21 +43,55 @@ static int RenderNewAudioFrame();
|
||||
/**
|
||||
* Render an audio subframe and output it to SDL3.
|
||||
*/
|
||||
static void RenderAudioSubframe();
|
||||
static int RenderAudioSubframe();
|
||||
|
||||
static void InitSDL3Output() {
|
||||
SDL_Init(SDL_INIT_AUDIO);
|
||||
static size_t GetChannelCountForOutputMode(dusk::AudioOutputMode config) {
|
||||
switch (config) {
|
||||
default:
|
||||
case dusk::AudioOutputMode::StereoSpeakers:
|
||||
case dusk::AudioOutputMode::StereoHeadphones:
|
||||
return 2;
|
||||
case dusk::AudioOutputMode::Surround6ch:
|
||||
return 6;
|
||||
case dusk::AudioOutputMode::Surround8ch:
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
|
||||
constexpr SDL_AudioSpec spec = {
|
||||
static bool InitSDL3Output() {
|
||||
const auto speakerConfig = dusk::getSettings().audio.outputMode.getValue();
|
||||
const auto desiredChannelCount = GetChannelCountForOutputMode(speakerConfig);
|
||||
const bool hrtf = speakerConfig == dusk::AudioOutputMode::StereoHeadphones;
|
||||
|
||||
if (PlaybackStream && desiredChannelCount == OutChannelCount) {
|
||||
JASCriticalSection section;
|
||||
EnableHrtf = hrtf;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PlaybackStream) {
|
||||
SDL_PauseAudioStreamDevice(PlaybackStream);
|
||||
SDL_DestroyAudioStream(PlaybackStream);
|
||||
} else {
|
||||
SDL_Init(SDL_INIT_AUDIO);
|
||||
}
|
||||
|
||||
const SDL_AudioSpec spec = {
|
||||
SDL_AUDIO_F32,
|
||||
2,
|
||||
static_cast<int>(desiredChannelCount),
|
||||
SampleRate,
|
||||
};
|
||||
PlaybackStream = SDL_OpenAudioDeviceStream(
|
||||
SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
|
||||
&spec,
|
||||
&GetNewAudio,
|
||||
nullptr);
|
||||
SDL_AudioStream* newStream =
|
||||
SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, &GetNewAudio, nullptr);
|
||||
|
||||
{
|
||||
JASCriticalSection section;
|
||||
EnableHrtf = hrtf;
|
||||
OutChannelCount = desiredChannelCount;
|
||||
PlaybackStream = newStream;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void dusk::audio::Initialize() {
|
||||
@@ -72,6 +106,12 @@ void dusk::audio::Initialize() {
|
||||
SDL_ResumeAudioStreamDevice(PlaybackStream);
|
||||
}
|
||||
|
||||
void dusk::audio::Reinitialize() {
|
||||
if (InitSDL3Output()) {
|
||||
SDL_ResumeAudioStreamDevice(PlaybackStream);
|
||||
}
|
||||
}
|
||||
|
||||
void dusk::audio::SetMasterVolume(const f32 value) {
|
||||
JASCriticalSection section;
|
||||
|
||||
@@ -113,53 +153,58 @@ int RenderNewAudioFrame() {
|
||||
ZoneScoped;
|
||||
JASCriticalSection section;
|
||||
const u32 countSubframes = JASDriver::getSubFrames();
|
||||
int bytesWritten = 0;
|
||||
|
||||
JASAudioThread::setDSPSyncCount(countSubframes);
|
||||
|
||||
for (u32 i = 0; i < countSubframes; i++) {
|
||||
RenderAudioSubframe();
|
||||
bytesWritten += RenderAudioSubframe();
|
||||
|
||||
JASAudioThread::snIntCount -= 1;
|
||||
}
|
||||
|
||||
return static_cast<u16>(countSubframes) * sizeof(OutputSubframe);
|
||||
return bytesWritten;
|
||||
}
|
||||
|
||||
static void InterleaveOutputData(const OutputSubframe& data, std::span<f32> target) {
|
||||
assert(target.size() >= data.channels[0].size() * OutputSubframe::NUM_CHANNELS);
|
||||
assert(target.size() >= data.channels[0].size() * OutChannelCount);
|
||||
|
||||
size_t outPos = 0;
|
||||
for (size_t inPos = 0; inPos < data.channels[0].size(); inPos++) {
|
||||
for (size_t channelIdx = 0; channelIdx < OutputSubframe::NUM_CHANNELS; channelIdx++) {
|
||||
for (size_t channelIdx = 0; channelIdx < OutChannelCount; channelIdx++) {
|
||||
target[outPos++] = data.channels[channelIdx][inPos];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RenderAudioSubframe() {
|
||||
int RenderAudioSubframe() {
|
||||
ZoneScoped;
|
||||
OutBuffer = {};
|
||||
|
||||
JASDriver::updateDSP();
|
||||
DspRender(OutBuffer);
|
||||
|
||||
std::span<f32> OutInterleaveBuffer{OutInterleaveBufferFull.data(), static_cast<size_t>(DSP_SUBFRAME_SIZE * OutChannelCount)};
|
||||
InterleaveOutputData(OutBuffer, OutInterleaveBuffer);
|
||||
|
||||
if (JASDriver::extMixCallback != nullptr && JASDriver::sMixMode == MIX_MODE_INTERLEAVE) {
|
||||
static_assert(OutputSubframe::NUM_CHANNELS == 2); // This code only works with Stereo so far.
|
||||
// NOTE: In the real game, this gets called on the entire audio frame, rather than the subframe.
|
||||
// That's probably more efficient, but I didn't wanna change the code to calculate the
|
||||
// entire audio buffers at once.
|
||||
// This is only used for the movie player, and it seems to work fine with the smaller calls.
|
||||
const auto mixData = JASDriver::extMixCallback(DSP_SUBFRAME_SIZE);
|
||||
if (mixData) {
|
||||
for (int i = 0; i < OutInterleaveBuffer.size(); i++) {
|
||||
OutInterleaveBuffer[i] += static_cast<f32>(mixData[i]) / static_cast<f32>(0x7FFF);
|
||||
for (int i = 0; i < DSP_SUBFRAME_SIZE; i++) {
|
||||
const auto oi = i * OutChannelCount;
|
||||
OutInterleaveBuffer[oi] += static_cast<f32>(mixData[i * 2]) / 32767.0f;
|
||||
OutInterleaveBuffer[oi + 1] += static_cast<f32>(mixData[i * 2 + 1]) / 32767.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SDL_PutAudioStreamData(PlaybackStream, &OutInterleaveBuffer, sizeof(OutInterleaveBuffer));
|
||||
auto bytesToWrite = OutInterleaveBuffer.size_bytes();
|
||||
SDL_PutAudioStreamData(PlaybackStream, OutInterleaveBuffer.data(), bytesToWrite);
|
||||
return bytesToWrite;
|
||||
}
|
||||
|
||||
u32 dusk::audio::GetResetCount(int channelIdx) {
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace dusk::audio {
|
||||
*/
|
||||
void Initialize();
|
||||
|
||||
void Reinitialize();
|
||||
|
||||
void SetEnableReverb(bool value);
|
||||
|
||||
void SetMasterVolume(f32 value);
|
||||
|
||||
+411
-303
@@ -16,6 +16,7 @@
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <span>
|
||||
#include <numbers>
|
||||
|
||||
using namespace dusk::audio;
|
||||
|
||||
@@ -50,7 +51,7 @@ bool dusk::audio::EnableReverb = true;
|
||||
bool dusk::audio::DumpAudio = false;
|
||||
bool dusk::audio::EnableHrtf = false;
|
||||
f32 dusk::audio::HrtfGain = 0.5f;
|
||||
|
||||
u8 dusk::audio::OutChannelCount = 0;
|
||||
|
||||
// 3dB at 5kHz.
|
||||
static constexpr f32 HRTF_LP_K = 0.75f;
|
||||
@@ -101,28 +102,6 @@ static u32 ConvertSamplesToDataLength(const JASDsp::TChannel& channel, u32 sampl
|
||||
return (samples / channel.mSamplesPerBlock) * BlockBytes(channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the audio data contributed by a single DSP channel. Reads & decodes new input samples.
|
||||
*/
|
||||
static void RenderChannel(
|
||||
JASDsp::TChannel& channel,
|
||||
ChannelAuxData& channelAux,
|
||||
OutputSubframe& subframe);
|
||||
|
||||
static void RenderOutputChannel(
|
||||
const JASDsp::TChannel& sourceChannel,
|
||||
ChannelAuxData& aux,
|
||||
OutputChannel outputChannel,
|
||||
const std::span<f32> inputSamples,
|
||||
OutputSubframe& fullOutputSubframe);
|
||||
|
||||
/**
|
||||
* Converts a pitch value on a DSP channel to a sample rate.
|
||||
*/
|
||||
constexpr static int PitchToSampleRate(u16 value) {
|
||||
return static_cast<int>(static_cast<u64>(SampleRate) * value / 4096);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset state for a DSP channel between independent playbacks.
|
||||
*/
|
||||
@@ -164,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,
|
||||
@@ -203,16 +188,14 @@ static void GenerateEvolvingHarmonic() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void RenderOscChannel(
|
||||
JASDsp::TChannel& channel,
|
||||
ChannelAuxData& channelAux,
|
||||
OutputSubframe& subframe) {
|
||||
DspSubframe& buf) {
|
||||
if (channel.mResetFlag)
|
||||
ResetChannel(channel, channelAux);
|
||||
|
||||
const u32 pitch = channel.mPitch;
|
||||
DspSubframe buf = {};
|
||||
const auto oscType = static_cast<OscType>(channel.mBytesPerBlock);
|
||||
|
||||
switch (oscType) {
|
||||
@@ -270,142 +253,6 @@ static void RenderOscChannel(
|
||||
DuskLog.error("RenderOscChannel: unimplemented oscillator type {}", channel.mBytesPerBlock);
|
||||
break;
|
||||
}
|
||||
|
||||
auto samples = std::span(buf).subspan(0, DSP_SUBFRAME_SIZE);
|
||||
RenderOutputChannel(channel, channelAux, OutputChannel::LEFT, samples, subframe);
|
||||
RenderOutputChannel(channel, channelAux, OutputChannel::RIGHT, samples, subframe);
|
||||
}
|
||||
|
||||
|
||||
void dusk::audio::DspRender(OutputSubframe& subframe) {
|
||||
ZoneScoped;
|
||||
if (DumpAudio != sDumpWasActive) {
|
||||
sDumpWasActive = DumpAudio;
|
||||
if (DumpAudio) {
|
||||
OpenChannelDumpFiles();
|
||||
} else {
|
||||
CloseChannelDumpFiles();
|
||||
}
|
||||
}
|
||||
|
||||
GenerateEvolvingHarmonic();
|
||||
|
||||
std::span channels(JASDsp::CH_BUF, DSP_CHANNELS);
|
||||
|
||||
DspSubframe reverbInputL = {};
|
||||
DspSubframe reverbInputR = {};
|
||||
bool anyReverbInput = false;
|
||||
|
||||
DspSubframe surroundBus = {};
|
||||
bool anySurroundInput = false;
|
||||
|
||||
for (int i = 0; i < channels.size(); i++) {
|
||||
auto& channel = channels[i];
|
||||
auto& channelAux = ChannelAux[i];
|
||||
|
||||
if (!channel.mIsActive) {
|
||||
continue;
|
||||
}
|
||||
else if (channel.mPauseFlag) {
|
||||
// Not really sure what the practical difference between pause and
|
||||
// deactivation is. Either avoids clearing state or allows the DSP to avoid popping?
|
||||
continue;
|
||||
}
|
||||
else if (channel.mForcedStop) {
|
||||
channel.mIsFinished = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
OutputSubframe channelSubframe = {};
|
||||
if (channel.mWaveAramAddress == 0) {
|
||||
RenderOscChannel(channel, channelAux, channelSubframe);
|
||||
} else {
|
||||
ValidateChannel(channel);
|
||||
RenderChannel(channel, channelAux, channelSubframe);
|
||||
}
|
||||
|
||||
if (EnableReverb) {
|
||||
// scale the input to the reverb rather than using wet/dry on the output.
|
||||
// this way the reverb's internal buffers accumulate energy proportional to mAutoMixerFxMix,
|
||||
// so any tail always decays at the correct level regardless of mAutoMixerFxMix changes
|
||||
// prevents transients when the next sound starts playing with a different reverb level
|
||||
// 600.0f was pulled out of my ass and just sounds good enough for console
|
||||
f32 inputGain = (channel.mAutoMixerFxMix >> 8) / 600.0f;
|
||||
if (inputGain > 0) {
|
||||
anyReverbInput = true;
|
||||
for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) {
|
||||
reverbInputL[j] += channelSubframe.channels[0][j] * inputGain;
|
||||
reverbInputR[j] += channelSubframe.channels[1][j] * inputGain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (EnableHrtf && channel.mAutoMixerBeenSet) {
|
||||
f32 dolby = (channel.mAutoMixerPanDolby & 0xFF) / 127.0f;
|
||||
if (dolby > 0.0f) {
|
||||
anySurroundInput = true;
|
||||
f32 extract = dolby * HRTF_EXTRACT_MAX;
|
||||
f32 frontScale = 1.0f - extract;
|
||||
for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) {
|
||||
f32 mono = (channelSubframe.channels[0][j] + channelSubframe.channels[1][j]) * 0.5f;
|
||||
surroundBus[j] += mono * extract;
|
||||
channelSubframe.channels[0][j] *= frontScale;
|
||||
channelSubframe.channels[1][j] *= frontScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (DumpAudio && sChannelDumpFiles[i]) {
|
||||
f32 interleaved[DSP_SUBFRAME_SIZE * 2];
|
||||
for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) {
|
||||
interleaved[j * 2 + 0] = channelSubframe.channels[0][j];
|
||||
interleaved[j * 2 + 1] = channelSubframe.channels[1][j];
|
||||
}
|
||||
fwrite(interleaved, sizeof(f32), DSP_SUBFRAME_SIZE * 2, sChannelDumpFiles[i]);
|
||||
}
|
||||
|
||||
for (int o = 0; o < subframe.channels.size(); o++) {
|
||||
MixSubframe(subframe.channels[o], channelSubframe.channels[o]);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
ReverbHasTail = wetEnergy >= REVERB_ENERGY_EPSILON;
|
||||
}
|
||||
|
||||
if (EnableHrtf && anySurroundInput) {
|
||||
// Two-pole LPF: -12 dB/oct above 3 kHz
|
||||
for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) {
|
||||
sHrtfLp1 = (1.0f - HRTF_LP_K) * sHrtfLp1 + HRTF_LP_K * surroundBus[j];
|
||||
sHrtfLp2 = (1.0f - HRTF_LP_K) * sHrtfLp2 + HRTF_LP_K * sHrtfLp1;
|
||||
surroundBus[j] = sHrtfLp2;
|
||||
}
|
||||
|
||||
// Mix into L and R
|
||||
// L gets the filtered signal directly; R gets it allpass for mild decorrelation
|
||||
for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) {
|
||||
f32 s = surroundBus[j];
|
||||
|
||||
subframe.channels[0][j] += s * HrtfGain;
|
||||
|
||||
f32 r = -HRTF_ALLPASS_G * s + sHrtfApIn1 + HRTF_ALLPASS_G * sHrtfApOut1;
|
||||
sHrtfApIn1 = s;
|
||||
sHrtfApOut1 = r;
|
||||
subframe.channels[1][j] += r * HrtfGain;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& channel : subframe.channels) {
|
||||
ApplyVolume(channel, channel, PrevMasterVolume, MasterVolume);
|
||||
}
|
||||
PrevMasterVolume = MasterVolume;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -539,135 +386,19 @@ static void FillDecodeBuf(JASDsp::TChannel& channel, ChannelAuxData& aux, int ne
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expected BusConnect value needed to define the given output channel in a DSP channel.
|
||||
*/
|
||||
constexpr u16 GetBusConnect(const OutputChannel channel) {
|
||||
switch (channel) {
|
||||
// TODO: This is a guess for now.
|
||||
case OutputChannel::LEFT:
|
||||
return 0x0D00;
|
||||
case OutputChannel::RIGHT:
|
||||
return 0x0D60;
|
||||
default:
|
||||
CRASH("Invalid output channel!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For a DSP channel the JASDsp::OutputChannelConfig value targeting the given output channel.
|
||||
* Returns null if the DSP channel does not output to this output channel.
|
||||
*/
|
||||
static const JASDsp::OutputChannelConfig* GetOutputConfig(
|
||||
const JASDsp::TChannel& sourceChannel,
|
||||
OutputChannel channel) {
|
||||
|
||||
auto busConnect = GetBusConnect(channel);
|
||||
for (const auto& mOutputChannel : sourceChannel.mOutputChannels) {
|
||||
auto config = &mOutputChannel;
|
||||
if (config->mBusConnect == busConnect) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
struct VolumeValue {
|
||||
f32 Target;
|
||||
f32 Init;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the volume that the given DSP channel should render to the given output channel at.
|
||||
*/
|
||||
static VolumeValue GetVolumeForOutputChannel(
|
||||
const JASDsp::TChannel& sourceChannel,
|
||||
OutputChannel outputChannel) {
|
||||
|
||||
u16 volume;
|
||||
u16 initVolume;
|
||||
f32 panValue = 1;
|
||||
if (sourceChannel.mAutoMixerBeenSet) {
|
||||
volume = sourceChannel.mAutoMixerVolume;
|
||||
initVolume = sourceChannel.mAutoMixerInitVolume;
|
||||
|
||||
auto autoMixerPan = static_cast<f32>(sourceChannel.mAutoMixerPanDolby >> 8) / 127;
|
||||
|
||||
switch (outputChannel) {
|
||||
case OutputChannel::LEFT:
|
||||
panValue = 1 - autoMixerPan;
|
||||
break;
|
||||
case OutputChannel::RIGHT:
|
||||
panValue = autoMixerPan;
|
||||
break;
|
||||
default:
|
||||
CRASH("Unhandled output channel: OutputChannel");
|
||||
}
|
||||
|
||||
} else {
|
||||
auto config = GetOutputConfig(sourceChannel, outputChannel);
|
||||
if (config == nullptr) {
|
||||
return {0, 0};
|
||||
}
|
||||
|
||||
volume = config->mTargetVolume;
|
||||
initVolume = config->mCurrentVolume;
|
||||
}
|
||||
|
||||
// TODO: interpolate to avoid popping.
|
||||
f32 targetRatio = VolumeFromU16(volume);
|
||||
targetRatio *= panValue;
|
||||
|
||||
f32 initRatio = VolumeFromU16(initVolume);
|
||||
initRatio *= panValue;
|
||||
|
||||
return {targetRatio, initRatio};
|
||||
}
|
||||
|
||||
/**
|
||||
* Given decoded & resampled input samples, render a DSP channel to a given output channel.
|
||||
*/
|
||||
static void RenderOutputChannel(
|
||||
const JASDsp::TChannel& sourceChannel,
|
||||
ChannelAuxData& aux,
|
||||
OutputChannel outputChannel,
|
||||
const std::span<f32> inputSamples,
|
||||
OutputSubframe& fullOutputSubframe) {
|
||||
|
||||
auto& outputSubframe = fullOutputSubframe[outputChannel];
|
||||
assert(inputSamples.size() <= outputSubframe.size());
|
||||
|
||||
auto volume = GetVolumeForOutputChannel(sourceChannel, outputChannel);
|
||||
|
||||
f32 targetVolume = volume.Target;
|
||||
auto& prevVolume = aux.PrevVolume(outputChannel);
|
||||
if (std::isnan(prevVolume)) {
|
||||
// Initialize previous volume to new volume on first render.
|
||||
prevVolume = volume.Init;
|
||||
}
|
||||
|
||||
if (prevVolume == 0 && targetVolume == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyVolume(outputSubframe, inputSamples, prevVolume, targetVolume);
|
||||
prevVolume = targetVolume;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch, decode, resample, output
|
||||
* Render the audio data contributed by a single DSP channel. Reads & decodes new input samples.
|
||||
*/
|
||||
static void RenderChannel(
|
||||
JASDsp::TChannel& channel,
|
||||
ChannelAuxData& channelAux,
|
||||
OutputSubframe& subframe) {
|
||||
DspSubframe& buf) {
|
||||
|
||||
if (channel.mResetFlag) {
|
||||
ResetChannel(channel, channelAux);
|
||||
}
|
||||
|
||||
// how many input samples we step per output sample, aka the resampling ratio
|
||||
f32 step = (f32)PitchToSampleRate(channel.mPitch) / SampleRate;
|
||||
auto step = static_cast<f32>(channel.mPitch) / 4096.0f;
|
||||
|
||||
// how many input samples to resample to DSP_SUBFRAME_SIZE output samples
|
||||
int needed = static_cast<int>(channelAux.resamplePos + DSP_SUBFRAME_SIZE * step) + 2;
|
||||
@@ -679,7 +410,6 @@ static void RenderChannel(
|
||||
channel.mIsFinished = true;
|
||||
}
|
||||
|
||||
DspSubframe audioLoadBuffer = {};
|
||||
f32 pos = channelAux.resamplePos;
|
||||
s16 prev = channelAux.resamplePrev;
|
||||
s16 next = channelAux.decodeBufCount > 0 ? channelAux.decodeBuf[0] : prev;
|
||||
@@ -687,7 +417,7 @@ static void RenderChannel(
|
||||
|
||||
// linear resampling and f32 conversion
|
||||
for (int i = 0; i < DSP_SUBFRAME_SIZE; i++) {
|
||||
audioLoadBuffer[i] = (prev + pos * (next - prev)) / 32768.0f;
|
||||
buf[i] = (prev + pos * (next - prev)) / 32768.0f;
|
||||
pos += step;
|
||||
while (pos >= 1.0f) {
|
||||
pos -= 1.0f;
|
||||
@@ -705,7 +435,7 @@ static void RenderChannel(
|
||||
|
||||
// IIR part 1, low-pass: out[n] = (in[n] - in[n-1]) * (coeff/128) + out[n-1]
|
||||
if (s16 coeff = channel.iir_filter_params[4]; coeff != 0) {
|
||||
for (f32& sample : audioLoadBuffer) {
|
||||
for (f32& sample : buf) {
|
||||
f32 out = std::clamp(
|
||||
(sample - channelAux.prev_lp_in) * ((f32)coeff / 128.0f) + channelAux.prev_lp_out, -1.0f, 1.0f
|
||||
);
|
||||
@@ -717,7 +447,7 @@ static void RenderChannel(
|
||||
|
||||
// IIR part 2, biquad: out[n] = (b1*in[n-1] + b2*in[n-2] + a1*out[n-1] + a2*out[n-2]) / 32768
|
||||
if ((channel.mFilterMode & 0x20) != 0) {
|
||||
for (f32& sample : audioLoadBuffer) {
|
||||
for (f32& sample : buf) {
|
||||
f32 out = std::clamp((
|
||||
channel.iir_filter_params[0] * channelAux.biq_in1 + // b1
|
||||
channel.iir_filter_params[1] * channelAux.biq_in2 + // b2
|
||||
@@ -740,28 +470,18 @@ static void RenderChannel(
|
||||
}
|
||||
|
||||
channelAux.decodeBufCount = std::max(0, remainingDecodeBuf);
|
||||
|
||||
auto hasReadSamples = std::span(audioLoadBuffer).subspan(0, DSP_SUBFRAME_SIZE);
|
||||
|
||||
static_assert(OutputSubframe::NUM_CHANNELS == 2, "Keep RenderChannel in sync!");
|
||||
|
||||
RenderOutputChannel(channel, channelAux, OutputChannel::LEFT, hasReadSamples, subframe);
|
||||
RenderOutputChannel(channel, channelAux, OutputChannel::RIGHT, hasReadSamples, subframe);
|
||||
}
|
||||
|
||||
void dusk::audio::DspInit() {
|
||||
SharedReverb.setwet(1.0f);
|
||||
SharedReverb.setdry(0.0f);
|
||||
SharedReverb.setroomsize(0.5f);
|
||||
SharedReverb.setdamp(0.7f);
|
||||
SharedReverb.setwidth(1.0f);
|
||||
SharedReverb.setmode(0.0f);
|
||||
SharedReverb.mute();
|
||||
}
|
||||
struct VolumeValue {
|
||||
f32 Target;
|
||||
f32 Init;
|
||||
};
|
||||
|
||||
void dusk::audio::ApplyVolume(
|
||||
using VolumeArray = std::array<VolumeValue, OutputSubframe::NUM_CHANNELS>;
|
||||
|
||||
static void ApplyVolume(
|
||||
std::span<f32> dst,
|
||||
const std::span<f32> src,
|
||||
std::span<const f32> src,
|
||||
const f32 startVolume,
|
||||
const f32 endVolume) {
|
||||
assert(dst.size() >= src.size());
|
||||
@@ -777,3 +497,391 @@ void dusk::audio::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)
|
||||
{
|
||||
VolumeArray volumes = {};
|
||||
|
||||
if (voice.mAutoMixerBeenSet) {
|
||||
if (OutChannelCount > 2) {
|
||||
CalcSurroundChannelVolumes(voice, volumes);
|
||||
} else {
|
||||
CalcStereoChannelVolumes(voice, volumes);
|
||||
}
|
||||
} else {
|
||||
for (const auto& outChannel : voice.mOutputChannels) {
|
||||
std::optional<OutputChannel> ch;
|
||||
|
||||
switch (outChannel.mBusConnect) {
|
||||
case 0x0D00:
|
||||
ch = OutputChannel::FRONT_LEFT;
|
||||
break;
|
||||
case 0x0D60:
|
||||
ch = OutputChannel::FRONT_RIGHT;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (ch) {
|
||||
auto& v = volumes[static_cast<size_t>(*ch)];
|
||||
v.Target = VolumeFromU16(outChannel.mTargetVolume);
|
||||
v.Init = VolumeFromU16(outChannel.mCurrentVolume);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
auto& prevVolume = aux.PrevVolume(ch);
|
||||
|
||||
if (std::isnan(prevVolume)) {
|
||||
// Initialize previous volume to new volume on first render.
|
||||
prevVolume = volume.Init;
|
||||
}
|
||||
|
||||
if (prevVolume == 0 && targetVolume == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ApplyVolume(output[ch], input, prevVolume, targetVolume);
|
||||
prevVolume = targetVolume;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
SharedReverb.setroomsize(0.5f);
|
||||
SharedReverb.setdamp(0.7f);
|
||||
SharedReverb.setwidth(1.0f);
|
||||
SharedReverb.setmode(0.0f);
|
||||
SharedReverb.mute();
|
||||
}
|
||||
|
||||
void dusk::audio::DspRender(OutputSubframe& subframe) {
|
||||
ZoneScoped;
|
||||
if (DumpAudio != sDumpWasActive) {
|
||||
sDumpWasActive = DumpAudio;
|
||||
if (DumpAudio) {
|
||||
OpenChannelDumpFiles();
|
||||
} else {
|
||||
CloseChannelDumpFiles();
|
||||
}
|
||||
}
|
||||
|
||||
GenerateEvolvingHarmonic();
|
||||
|
||||
std::span voices(JASDsp::CH_BUF, DSP_CHANNELS);
|
||||
|
||||
DspSubframe reverbInputL = {};
|
||||
DspSubframe reverbInputR = {};
|
||||
bool anyReverbInput = false;
|
||||
|
||||
DspSubframe surroundBus = {};
|
||||
bool anySurroundInput = false;
|
||||
|
||||
for (int i = 0; i < voices.size(); i++) {
|
||||
auto& voice = voices[i];
|
||||
auto& aux = ChannelAux[i];
|
||||
|
||||
if (!voice.mIsActive) {
|
||||
continue;
|
||||
}
|
||||
else if (voice.mPauseFlag) {
|
||||
// Not really sure what the practical difference between pause and
|
||||
// deactivation is. Either avoids clearing state or allows the DSP to avoid popping?
|
||||
continue;
|
||||
}
|
||||
else if (voice.mForcedStop) {
|
||||
voice.mIsFinished = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
DspSubframe monoBuf = {};
|
||||
if (voice.mWaveAramAddress == 0) {
|
||||
RenderOscChannel(voice, aux, monoBuf);
|
||||
} else {
|
||||
ValidateChannel(voice);
|
||||
RenderChannel(voice, aux, monoBuf);
|
||||
}
|
||||
|
||||
OutputSubframe buf = {};
|
||||
ApplyPanning(voice, aux, monoBuf, buf);
|
||||
|
||||
if (EnableReverb) {
|
||||
// scale the input to the reverb rather than using wet/dry on the output.
|
||||
// this way the reverb's internal buffers accumulate energy proportional to mAutoMixerFxMix,
|
||||
// so any tail always decays at the correct level regardless of mAutoMixerFxMix changes
|
||||
// prevents transients when the next sound starts playing with a different reverb level
|
||||
// 600.0f was pulled out of my ass and just sounds good enough for console
|
||||
f32 inputGain = (voice.mAutoMixerFxMix >> 8) / 600.0f;
|
||||
if (inputGain > 0) {
|
||||
anyReverbInput = true;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (EnableHrtf && voice.mAutoMixerBeenSet) {
|
||||
f32 dolby = (voice.mAutoMixerPanDolby & 0xFF) / 127.0f;
|
||||
if (dolby > 0.0f) {
|
||||
anySurroundInput = true;
|
||||
f32 extract = dolby * HRTF_EXTRACT_MAX;
|
||||
f32 frontScale = 1.0f - extract;
|
||||
for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) {
|
||||
f32 mono = (buf.channels[0][j] + buf.channels[1][j]) * 0.5f;
|
||||
surroundBus[j] += mono * extract;
|
||||
buf.channels[0][j] *= frontScale;
|
||||
buf.channels[1][j] *= frontScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (DumpAudio && sChannelDumpFiles[i]) {
|
||||
f32 interleaved[DSP_SUBFRAME_SIZE * 2];
|
||||
for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) {
|
||||
interleaved[j * 2 + 0] = buf.channels[0][j];
|
||||
interleaved[j * 2 + 1] = buf.channels[1][j];
|
||||
}
|
||||
fwrite(interleaved, sizeof(f32), DSP_SUBFRAME_SIZE * 2, sChannelDumpFiles[i]);
|
||||
}
|
||||
|
||||
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 = 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;
|
||||
}
|
||||
|
||||
if (EnableHrtf && anySurroundInput) {
|
||||
// Two-pole LPF: -12 dB/oct above 3 kHz
|
||||
for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) {
|
||||
sHrtfLp1 = (1.0f - HRTF_LP_K) * sHrtfLp1 + HRTF_LP_K * surroundBus[j];
|
||||
sHrtfLp2 = (1.0f - HRTF_LP_K) * sHrtfLp2 + HRTF_LP_K * sHrtfLp1;
|
||||
surroundBus[j] = sHrtfLp2;
|
||||
}
|
||||
|
||||
// Mix into L and R
|
||||
// L gets the filtered signal directly; R gets it allpass for mild decorrelation
|
||||
for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) {
|
||||
f32 s = surroundBus[j];
|
||||
|
||||
subframe.channels[0][j] += s * HrtfGain;
|
||||
|
||||
f32 r = -HRTF_ALLPASS_G * s + sHrtfApIn1 + HRTF_ALLPASS_G * sHrtfApOut1;
|
||||
sHrtfApIn1 = s;
|
||||
sHrtfApOut1 = r;
|
||||
subframe.channels[1][j] += r * HrtfGain;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < OutChannelCount; i++) {
|
||||
auto& channel = subframe.channels[i];
|
||||
ApplyVolume(channel, channel, PrevMasterVolume, MasterVolume);
|
||||
}
|
||||
PrevMasterVolume = MasterVolume;
|
||||
}
|
||||
|
||||
@@ -8,14 +8,20 @@
|
||||
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <span>
|
||||
|
||||
namespace dusk::audio {
|
||||
constexpr int SampleRate = 32000;
|
||||
|
||||
enum class OutputChannel : u8 {
|
||||
LEFT,
|
||||
RIGHT,
|
||||
// same as SDL channel layout for 7.1
|
||||
FRONT_LEFT,
|
||||
FRONT_RIGHT,
|
||||
FRONT_CENTER,
|
||||
LFE,
|
||||
REAR_LEFT,
|
||||
REAR_RIGHT,
|
||||
SURROUND_LEFT,
|
||||
SURROUND_RIGHT,
|
||||
OutputChannel_MAX
|
||||
};
|
||||
|
||||
@@ -122,16 +128,11 @@ namespace dusk::audio {
|
||||
return channel.mBytesPerBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a volume level to audio data.
|
||||
* Interpolates across the two provided volume levels to avoid clicking.
|
||||
*/
|
||||
void ApplyVolume(std::span<f32> dst, std::span<f32> src, f32 startVolume, f32 endVolume);
|
||||
|
||||
extern f32 MasterVolume;
|
||||
extern f32 PrevMasterVolume;
|
||||
extern bool EnableReverb;
|
||||
extern bool DumpAudio;
|
||||
extern bool EnableHrtf;
|
||||
extern f32 HrtfGain;
|
||||
extern u8 OutChannelCount;
|
||||
}
|
||||
|
||||
+2
-1
@@ -316,6 +316,7 @@ template class ConfigImpl<BloomMode>;
|
||||
template class ConfigImpl<DepthOfFieldMode>;
|
||||
template class ConfigImpl<DiscVerificationState>;
|
||||
template class ConfigImpl<GameLanguage>;
|
||||
template class ConfigImpl<AudioOutputMode>;
|
||||
|
||||
template <>
|
||||
void ConfigImpl<FrameInterpMode>::loadFromJson(
|
||||
@@ -639,4 +640,4 @@ void shutdown() {
|
||||
s_activeChangeNotifications.clear();
|
||||
}
|
||||
|
||||
} // namespace dusk::config
|
||||
} // namespace dusk::config
|
||||
|
||||
@@ -25,13 +25,13 @@ UserSettings g_userSettings = {
|
||||
},
|
||||
|
||||
.audio = {
|
||||
.outputMode {"audio.outputMode", AudioOutputMode::StereoSpeakers},
|
||||
.masterVolume {"audio.masterVolume", 60},
|
||||
.mainMusicVolume {"audio.mainMusicVolume", 100},
|
||||
.subMusicVolume {"audio.subMusicVolume", 100},
|
||||
.soundEffectsVolume {"audio.soundEffectsVolume", 100},
|
||||
.fanfareVolume {"audio.fanfareVolume", 100},
|
||||
.enableReverb {"audio.enableReverb", true},
|
||||
.enableHrtf {"audio.enableHrtf", false},
|
||||
.menuSounds {"audio.menuSounds", true},
|
||||
},
|
||||
|
||||
@@ -256,13 +256,13 @@ void registerSettings() {
|
||||
[](const int&, const int&) { dusk::ui::apply_scale(); });
|
||||
|
||||
// Audio
|
||||
Register(g_userSettings.audio.outputMode);
|
||||
Register(g_userSettings.audio.masterVolume);
|
||||
Register(g_userSettings.audio.mainMusicVolume);
|
||||
Register(g_userSettings.audio.subMusicVolume);
|
||||
Register(g_userSettings.audio.soundEffectsVolume);
|
||||
Register(g_userSettings.audio.fanfareVolume);
|
||||
Register(g_userSettings.audio.enableReverb);
|
||||
Register(g_userSettings.audio.enableHrtf);
|
||||
Register(g_userSettings.audio.menuSounds);
|
||||
|
||||
// Game
|
||||
|
||||
+14
-1
@@ -68,6 +68,13 @@ enum class MagicArmorMode : u8 {
|
||||
COSMETIC = 4,
|
||||
};
|
||||
|
||||
enum class AudioOutputMode : u8 {
|
||||
StereoSpeakers = 0,
|
||||
StereoHeadphones = 1, // spatial audio
|
||||
Surround6ch = 2, // discrete 5.1
|
||||
Surround8ch = 3, // discrete 7.1
|
||||
};
|
||||
|
||||
namespace config {
|
||||
template <>
|
||||
struct ConfigEnumRange<BloomMode> {
|
||||
@@ -123,6 +130,12 @@ struct ConfigEnumRange<MagicArmorMode> {
|
||||
static constexpr auto max = MagicArmorMode::COSMETIC;
|
||||
};
|
||||
|
||||
template <>
|
||||
struct ConfigEnumRange<AudioOutputMode> {
|
||||
static constexpr auto min = AudioOutputMode::StereoSpeakers;
|
||||
static constexpr auto max = AudioOutputMode::Surround8ch;
|
||||
};
|
||||
|
||||
template <>
|
||||
struct ConfigValueTraits<ui::ControlLayout> {
|
||||
static constexpr bool enabled = true;
|
||||
@@ -150,13 +163,13 @@ struct UserSettings {
|
||||
|
||||
struct {
|
||||
// Audio
|
||||
ConfigVar<AudioOutputMode> outputMode;
|
||||
ConfigVar<int> masterVolume;
|
||||
ConfigVar<int> mainMusicVolume;
|
||||
ConfigVar<int> subMusicVolume;
|
||||
ConfigVar<int> soundEffectsVolume;
|
||||
ConfigVar<int> fanfareVolume;
|
||||
ConfigVar<bool> enableReverb;
|
||||
ConfigVar<bool> enableHrtf;
|
||||
ConfigVar<bool> menuSounds;
|
||||
} audio;
|
||||
|
||||
|
||||
@@ -69,6 +69,13 @@ constexpr std::array kInterpolationModes = {
|
||||
"Unlimited",
|
||||
};
|
||||
|
||||
constexpr std::array kAudioOutputModeNames = {
|
||||
"Stereo (Speakers)",
|
||||
"Stereo (Headphones)",
|
||||
"5.1 Surround",
|
||||
"7.1 Surround",
|
||||
};
|
||||
|
||||
constexpr std::array kTouchTargetingLabels = {
|
||||
"Hybrid",
|
||||
"Hold",
|
||||
@@ -1041,6 +1048,35 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
|
||||
leftPane.add_section("Output");
|
||||
leftPane.register_control(
|
||||
leftPane.add_select_button({
|
||||
.key = "Output Mode",
|
||||
.getValue = [] {
|
||||
const auto idx = static_cast<int>(getSettings().audio.outputMode.getValue());
|
||||
return Rml::String{kAudioOutputModeNames[idx]};
|
||||
},
|
||||
.isModified = [] {
|
||||
const auto& setting = getSettings().audio.outputMode;
|
||||
return setting.getValue() != setting.getDefaultValue();
|
||||
},
|
||||
}), rightPane, [](Pane& pane) {
|
||||
for (int i = 0; i < static_cast<int>(kAudioOutputModeNames.size()); ++i) {
|
||||
pane.add_button({
|
||||
.text = kAudioOutputModeNames[i],
|
||||
.isSelected = [i] {
|
||||
const auto& setting = getSettings().audio.outputMode;
|
||||
return setting.getValue() == static_cast<AudioOutputMode>(i);
|
||||
},
|
||||
}).on_pressed([i] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
getSettings().audio.outputMode.setValue(static_cast<AudioOutputMode>(i));
|
||||
config::save();
|
||||
audio::Reinitialize();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Individual sliders for Main Music, Sub Music, Sound Effects, and Fanfare.
|
||||
leftPane.add_section("Volume");
|
||||
leftPane.register_control(
|
||||
@@ -1073,13 +1109,6 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.helpText = "Enables the reverb effect in game audio.",
|
||||
.onChange = [](bool value) { audio::SetEnableReverb(value); },
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().audio.enableHrtf,
|
||||
{
|
||||
.key = "Enable Spatial Sound",
|
||||
.helpText =
|
||||
"Emulate surround sound via HRTF. Recommended only for use with headphones!",
|
||||
.onChange = [](bool value) { audio::EnableHrtf = value; },
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().audio.menuSounds,
|
||||
{
|
||||
.key = "Dusklight Menu Sounds",
|
||||
|
||||
@@ -731,7 +731,6 @@ int game_main(int argc, char* argv[]) {
|
||||
|
||||
dusk::audio::SetMasterVolume(dusk::audio::MasterVolumeToLinear(dusk::getSettings().audio.masterVolume / 100.0f));
|
||||
dusk::audio::SetEnableReverb(dusk::getSettings().audio.enableReverb);
|
||||
dusk::audio::EnableHrtf = dusk::getSettings().audio.enableHrtf;
|
||||
|
||||
// Run ImGui UI loop if Aurora couldn't initialize a backend
|
||||
if (auroraInfo.backend == BACKEND_NULL) {
|
||||
|
||||
Reference in New Issue
Block a user