Browse Source

Fix sound volume/panning attenuation (#1789)

* Fixing volume adjustment and scaling
pull/1843/head
thebigMuh 5 years ago committed by GitHub
parent
commit
24f32a1d53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 24
      Source/effects.cpp
  2. 21
      Source/engine.h
  3. 2
      Source/gamemenu.cpp
  4. 15
      Source/sound.cpp
  5. 7
      Source/sound.h
  6. 51
      Source/utils/math.h
  7. 62
      Source/utils/soundsample.cpp
  8. 11
      Source/utils/soundsample.h
  9. 20
      test/effects_test.cpp

24
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;
}

21
Source/engine.h

@ -17,6 +17,7 @@
#include <cstdint>
#include <cstdlib>
#include <memory>
#include <tuple>
#include <utility>
#include <SDL.h>
@ -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 {

2
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;
}

15
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<float>(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<float>(sgOptions.Audio.nMusicVolume) / VOLUME_MIN);
music->setVolume(VolumeLogToLinear(sgOptions.Audio.nMusicVolume, VOLUME_MIN, VOLUME_MAX));
return sgOptions.Audio.nMusicVolume;
}

7
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,

51
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<typename T>
template <typename T>
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 <typename V, typename T>
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 <typename T>
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 <typename T>
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

62
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<float>(-std::abs(logPan)) / StereoSeparation);
return copysign(1.0f - factor, static_cast<float>(logPan));
}
} // namespace
float VolumeLogToLinear(int logVolume, int logMin, int logMax)
{
const float logScaled = math::Remap<float>(logMin, logMax, MillibelMin, MillibelMax, logVolume);
const auto linVolume = std::pow(LogBase, static_cast<float>(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<float>(lVolume) / Scale));
stream_->setStereoPosition(
lPan == 0 ? 0
: copysign(1.F - std::pow(Base, static_cast<float>(-std::fabs(lPan) / Scale)),
static_cast<float>(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());

11
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);

20
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);
}

Loading…
Cancel
Save