Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keep track of floating point volume internally for Sound and Channel #3091

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src_c/include/pygame_mixer.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ typedef struct {
PyObject_HEAD Mix_Chunk *chunk;
Uint8 *mem;
PyObject *weakreflist;
float volume;
} pgSoundObject;

typedef struct {
Expand Down
55 changes: 48 additions & 7 deletions src_c/mixer.c
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,37 @@ struct ChannelData {
PyObject *sound;
PyObject *queue;
int endevent;
float volume;
};
static struct ChannelData *channeldata = NULL;
static int numchanneldata = 0;

Mix_Music **mx_current_music;
Mix_Music **mx_queue_music;

// SDL volume range is 0 to 128
// 1.0 / 128 = 0.0078125 which is roughly 0.008
#define MIXER_VOLUME_DELTA 0.008f
// it's a RuntimeError because it's not a misuse of the API
// something has gone terribly wrong because this should
// never be raised, it is here simply as a precaution
#define MIXER_VOLUME_DELTA_CHECK(delta) \
do { \
if (delta > MIXER_VOLUME_DELTA) { \
PyObject *diff_threshold_obj = \
PyFloat_FromDouble(MIXER_VOLUME_DELTA), \
*abs_diff_obj = PyFloat_FromDouble(delta); \
PyErr_Format(PyExc_RuntimeError, \
"The absolute difference between SDL's returned " \
"channel volume and the internally tracked one is " \
"greater than the set threshold (%S): %S", \
diff_threshold_obj, abs_diff_obj); \
Py_DECREF(diff_threshold_obj); \
Py_DECREF(abs_diff_obj); \
return NULL; \
} \
} while (0)

static int
_format_itemsize(Uint16 format)
{
Expand Down Expand Up @@ -421,6 +445,7 @@ _init(int freq, int size, int channels, int chunk, char *devicename,
channeldata[i].sound = NULL;
channeldata[i].queue = NULL;
channeldata[i].endevent = 0;
channeldata[i].volume = 1.0f;
}
}

Expand Down Expand Up @@ -731,6 +756,10 @@ snd_set_volume(PyObject *self, PyObject *args)

MIXER_INIT_CHECK();

if (volume >= 0.0f) {
((pgSoundObject *)self)->volume = MIN(volume, 1.0f);
}

Mix_VolumeChunk(chunk, (int)(volume * 128));
Py_RETURN_NONE;
}
Expand All @@ -739,14 +768,17 @@ static PyObject *
snd_get_volume(PyObject *self, PyObject *_null)
{
Mix_Chunk *chunk = pgSound_AsChunk(self);
float mix_volume, volume;

CHECK_CHUNK_VALID(chunk, NULL);

int volume;
MIXER_INIT_CHECK();

volume = Mix_VolumeChunk(chunk, -1);
return PyFloat_FromDouble(volume / 128.0);
mix_volume = Mix_VolumeChunk(chunk, -1) / 128.0f;
volume = ((pgSoundObject *)self)->volume;

MIXER_VOLUME_DELTA_CHECK(fabsf(mix_volume - volume));

return PyFloat_FromDouble(volume);
}

static PyObject *
Expand Down Expand Up @@ -1245,6 +1277,10 @@ chan_set_volume(PyObject *self, PyObject *args)
volume = 1.0f;
}

if (volume >= 0.0f) {
channeldata[channelnum].volume = MIN(volume, 1.0f);
}

#ifdef Py_DEBUG
result =
#endif
Expand All @@ -1256,13 +1292,16 @@ static PyObject *
chan_get_volume(PyObject *self, PyObject *_null)
{
int channelnum = pgChannel_AsInt(self);
int volume;
float mix_volume, volume;

MIXER_INIT_CHECK();

volume = Mix_Volume(channelnum, -1);
mix_volume = Mix_Volume(channelnum, -1) / 128.0f;
volume = channeldata[channelnum].volume;

MIXER_VOLUME_DELTA_CHECK(fabsf(mix_volume - volume));

return PyFloat_FromDouble(volume / 128.0);
return PyFloat_FromDouble(volume);
}

static PyObject *
Expand Down Expand Up @@ -1423,6 +1462,7 @@ set_num_channels(PyObject *self, PyObject *args)
channeldata[i].sound = NULL;
channeldata[i].queue = NULL;
channeldata[i].endevent = 0;
channeldata[i].volume = 1.0f;
}
numchanneldata = numchans;
}
Expand Down Expand Up @@ -1800,6 +1840,7 @@ sound_init(PyObject *self, PyObject *arg, PyObject *kwarg)

((pgSoundObject *)self)->chunk = NULL;
((pgSoundObject *)self)->mem = NULL;
((pgSoundObject *)self)->volume = 1.0f;

/* Similar to MIXER_INIT_CHECK(), but different return value. */
if (!SDL_WasInit(SDL_INIT_AUDIO)) {
Expand Down
121 changes: 96 additions & 25 deletions test/mixer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -913,33 +913,54 @@ def todo_test_set_endevent(self):

self.fail()

def todo_test_set_volume(self):
# __doc__ (as of 2008-08-02) for pygame.mixer.Channel.set_volume:
def test_set_volume(self):
float_delta = 1.0 / 128 # SDL volume range is 0 to 128
ch = mixer.Channel(0)
self.assertAlmostEqual(ch.get_volume(), 1.0)

# Channel.set_volume(value): return None
# Channel.set_volume(left, right): return None
# set the volume of a playing channel
#
# Set the volume (loudness) of a playing sound. When a channel starts
# to play its volume value is reset. This only affects the current
# sound. The value argument is between 0.0 and 1.0.
#
# If one argument is passed, it will be the volume of both speakers.
# If two arguments are passed and the mixer is in stereo mode, the
# first argument will be the volume of the left speaker and the second
# will be the volume of the right speaker. (If the second argument is
# None, the first argument will be the volume of both speakers.)
#
# If the channel is playing a Sound on which set_volume() has also
# been called, both calls are taken into account. For example:
#
# sound = pygame.mixer.Sound("s.wav")
# channel = s.play() # Sound plays at full volume by default
# sound.set_volume(0.9) # Now plays at 90% of full volume.
# sound.set_volume(0.6) # Now plays at 60% (previous value replaced).
# channel.set_volume(0.5) # Now plays at 30% (0.6 * 0.5).
ch.set_volume(0.0)
self.assertAlmostEqual(ch.get_volume(), 0.0)

self.fail()
ch.set_volume(0.5)
self.assertAlmostEqual(ch.get_volume(), 0.5)
ch.set_volume(-0.1)
self.assertAlmostEqual(ch.get_volume(), 0.5)
ch.set_volume(-5)
self.assertAlmostEqual(ch.get_volume(), 0.5)

ch.set_volume(0.99)
self.assertAlmostEqual(ch.get_volume(), 0.99)
ch.set_volume(1.0)
self.assertAlmostEqual(ch.get_volume(), 1.0)
ch.set_volume(1.1)
self.assertAlmostEqual(ch.get_volume(), 1.0)
ch.set_volume(3)
self.assertAlmostEqual(ch.get_volume(), 1.0)

ch.set_volume(-0.5)
self.assertAlmostEqual(ch.get_volume(), 1.0)

for volume_1000x in range(0, 1_000 + 1):
set_volume = volume_1000x / 1_000

ch.set_volume(set_volume)
true_volume = ch.get_volume()

with self.subTest(
"Loose delta", set_volume=set_volume, true_volume=true_volume
):
self.assertAlmostEqual(set_volume, true_volume, delta=float_delta)

for volume_1000x in range(0, 1_000 + 1):
set_volume = volume_1000x / 1_000

ch.set_volume(set_volume)
true_volume = ch.get_volume()

with self.subTest(
"Strict delta", set_volume=set_volume, true_volume=true_volume
):
self.assertAlmostEqual(set_volume, true_volume)

def todo_test_stop(self):
# __doc__ (as of 2008-08-02) for pygame.mixer.Channel.stop:
Expand Down Expand Up @@ -1260,6 +1281,56 @@ def test_set_volume(self):
with self.assertRaisesRegex(pygame.error, "mixer not initialized"):
sound.set_volume(1)

def test_set_volume_exact(self):
float_delta = 1.0 / 128 # SDL volume range is 0 to 128
filename = example_path(os.path.join("data", "house_lo.wav"))
snd = mixer.Sound(file=filename)
self.assertAlmostEqual(snd.get_volume(), 1.0)

snd.set_volume(0.0)
self.assertAlmostEqual(snd.get_volume(), 0.0)

snd.set_volume(0.5)
self.assertAlmostEqual(snd.get_volume(), 0.5)
snd.set_volume(-0.1)
self.assertAlmostEqual(snd.get_volume(), 0.5)
snd.set_volume(-5)
self.assertAlmostEqual(snd.get_volume(), 0.5)

snd.set_volume(0.99)
self.assertAlmostEqual(snd.get_volume(), 0.99)
snd.set_volume(1.0)
self.assertAlmostEqual(snd.get_volume(), 1.0)
snd.set_volume(1.1)
self.assertAlmostEqual(snd.get_volume(), 1.0)
snd.set_volume(3)
self.assertAlmostEqual(snd.get_volume(), 1.0)

snd.set_volume(-0.5)
self.assertAlmostEqual(snd.get_volume(), 1.0)

for volume_1000x in range(0, 1_000 + 1):
set_volume = volume_1000x / 1_000

snd.set_volume(set_volume)
true_volume = snd.get_volume()

with self.subTest(
"Loose delta", set_volume=set_volume, true_volume=true_volume
):
self.assertAlmostEqual(set_volume, true_volume, delta=float_delta)

for volume_1000x in range(0, 1_000 + 1):
set_volume = volume_1000x / 1_000

snd.set_volume(set_volume)
true_volume = snd.get_volume()

with self.subTest(
"Strict delta", set_volume=set_volume, true_volume=true_volume
):
self.assertAlmostEqual(set_volume, true_volume)

def todo_test_set_volume__while_playing(self):
"""Ensure a sound's volume can be set while playing."""
self.fail()
Expand Down
Loading