diff --git a/Source/effects.cpp b/Source/effects.cpp index 686f83440..f8a238db5 100644 --- a/Source/effects.cpp +++ b/Source/effects.cpp @@ -1091,13 +1091,13 @@ static void stream_play(TSFX *pSFX, int lVolume, int lPan) assert(pSFX); assert(pSFX->bFlags & sfx_STREAM); stream_stop(); - lVolume += sound_get_or_set_sound_volume(1); + if (lVolume >= VOLUME_MIN) { if (lVolume > VOLUME_MAX) lVolume = VOLUME_MAX; if (pSFX->pSnd == nullptr) pSFX->pSnd = sound_file_load(pSFX->pszName, AllowStreaming); - pSFX->pSnd->DSB.Play(lVolume, lPan, 0); + pSFX->pSnd->DSB.Play(lVolume, sound_get_or_set_sound_volume(1), lPan, 0); sgpStreamSFX = pSFX; } } @@ -1144,23 +1144,21 @@ bool calc_snd_position(int x, int y, int *plVolume, int *plPan) { int pan, volume; - x -= plr[myplr].position.tile.x; - y -= plr[myplr].position.tile.y; + const auto &playerPosition = plr[myplr].position.tile; - pan = (x - y) * 256; - *plPan = pan; + const int dx = x - playerPosition.x; + const int dy = y - playerPosition.y; - if (abs(pan) > 6400) - return false; + pan = (dx - dy) * 256; + *plPan = clamp(pan, PAN_MIN, PAN_MAX); - volume = abs(x) > abs(y) ? abs(x) : abs(y); - volume *= 64; - *plVolume = volume; + volume = playerPosition.ApproxDistance({ x, y }); + volume *= -64; - if (volume >= 6400) + if (volume <= ATTENUATION_MIN) return false; - *plVolume = -volume; + *plVolume = volume; return true; } diff --git a/Source/engine.h b/Source/engine.h index 9e8441282..8c66a08b0 100644 --- a/Source/engine.h +++ b/Source/engine.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -91,6 +92,26 @@ struct Point { a -= b; return a; } + + /** + * @brief Fast approximate distance between two points, using only integer arithmetic, with less than ~5% error + * @param other Pointer to which we want the distance + * @return Magnitude of vector this -> other + */ + + int ApproxDistance(Point other) const + { + int min; + int max; + + std::tie(min, max) = std::minmax(std::abs(other.x - x), std::abs(other.y - y)); + + int approx = max * 1007 + min * 441; + if (max < (min * 16)) + approx -= max * 40; + + return (approx + 512) / 1024; + } }; struct ActorPosition { diff --git a/Source/gamemenu.cpp b/Source/gamemenu.cpp index 42113a775..bf115e425 100644 --- a/Source/gamemenu.cpp +++ b/Source/gamemenu.cpp @@ -186,7 +186,7 @@ void gamemenu_sound_music_toggle(const char *const *names, TMenuItem *menu_item, if (gbSndInited) { menu_item->dwFlags |= GMENU_ENABLED | GMENU_SLIDER; menu_item->pszStr = names[0]; - gmenu_slider_steps(menu_item, 17); + gmenu_slider_steps(menu_item, VOLUME_STEPS); gmenu_slider_set(menu_item, VOLUME_MIN, VOLUME_MAX, volume); return; } diff --git a/Source/sound.cpp b/Source/sound.cpp index 9914da589..001cbd00b 100644 --- a/Source/sound.cpp +++ b/Source/sound.cpp @@ -20,6 +20,7 @@ #include "storm/storm_sdl_rw.h" #include "storm/storm.h" #include "utils/log.hpp" +#include "utils/math.h" #include "utils/sdl_mutex.h" #include "utils/stdcompat/optional.hpp" #include "utils/stdcompat/shared_ptr_array.hpp" @@ -121,12 +122,7 @@ const char *const sgszMusicTracks[NUM_MUSIC] = { static int CapVolume(int volume) { - if (volume < VOLUME_MIN) { - volume = VOLUME_MIN; - } else if (volume > VOLUME_MAX) { - volume = VOLUME_MAX; - } - return volume - volume % 100; + return clamp(volume, VOLUME_MIN, VOLUME_MAX); } void ClearDuplicateSounds() { @@ -154,8 +150,7 @@ void snd_play_snd(TSnd *pSnd, int lVolume, int lPan) return; } - lVolume = CapVolume(lVolume + sgOptions.Audio.nSoundVolume); - sound->Play(lVolume, lPan); + sound->Play(lVolume, sgOptions.Audio.nSoundVolume, lPan); pSnd->start_tc = tc; } @@ -259,7 +254,7 @@ void music_start(uint8_t nTrack) return; } - music->setVolume(1.F - static_cast(sgOptions.Audio.nMusicVolume) / VOLUME_MIN); + music->setVolume(VolumeLogToLinear(sgOptions.Audio.nMusicVolume, VOLUME_MIN, VOLUME_MAX)); if (!music->play(/*iterations=*/0)) { LogError(LogCategory::Audio, "Aulib::Stream::play (from music_start): {}", SDL_GetError()); CleanupMusic(); @@ -288,7 +283,7 @@ int sound_get_or_set_music_volume(int volume) sgOptions.Audio.nMusicVolume = volume; if (music) - music->setVolume(1.F - static_cast(sgOptions.Audio.nMusicVolume) / VOLUME_MIN); + music->setVolume(VolumeLogToLinear(sgOptions.Audio.nMusicVolume, VOLUME_MIN, VOLUME_MAX)); return sgOptions.Audio.nMusicVolume; } diff --git a/Source/sound.h b/Source/sound.h index 8c5140789..2f66e3251 100644 --- a/Source/sound.h +++ b/Source/sound.h @@ -19,6 +19,13 @@ namespace devilution { #define VOLUME_MIN -1600 #define VOLUME_MAX 0 +#define VOLUME_STEPS 64 + +#define ATTENUATION_MIN -6400 +#define ATTENUATION_MAX 0 + +#define PAN_MIN -6400 +#define PAN_MAX 6400 enum _music_id : uint8_t { TMUSIC_TOWN, diff --git a/Source/utils/math.h b/Source/utils/math.h index 6cb2b97f3..b4feac8e3 100644 --- a/Source/utils/math.h +++ b/Source/utils/math.h @@ -14,11 +14,60 @@ namespace math { * @param t Value to compute sign of * @return -1 if t < 0, 1 if t > 0, 0 if t == 0 */ -template +template int Sign(T t) { return (t > T(0)) - (t < T(0)); } +/** + * @brief Linearly interpolate from a towards b using mixing value t + * @tparam V Any arithmetic type, used for interpolants and return value + * @tparam T Any arithmetic type, used for interpolator + * @param a Low interpolation value (returned when t == 0) + * @param b High interpolation value (returned when t == 1) + * @param t Interpolator, commonly in range [0..1], values outside this range will extrapolate + * @return a + (b - a) * t +*/ +template +V Lerp(V a, V b, T t) +{ + return a + (b - a) * t; +} + +/** + * @brief Inverse lerp, given two key values a and b, and a free value v, determine mixing factor t so that v = Lerp(a, b, t) + * @tparam T Any arithmetic type + * @param a Low key value (function returns 0 if v == a) + * @param b High key value (function returns 1 if v == b) + * @param v Mixing factor, commonly in range [a..b] to get a return [0..1] + * @return Value t so that v = Lerp(a, b, t); or 0 if b == a +*/ +template +T InvLerp(T a, T b, T v) +{ + if (b == a) + return T(0); + + return (v - a) / (b - a); +} + +/** + * @brief Remaps value v from range [inMin, inMax] to [outMin, outMax] + * @tparam T Any arithmetic type + * @param inMin First bound of input range + * @param inMax Second bound of input range + * @param outMin First bound of output range + * @param outMax Second bound of output range + * @param v Value to remap + * @return Transformed value so that InvLerp(inMin, inMax, v) == InvLerp(outMin, outMax, return) +*/ +template +T Remap(T inMin, T inMax, T outMin, T outMax, T v) +{ + auto t = InvLerp(inMin, inMax, v); + return Lerp(outMin, outMax, t); +} + } // namespace math } // namespace devilution diff --git a/Source/utils/soundsample.cpp b/Source/utils/soundsample.cpp index e6e960552..f993927f3 100644 --- a/Source/utils/soundsample.cpp +++ b/Source/utils/soundsample.cpp @@ -16,10 +16,57 @@ #include "storm/storm_sdl_rw.h" #include "storm/storm.h" #include "utils/log.hpp" +#include "utils/math.h" #include "utils/stubs.h" namespace devilution { +namespace { + +constexpr float LogBase = 10.0f; + +/** + * Scaling factor for attenuating volume. + * Picked so that a volume change of -10 dB results in half perceived loudness. + * VolumeScale = -1000 / log(0.5) + */ +constexpr float VolumeScale = 3321.9281f; + +/** + * Min and max volume range, in millibel. + * -100 dB (muted) to 0 dB (max. loudness). + */ +constexpr float MillibelMin = -10000.0f; +constexpr float MillibelMax = 0.0f; + +/** + * Stereo separation factor for left/right speaker panning. Lower values increase separation, moving + * sounds further left/right, while higher values will pull sounds more towards the middle, reducing separation. + * Current value is tuned to have ~2:1 mix for sounds that happen on the edge of a 640x480 screen. + */ +constexpr float StereoSeparation = 6000.0f; + +float PanLogToLinear(int logPan) +{ + if (logPan == 0) + return 0; + + auto factor = std::pow(LogBase, static_cast(-std::abs(logPan)) / StereoSeparation); + + return copysign(1.0f - factor, static_cast(logPan)); +} + +} // namespace + +float VolumeLogToLinear(int logVolume, int logMin, int logMax) +{ + const float logScaled = math::Remap(logMin, logMax, MillibelMin, MillibelMax, logVolume); + const auto linVolume = std::pow(LogBase, static_cast(logScaled) / VolumeScale); + return linVolume; +} + +///// SoundSample ///// + void SoundSample::Release() { stream_ = nullptr; @@ -38,18 +85,17 @@ bool SoundSample::IsPlaying() /** * @brief Start playing the sound */ -void SoundSample::Play(int lVolume, int lPan, int channel) +void SoundSample::Play(int logSoundVolume, int logUserVolume, int logPan, int channel) { if (!stream_) return; - constexpr float Base = 10.F; - constexpr float Scale = 2000.F; - stream_->setVolume(std::pow(Base, static_cast(lVolume) / Scale)); - stream_->setStereoPosition( - lPan == 0 ? 0 - : copysign(1.F - std::pow(Base, static_cast(-std::fabs(lPan) / Scale)), - static_cast(lPan))); + const int combinedLogVolume = logSoundVolume + logUserVolume * (ATTENUATION_MIN / VOLUME_MIN); + const float linearVolume = VolumeLogToLinear(combinedLogVolume, ATTENUATION_MIN, 0); + stream_->setVolume(linearVolume); + + const float linearPan = PanLogToLinear(logPan); + stream_->setStereoPosition(linearPan); if (!stream_->play()) { LogError(LogCategory::Audio, "Aulib::Stream::play (from SoundSample::Play): {}", SDL_GetError()); diff --git a/Source/utils/soundsample.h b/Source/utils/soundsample.h index 583e2d667..519b1ce7c 100644 --- a/Source/utils/soundsample.h +++ b/Source/utils/soundsample.h @@ -11,6 +11,15 @@ namespace devilution { +/** + * @brief Converts log volume passed in into linear volume. + * @param logVolume Logarithmic volume in the range [logMin..logMax] + * @param logMin Volume range minimum (usually ATTENUATION_MIN for game sounds and VOLUME_MIN for volume sliders) + * @param logMax Volume range maximum (usually 0) + * @return Linear volume in the range [0..1] +*/ +float VolumeLogToLinear(int logVolume, int logMin, int logMax); + class SoundSample final { public: SoundSample() = default; @@ -19,7 +28,7 @@ public: void Release(); bool IsPlaying(); - void Play(int lVolume, int lPan, int channel = -1); + void Play(int logSoundVolume, int logUserVolume, int logPan, int channel = -1); void Stop(); int SetChunkStream(std::string filePath); diff --git a/test/effects_test.cpp b/test/effects_test.cpp index 82a913260..25ef2cf3e 100644 --- a/test/effects_test.cpp +++ b/test/effects_test.cpp @@ -28,29 +28,29 @@ TEST(Effects, calc_snd_position_near) TEST(Effects, calc_snd_position_out_of_range) { plr[myplr].position.tile = { 12, 12 }; - int plVolume = 0; + int plVolume = 1234; int plPan = 0; EXPECT_EQ(calc_snd_position(112, 112, &plVolume, &plPan), false); - ASSERT_GE(plVolume, 6400); + EXPECT_EQ(plVolume, 1234); EXPECT_EQ(plPan, 0); } -TEST(Effects, calc_snd_position_extream_right) +TEST(Effects, calc_snd_position_extreme_right) { plr[myplr].position.tile = { 50, 50 }; int plVolume = 0; int plPan = 0; - EXPECT_EQ(calc_snd_position(76, 50, &plVolume, &plPan), false); - EXPECT_EQ(plVolume, 0); - EXPECT_GT(plPan, 6400); + EXPECT_EQ(calc_snd_position(75, 25, &plVolume, &plPan), true); + EXPECT_EQ(plVolume, -2176); + EXPECT_EQ(plPan, 6400); } -TEST(Effects, calc_snd_position_extream_left) +TEST(Effects, calc_snd_position_extreme_left) { plr[myplr].position.tile = { 50, 50 }; int plVolume = 0; int plPan = 0; - EXPECT_EQ(calc_snd_position(24, 50, &plVolume, &plPan), false); - EXPECT_EQ(plVolume, 0); - EXPECT_LT(plPan, -6400); + EXPECT_EQ(calc_snd_position(25, 75, &plVolume, &plPan), true); + EXPECT_EQ(plVolume, -2176); + EXPECT_EQ(plPan, -6400); }