Skip to content

Commit 2ea7b39

Browse files
oddbookwormBorishkofMyreMylar
authored
Merge pull request #3556 from oddbookworm/sound-copy-bytes
Add Sound.copy and Sound.__copy__ Co-authored-by: Borishkof <[email protected]> Co-authored-by: MyreMylar <[email protected]>
1 parent d601723 commit 2ea7b39

File tree

5 files changed

+201
-0
lines changed

5 files changed

+201
-0
lines changed

buildconfig/stubs/pygame/mixer.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ from pygame.event import Event
55
from pygame.typing import FileLike
66
from typing_extensions import (
77
Buffer, # collections.abc 3.12
8+
Self,
89
deprecated, # added in 3.13
910
)
1011

@@ -72,6 +73,8 @@ class Sound:
7273
def get_num_channels(self) -> int: ...
7374
def get_length(self) -> float: ...
7475
def get_raw(self) -> bytes: ...
76+
def copy(self) -> Self: ...
77+
def __copy__(self) -> Self: ...
7578

7679
class Channel:
7780
def __init__(self, id: int) -> None: ...

docs/reST/ref/mixer.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,10 @@ The following file formats are supported
369369
an exception when different. Also, source samples are truncated to fit the
370370
audio sample size. This will not change.
371371

372+
.. note:: ``bytes(Sound)`` and ``bytearray(Sound)`` make use of the buffer
373+
interface, which is implemented internally by ``pygame.mixer.Sound``.
374+
Because of this, there is no need to directly implement ``__bytes__``.
375+
372376
.. versionaddedold:: 1.8 ``pygame.mixer.Sound(buffer)``
373377
.. versionaddedold:: 1.9.2
374378
:class:`pygame.mixer.Sound` keyword arguments and array interface support
@@ -500,6 +504,26 @@ The following file formats are supported
500504

501505
.. ## Sound.get_raw ##
502506
507+
.. method:: copy
508+
509+
| :sl:`return a new Sound object that is a deep copy of this Sound`
510+
| :sg:`copy() -> Sound`
511+
| :sg:`copy.copy(original_sound) -> Sound`
512+
513+
Return a new Sound object that is a deep copy of this Sound. The new Sound
514+
will be just as if you loaded it from the same file on disk as you did the
515+
original Sound. If the copy fails, a ``TypeError`` or :meth:`pygame.error`
516+
exception will be raised.
517+
518+
If copying a subclass of ``mixer.Sound``, an instance of the same subclass
519+
will be returned.
520+
521+
Also note that this functions as ``Sound.__copy__``.
522+
523+
.. versionadded:: 2.5.6
524+
525+
.. ## Sound.copy ##
526+
503527
.. ## pygame.mixer.Sound ##
504528
505529
.. class:: Channel

src_c/doc/mixer_doc.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#define DOC_MIXER_SOUND_GETNUMCHANNELS "get_num_channels() -> count\ncount how many times this Sound is playing"
2727
#define DOC_MIXER_SOUND_GETLENGTH "get_length() -> seconds\nget the length of the Sound"
2828
#define DOC_MIXER_SOUND_GETRAW "get_raw() -> bytes\nreturn a bytestring copy of the Sound samples."
29+
#define DOC_MIXER_SOUND_COPY "copy() -> Sound\ncopy.copy(original_sound) -> Sound\nreturn a new Sound object that is a deep copy of this Sound"
2930
#define DOC_MIXER_CHANNEL "Channel(id) -> Channel\nCreate a Channel object for controlling playback"
3031
#define DOC_MIXER_CHANNEL_ID "id -> int\nget the channel id for the Channel object"
3132
#define DOC_MIXER_CHANNEL_PLAY "play(Sound, loops=0, maxtime=0, fade_ms=0) -> None\nplay a Sound on a specific Channel"

src_c/mixer.c

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ pgChannel_New(int);
8989
#define pgChannel_Check(x) \
9090
(PyObject_IsInstance(x, (PyObject *)&pgChannel_Type))
9191

92+
static PyObject *
93+
snd_get_arraystruct(PyObject *self, void *closure);
9294
static int
9395
snd_getbuffer(PyObject *, Py_buffer *, int);
9496
static void
@@ -514,6 +516,7 @@ mixer_quit(PyObject *self, PyObject *_null)
514516
Py_BEGIN_ALLOW_THREADS;
515517
Mix_HaltMusic();
516518
Py_END_ALLOW_THREADS;
519+
Mix_ChannelFinished(NULL);
517520

518521
if (channeldata) {
519522
for (i = 0; i < numchanneldata; ++i) {
@@ -801,6 +804,65 @@ snd_get_raw(PyObject *self, PyObject *_null)
801804
(Py_ssize_t)chunk->alen);
802805
}
803806

807+
static PyObject *
808+
snd_copy(PyObject *self, PyObject *_null)
809+
{
810+
MIXER_INIT_CHECK();
811+
812+
pgSoundObject *newSound =
813+
(pgSoundObject *)Py_TYPE(self)->tp_new(Py_TYPE(self), NULL, NULL);
814+
815+
if (!newSound) {
816+
if (!PyErr_Occurred()) {
817+
PyErr_SetString(PyExc_MemoryError,
818+
"Failed to create new Sound object for copy");
819+
}
820+
return NULL;
821+
}
822+
823+
PyObject *dict = PyDict_New();
824+
if (!dict) {
825+
Py_DECREF(newSound);
826+
return NULL;
827+
}
828+
829+
PyObject *bytes = snd_get_raw(self, NULL);
830+
if (bytes == NULL) {
831+
// exception set already by PyBytes_FromStringAndSize
832+
Py_DECREF(dict);
833+
Py_DECREF(newSound);
834+
return NULL;
835+
}
836+
837+
if (PyDict_SetItemString(dict, "buffer", bytes) < 0) {
838+
// exception set already
839+
Py_DECREF(bytes);
840+
Py_DECREF(dict);
841+
Py_DECREF(newSound);
842+
return NULL;
843+
}
844+
Py_DECREF(bytes);
845+
846+
if (sound_init((PyObject *)newSound, NULL, dict) != 0) {
847+
Py_DECREF(dict);
848+
Py_DECREF(newSound);
849+
// Exception set by sound_init
850+
return NULL;
851+
}
852+
853+
// Preserve original volume on the new chunk
854+
Mix_Chunk *orig = pgSound_AsChunk(self);
855+
Mix_Chunk *dst = pgSound_AsChunk((PyObject *)newSound);
856+
857+
if (orig && dst) {
858+
int vol = Mix_VolumeChunk(orig, -1);
859+
Mix_VolumeChunk(dst, vol);
860+
}
861+
862+
Py_DECREF(dict);
863+
return (PyObject *)newSound;
864+
}
865+
804866
static PyObject *
805867
snd_get_arraystruct(PyObject *self, void *closure)
806868
{
@@ -858,6 +920,8 @@ PyMethodDef sound_methods[] = {
858920
{"get_volume", snd_get_volume, METH_NOARGS, DOC_MIXER_SOUND_GETVOLUME},
859921
{"get_length", snd_get_length, METH_NOARGS, DOC_MIXER_SOUND_GETLENGTH},
860922
{"get_raw", snd_get_raw, METH_NOARGS, DOC_MIXER_SOUND_GETRAW},
923+
{"copy", snd_copy, METH_NOARGS, DOC_MIXER_SOUND_COPY},
924+
{"__copy__", snd_copy, METH_NOARGS, DOC_MIXER_SOUND_COPY},
861925
{NULL, NULL, 0, NULL}};
862926

863927
static PyGetSetDef sound_getset[] = {

test/mixer_test.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import copy
12
import os
23
import pathlib
34
import platform
@@ -1330,6 +1331,114 @@ def __init__(self):
13301331

13311332
self.assertRaises(RuntimeError, incorrect.get_volume)
13321333

1334+
def test_snd_copy(self):
1335+
class SubSound(mixer.Sound):
1336+
def __init__(self, *args, **kwargs):
1337+
super().__init__(*args, **kwargs)
1338+
1339+
mixer.init()
1340+
1341+
filenames = [
1342+
"house_lo.ogg",
1343+
"house_lo.wav",
1344+
"house_lo.flac",
1345+
"house_lo.opus",
1346+
"surfonasinewave.xm",
1347+
]
1348+
old_volumes = [0.1, 0.2, 0.5, 0.7, 1.0]
1349+
new_volumes = [0.2, 0.3, 0.7, 1.0, 0.1]
1350+
if pygame.mixer.get_sdl_mixer_version() >= (2, 6, 0):
1351+
filenames.append("house_lo.mp3")
1352+
old_volumes.append(0.9)
1353+
new_volumes.append(0.5)
1354+
1355+
for f, old_vol, new_vol in zip(filenames, old_volumes, new_volumes):
1356+
filename = example_path(os.path.join("data", f))
1357+
try:
1358+
sound = mixer.Sound(file=filename)
1359+
sound.set_volume(old_vol)
1360+
except pygame.error:
1361+
continue
1362+
sound_copy = sound.copy()
1363+
self.assertEqual(sound.get_length(), sound_copy.get_length())
1364+
self.assertEqual(sound.get_num_channels(), sound_copy.get_num_channels())
1365+
self.assertEqual(sound.get_volume(), sound_copy.get_volume())
1366+
self.assertEqual(sound.get_raw(), sound_copy.get_raw())
1367+
1368+
sound.set_volume(new_vol)
1369+
self.assertNotEqual(sound.get_volume(), sound_copy.get_volume())
1370+
1371+
del sound
1372+
1373+
# Test on the copy for playable sounds
1374+
channel = sound_copy.play()
1375+
if channel is None:
1376+
continue
1377+
self.assertTrue(channel.get_busy())
1378+
sound_copy.stop()
1379+
self.assertFalse(channel.get_busy())
1380+
sound_copy.play()
1381+
self.assertEqual(sound_copy.get_num_channels(), 1)
1382+
1383+
# Test __copy__
1384+
for f, old_vol, new_vol in zip(filenames, old_volumes, new_volumes):
1385+
filename = example_path(os.path.join("data", f))
1386+
try:
1387+
sound = mixer.Sound(file=filename)
1388+
sound.set_volume(old_vol)
1389+
except pygame.error:
1390+
continue
1391+
sound_copy = copy.copy(sound)
1392+
self.assertEqual(sound.get_length(), sound_copy.get_length())
1393+
self.assertEqual(sound.get_num_channels(), sound_copy.get_num_channels())
1394+
self.assertEqual(sound.get_volume(), sound_copy.get_volume())
1395+
self.assertEqual(sound.get_raw(), sound_copy.get_raw())
1396+
1397+
sound.set_volume(new_vol)
1398+
self.assertNotEqual(sound.get_volume(), sound_copy.get_volume())
1399+
1400+
del sound
1401+
1402+
# Test on the copy for playable sounds
1403+
channel = sound_copy.play()
1404+
if channel is None:
1405+
continue
1406+
self.assertTrue(channel.get_busy())
1407+
sound_copy.stop()
1408+
self.assertFalse(channel.get_busy())
1409+
sound_copy.play()
1410+
self.assertEqual(sound_copy.get_num_channels(), 1)
1411+
1412+
# Test copying a subclass of Sound
1413+
for f, old_vol, new_vol in zip(filenames, old_volumes, new_volumes):
1414+
filename = example_path(os.path.join("data", f))
1415+
try:
1416+
sound = SubSound(file=filename)
1417+
sound.set_volume(old_vol)
1418+
except pygame.error:
1419+
continue
1420+
sound_copy = sound.copy()
1421+
self.assertIsInstance(sound_copy, SubSound)
1422+
self.assertEqual(sound.get_length(), sound_copy.get_length())
1423+
self.assertEqual(sound.get_num_channels(), sound_copy.get_num_channels())
1424+
self.assertEqual(sound.get_volume(), sound_copy.get_volume())
1425+
self.assertEqual(sound.get_raw(), sound_copy.get_raw())
1426+
1427+
sound.set_volume(new_vol)
1428+
self.assertNotEqual(sound.get_volume(), sound_copy.get_volume())
1429+
1430+
del sound
1431+
1432+
# Test on the copy for playable sounds
1433+
channel = sound_copy.play()
1434+
if channel is None:
1435+
continue
1436+
self.assertTrue(channel.get_busy())
1437+
sound_copy.stop()
1438+
self.assertFalse(channel.get_busy())
1439+
sound_copy.play()
1440+
self.assertEqual(sound_copy.get_num_channels(), 1)
1441+
13331442

13341443
##################################### MAIN #####################################
13351444

0 commit comments

Comments
 (0)