diff --git a/Core/NES/APU/ApuTimer.h b/Core/NES/APU/ApuTimer.h index bb502ab53..93808e192 100644 --- a/Core/NES/APU/ApuTimer.h +++ b/Core/NES/APU/ApuTimer.h @@ -51,6 +51,12 @@ class ApuTimer : public ISerializable _lastOutput = output; } } + + __forceinline void SendVolume(uint8_t rawVolume) + { + //Only square channel needs raw volume, for linear mixing + _mixer->RawVolume(_channel, rawVolume); + } int8_t GetLastOutput() { diff --git a/Core/NES/APU/SquareChannel.h b/Core/NES/APU/SquareChannel.h index 7c3bd9bee..14793399c 100644 --- a/Core/NES/APU/SquareChannel.h +++ b/Core/NES/APU/SquareChannel.h @@ -18,6 +18,13 @@ class SquareChannel : public INesMemoryHandler, public ISerializable { 0, 0, 0, 0, 1, 1, 1, 1 }, { 1, 1, 1, 1, 1, 1, 0, 0 } }; + + static constexpr int8_t _dutySequencesUnbiased[4][8] = { + {-1,-1,-1,-1,-1,-1,-1, 7 }, + {-2,-2,-2,-2,-2,-2, 6, 6 }, + {-4,-4,-4,-4, 4, 4, 4, 4 }, + { 2, 2, 2, 2, 2, 2,-6,-6 }, + }; NesConsole* _console = nullptr; ApuEnvelope _envelope; @@ -84,8 +91,10 @@ class SquareChannel : public INesMemoryHandler, public ISerializable { if(IsMuted()) { _timer.AddOutput(0); + _timer.SendVolume(0); } else { - _timer.AddOutput(_dutySequences[_duty][_dutyPos] * _envelope.GetVolume()); + _timer.AddOutput(_console->GetNesConfig().NonlinearSquareMixer ? _dutySequences[_duty][_dutyPos] * _envelope.GetVolume() : _dutySequencesUnbiased[_duty][_dutyPos] * _envelope.GetVolume()); + _timer.SendVolume(_envelope.GetVolume()); } } diff --git a/Core/NES/NesSoundMixer.cpp b/Core/NES/NesSoundMixer.cpp index 22d1770b8..be6b483c2 100644 --- a/Core/NES/NesSoundMixer.cpp +++ b/Core/NES/NesSoundMixer.cpp @@ -177,10 +177,13 @@ double NesSoundMixer::GetChannelOutput(AudioChannel channel, bool forRightChanne int16_t NesSoundMixer::GetOutputVolume(bool forRightChannel) { double squareOutput = GetChannelOutput(AudioChannel::Square1, forRightChannel) + GetChannelOutput(AudioChannel::Square2, forRightChannel); - double tndOutput = GetChannelOutput(AudioChannel::DMC, forRightChannel) + 2.7516713261 * GetChannelOutput(AudioChannel::Triangle, forRightChannel) + 1.8493587125 * GetChannelOutput(AudioChannel::Noise, forRightChannel); + double tndOutput = 2.7516713261 * GetChannelOutput(AudioChannel::Triangle, forRightChannel) + 1.8493587125 * GetChannelOutput(AudioChannel::Noise, forRightChannel) + GetChannelOutput(AudioChannel::DMC, forRightChannel); - uint16_t squareVolume = (uint16_t)((95.88 * 5000.0) / (8128.0 / squareOutput + 100.0)); - uint16_t tndVolume = (uint16_t)((159.79 * 5000.0) / (22638.0 / tndOutput + 100.0)); + //Non-linear square channel mixer flag + double squareVolume = _console->GetNesConfig().NonlinearSquareMixer ? + 95.88 / (8128.0 / squareOutput + 100.0) * 5000.0 + : squareOutput / 240.0 * squareSumFactor[_squareVolume[(int)AudioChannel::Square1] + _squareVolume[(int)AudioChannel::Square2]] * 0.258483 * 5000.0; + double tndVolume = 159.79 / (1.0 / (tndOutput / 22638.0) + 100.0) * 5000.0; return (int16_t)(squareVolume + tndVolume + GetChannelOutput(AudioChannel::FDS, forRightChannel) * 20 + @@ -198,6 +201,11 @@ void NesSoundMixer::AddDelta(AudioChannel channel, uint32_t time, int16_t delta) } } +void NesSoundMixer::RawVolume(AudioChannel channel, uint8_t rawVolume) +{ + _squareVolume[(int)channel] = rawVolume; +} + void NesSoundMixer::EndFrame(uint32_t time) { sort(_timestamps.begin(), _timestamps.end()); diff --git a/Core/NES/NesSoundMixer.h b/Core/NES/NesSoundMixer.h index 4c25e20e0..cead8055a 100644 --- a/Core/NES/NesSoundMixer.h +++ b/Core/NES/NesSoundMixer.h @@ -22,6 +22,7 @@ class NesSoundMixer : public ISerializable static constexpr uint32_t MaxSampleRate = 96000; static constexpr uint32_t MaxSamplesPerFrame = MaxSampleRate / 60 * 4 * 2; //x4 to allow CPU overclocking up to 10x, x2 for panning stereo static constexpr uint32_t MaxChannelCount = 11; + static constexpr double squareSumFactor[31] = { 1.0, 1.352455, 1.336216, 1.320361, 1.304878, 1.289755, 1.274978, 1.260535, 1.246416, 1.232610, 1.219107, 1.205896, 1.192968, 1.180314, 1.167927, 1.155796, 1.143915, 1.132276, 1.120871, 1.109693, 1.098737, 1.087994, 1.077460, 1.067127, 1.056991, 1.047046, 1.037286, 1.027706, 1.018302, 1.009068, 1.0 }; NesConsole* _console = nullptr; SoundMixer* _mixer = nullptr; @@ -36,6 +37,7 @@ class NesSoundMixer : public ISerializable vector _timestamps; int16_t _channelOutput[MaxChannelCount][CycleLength] = {}; int16_t _currentOutput[MaxChannelCount] = {}; + uint8_t _squareVolume[2] = {}; blip_t* _blipBufLeft = nullptr; blip_t* _blipBufRight = nullptr; @@ -66,6 +68,7 @@ class NesSoundMixer : public ISerializable void PlayAudioBuffer(uint32_t cycle); void AddDelta(AudioChannel channel, uint32_t time, int16_t delta); + void RawVolume(AudioChannel channel, uint8_t rawVolume); void Serialize(Serializer& s) override; }; \ No newline at end of file diff --git a/Core/Shared/SettingTypes.h b/Core/Shared/SettingTypes.h index 3403dac13..8f3c0d04d 100644 --- a/Core/Shared/SettingTypes.h +++ b/Core/Shared/SettingTypes.h @@ -132,6 +132,7 @@ struct AudioConfig uint32_t SampleRate = 48000; uint32_t AudioLatency = 60; + bool NonlinearSquareMixer = true; bool MuteSoundInBackground = false; bool ReduceSoundInBackground = true; bool ReduceSoundInFastForward = false; @@ -669,6 +670,7 @@ struct NesConfig uint32_t PpuExtraScanlinesBeforeNmi = 0; uint32_t PpuExtraScanlinesAfterNmi = 0; + bool NonlinearSquareMixer = true; bool DisableNoiseModeFlag = false; bool ReduceDmcPopping = false; bool SilenceTriangleHighFreq = false; diff --git a/UI/Config/NesConfig.cs b/UI/Config/NesConfig.cs index b33bad67e..2c4327a8c 100644 --- a/UI/Config/NesConfig.cs +++ b/UI/Config/NesConfig.cs @@ -88,6 +88,7 @@ public class NesConfig : BaseConfig [Reactive][MinMax(0, 1000)] public UInt32 PpuExtraScanlinesAfterNmi { get; set; } = 0; //Audio + [Reactive] public bool NonlinearSquareMixer { get; set; } = true; [Reactive] public bool DisableNoiseModeFlag { get; set; } = false; [Reactive] public bool ReduceDmcPopping { get; set; } = false; [Reactive] public bool SilenceTriangleHighFreq { get; set; } = false; @@ -207,6 +208,7 @@ public void ApplyConfig() PpuExtraScanlinesAfterNmi = PpuExtraScanlinesAfterNmi, PpuExtraScanlinesBeforeNmi = PpuExtraScanlinesBeforeNmi, + NonlinearSquareMixer = NonlinearSquareMixer, DisableNoiseModeFlag = DisableNoiseModeFlag, ReduceDmcPopping = ReduceDmcPopping, SilenceTriangleHighFreq = SilenceTriangleHighFreq, @@ -344,6 +346,7 @@ public struct InteropNesConfig public UInt32 PpuExtraScanlinesBeforeNmi; public UInt32 PpuExtraScanlinesAfterNmi; + [MarshalAs(UnmanagedType.I1)] public bool NonlinearSquareMixer; [MarshalAs(UnmanagedType.I1)] public bool DisableNoiseModeFlag; [MarshalAs(UnmanagedType.I1)] public bool ReduceDmcPopping; [MarshalAs(UnmanagedType.I1)] public bool SilenceTriangleHighFreq; diff --git a/UI/Localization/resources.en.xml b/UI/Localization/resources.en.xml index cadb77149..de62fc4c6 100644 --- a/UI/Localization/resources.en.xml +++ b/UI/Localization/resources.en.xml @@ -321,6 +321,7 @@ Swap square channels duty cycles (mimics old clones) Reverse DPCM bit order (higher or lower quality, depends on game) Mute ultrasonic frequencies on triangle channel (reduces popping) + Use non-linear square channel mixer Reduce popping sounds on the DMC channel Disable noise channel mode flag (mimics oldest Famicom models) Effects diff --git a/UI/Views/NesConfigView.axaml b/UI/Views/NesConfigView.axaml index e787fc94a..3a083c223 100644 --- a/UI/Views/NesConfigView.axaml +++ b/UI/Views/NesConfigView.axaml @@ -102,6 +102,7 @@ +