Skip to content

Commit c36788a

Browse files
Enabling users to set the soundfont on midi playback (#2274)
1 parent 31f143c commit c36788a

File tree

6 files changed

+127
-0
lines changed

6 files changed

+127
-0
lines changed

buildconfig/stubs/pygame/mixer.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ def set_num_channels(count: int) -> None: ...
3636
def get_num_channels() -> int: ...
3737
def set_reserved(count: int) -> int: ...
3838
def find_channel(force: bool = False) -> Channel: ...
39+
def set_soundfont(paths: Optional[str] = None) -> None: ...
40+
def get_soundfont() -> Optional[str]: ...
3941
def get_busy() -> bool: ...
4042
def get_sdl_mixer_version(linked: bool = True) -> Tuple[int, int, int]: ...
4143

docs/reST/ref/mixer.rst

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,43 @@ change the default buffer by calling :func:`pygame.mixer.pre_init` before
230230

231231
.. ## pygame.mixer.find_channel ##
232232
233+
.. function:: set_soundfont
234+
235+
| :sl:`set the soundfont for playing midi music`
236+
| :sg:`set_soundfont(path) -> None`
237+
238+
This sets the soundfont file to be used in the playback of midi music.
239+
The soundfont only affects the playback of ``MID``, ``MIDI``, and ``KAR`` file formats.
240+
The optional ``path`` argument, a string (or multiple strings separated by a semi-colon),
241+
must point to the soundfont file(s) to be searched for in order given if some
242+
are missing. If ``path`` is an empty string or the default (``None``), any specified soundfont paths
243+
will be cleared from the mixer.
244+
245+
Note on Windows, the mixer always uses the built-in soundfont instead of the one specified.
246+
247+
Function :func:`set_soundfont` calls underlying SDL_mixer function
248+
``Mix_SetSoundFonts``.
249+
250+
.. versionadded:: 2.3.1
251+
252+
.. ## pygame.mixer.set_soundfont ##
253+
254+
.. function:: get_soundfont
255+
256+
| :sl:`get the soundfont for playing midi music`
257+
| :sg:`get_soundfont() -> paths`
258+
259+
This gets the soundfont filepaths as a string (each path is separated by a semi-colon)
260+
to be used in the playback of ``MID``, ``MIDI``, and ``KAR`` music file formats. If no
261+
soundfont is specified, the return type is ``None``.
262+
263+
Function :func:`get_soundfont` calls underlying SDL_mixer function
264+
``Mix_GetSoundFonts``.
265+
266+
.. versionadded:: 2.3.1
267+
268+
.. ## pygame.mixer.get_soundfont ##
269+
233270
.. function:: get_busy
234271

235272
| :sl:`test if any sound is being mixed`

src_c/_pygame.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,17 @@ supported Python version. #endif */
294294

295295
#define PyType_Init(x) (((x).ob_type) = &PyType_Type)
296296

297+
/* Python macro for comparing to Py_None
298+
* Py_IsNone is naturally supported by
299+
* Python 3.10 or higher
300+
* so this macro can be removed after the minimum
301+
* supported
302+
* Python version reaches 3.10
303+
*/
304+
#ifndef Py_IsNone
305+
#define Py_IsNone(x) (x == Py_None)
306+
#endif
307+
297308
/* Update this function if new sequences are added to the fast sequence
298309
* type. */
299310
#ifndef pgSequenceFast_Check

src_c/doc/mixer_doc.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
#define DOC_MIXER_GETNUMCHANNELS "get_num_channels() -> count\nget the total number of playback channels"
1313
#define DOC_MIXER_SETRESERVED "set_reserved(count) -> count\nreserve channels from being automatically used"
1414
#define DOC_MIXER_FINDCHANNEL "find_channel(force=False) -> Channel\nfind an unused channel"
15+
#define DOC_MIXER_SETSOUNDFONT "set_soundfont(path) -> None\nset the soundfont for playing midi music"
16+
#define DOC_MIXER_GETSOUNDFONT "get_soundfont() -> paths\nget the soundfont for playing midi music"
1517
#define DOC_MIXER_GETBUSY "get_busy() -> bool\ntest if any sound is being mixed"
1618
#define DOC_MIXER_GETSDLMIXERVERSION "get_sdl_mixer_version() -> (major, minor, patch)\nget_sdl_mixer_version(linked=True) -> (major, minor, patch)\nget the mixer's SDL version"
1719
#define DOC_MIXER_SOUND "Sound(filename) -> Sound\nSound(file=filename) -> Sound\nSound(file=pathlib_path) -> Sound\nSound(buffer) -> Sound\nSound(buffer=buffer) -> Sound\nSound(object) -> Sound\nSound(file=object) -> Sound\nSound(array=object) -> Sound\nCreate a new Sound object from a file or buffer object"

src_c/mixer.c

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1460,6 +1460,58 @@ mixer_find_channel(PyObject *self, PyObject *args, PyObject *kwargs)
14601460
return pgChannel_New(chan);
14611461
}
14621462

1463+
static PyObject *
1464+
mixer_set_soundfont(PyObject *self, PyObject *args)
1465+
{
1466+
int paths_set;
1467+
PyObject *path = Py_None;
1468+
const char *string_path = "";
1469+
1470+
if (!PyArg_ParseTuple(args, "|O", &path)) {
1471+
return NULL;
1472+
}
1473+
1474+
MIXER_INIT_CHECK();
1475+
1476+
if (PyUnicode_Check(path)) {
1477+
string_path = PyUnicode_AsUTF8(path);
1478+
}
1479+
else if (!Py_IsNone(path)) {
1480+
PyErr_SetString(PyExc_TypeError,
1481+
"Must pass string or None to set_soundfont");
1482+
return NULL;
1483+
}
1484+
1485+
if (strlen(string_path) == 0) {
1486+
paths_set = Mix_SetSoundFonts(NULL);
1487+
}
1488+
else {
1489+
paths_set = Mix_SetSoundFonts(string_path);
1490+
}
1491+
1492+
if (paths_set == 0) {
1493+
return RAISE(pgExc_SDLError, SDL_GetError());
1494+
}
1495+
1496+
Py_RETURN_NONE;
1497+
}
1498+
1499+
static PyObject *
1500+
mixer_get_soundfont(PyObject *self, PyObject *_null)
1501+
{
1502+
const char *paths;
1503+
1504+
MIXER_INIT_CHECK();
1505+
1506+
paths = Mix_GetSoundFonts();
1507+
1508+
if (paths) {
1509+
return PyUnicode_FromString(paths);
1510+
}
1511+
1512+
Py_RETURN_NONE;
1513+
}
1514+
14631515
static PyObject *
14641516
mixer_fadeout(PyObject *self, PyObject *args)
14651517
{
@@ -1895,6 +1947,10 @@ static PyMethodDef _mixer_methods[] = {
18951947
{"get_busy", (PyCFunction)get_busy, METH_NOARGS, DOC_MIXER_GETBUSY},
18961948
{"find_channel", (PyCFunction)mixer_find_channel,
18971949
METH_VARARGS | METH_KEYWORDS, DOC_MIXER_FINDCHANNEL},
1950+
{"set_soundfont", (PyCFunction)mixer_set_soundfont, METH_VARARGS,
1951+
DOC_MIXER_SETSOUNDFONT},
1952+
{"get_soundfont", (PyCFunction)mixer_get_soundfont, METH_NOARGS,
1953+
DOC_MIXER_GETSOUNDFONT},
18981954
{"fadeout", mixer_fadeout, METH_VARARGS, DOC_MIXER_FADEOUT},
18991955
{"stop", (PyCFunction)mixer_stop, METH_NOARGS, DOC_MIXER_STOP},
19001956
{"pause", (PyCFunction)mixer_pause, METH_NOARGS, DOC_MIXER_PAUSE},

test/mixer_test.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,25 @@ def test_set_num_channels(self):
116116
mixer.set_num_channels(i)
117117
self.assertEqual(mixer.get_num_channels(), i)
118118

119+
def test_set_soundfont(self):
120+
"""Ensure soundfonts can be set, cleared, and retrieved"""
121+
mixer.init()
122+
123+
mixer.set_soundfont()
124+
self.assertEqual(mixer.get_soundfont(), None)
125+
126+
mixer.set_soundfont(None)
127+
self.assertEqual(mixer.get_soundfont(), None)
128+
129+
mixer.set_soundfont("")
130+
self.assertEqual(mixer.get_soundfont(), None)
131+
132+
mixer.set_soundfont("test1.sf2;test2.sf2")
133+
self.assertEqual(mixer.get_soundfont(), "test1.sf2;test2.sf2")
134+
135+
self.assertRaises(TypeError, mixer.set_soundfont, 0)
136+
self.assertRaises(TypeError, mixer.set_soundfont, ["one", "two"])
137+
119138
def test_quit(self):
120139
"""get_num_channels() Should throw pygame.error if uninitialized
121140
after mixer.quit()"""

0 commit comments

Comments
 (0)