PySDL2: Playing a sound from a WAV file

3 minute read

For the last couple of weeks I have been playing around with PySDL2 to learn a bit of game programming. Figuring that prototyping in Python was a good place to start learning, I originally planned to use Pygame which is a popular wrapper and extension of SDL. Now it seems that SDL (and Pygame) has been discontinued and PySDL2 looks to be the successor.

So far, using PySDL2 has been both easy and fun to create graphics and move them around, especially when using the extensions methods in sdl2.ext. But when adding sound to the mix, I hit upon a bit of trouble. For playing sounds it is necessary to use the SDL2 library functions. I decided to use a WAVE file (.wav) for my sound testing which meant using SDL_LoadWav to load the file. PySDL2 uses a ctypes wrapper around the SDL2 library so that you can use the methods in almost the exact same way as from C code. This is both good and bad. The bad, at least for me, was that I had very little experience with ctypes and spent a lot of time figuring out how to declare the variables and types to use in the function calls. PySDL2 offer very little documentation on how to do it, and the SDL2 documentation doesn’t have too much example code on using SDL_LoadWav with SDL_OpenAudio. After a lot of trying and failing I finally figured out a way that worked.

I also hit on a bug in the PySDL2 wrapper: SDL_LoadWav is a macro that calls SDL_LoadWav_RW wrapping the RWOPS part. What I found was that the PySDL2 macro uses wrong encoding of the mode string in the SDL_RWFromFile call which makes SDL_LoadWav fail. To make it work I had to call SDL_LoadWav_RW and SDL_RWFromFile myself as you see below. Also note the use of the compatibility method byteify() to convert the file name string to the encoding that SDL_RWFromFile expects.

Here is the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
from ctypes import *

try:
    from sdl2 import *
    from sdl2.ext import Resources
    from sdl2.ext.compat import byteify
except ImportError:
    import traceback
    traceback.print_exc()
    sys.exit(1)

RESOURCES = Resources(__file__, "assets")


class WavSound(object):
    def __init__(self, file):
        super(WavSound, self).__init__()
        self._buf = POINTER(Uint8)()
        self._length = Uint32()
        self._bufpos = 0
        self.spec = SDL_AudioSpec(0, 0, 0, 0)
        self._load_file(file)
        self.spec.callback = SDL_AudioCallback(self._play_next)
        self.done = False

    def __del__(self):
        SDL_FreeWAV(self._buf)

    def _load_file(self, file):
        rw = SDL_RWFromFile(byteify(file, "utf-8"), b"rb")
        sp = SDL_LoadWAV_RW(rw, 1, byref(self.spec), byref(self._buf), byref(self._length))
        if sp is None:
            raise RuntimeError("Could not open audio file: {}".format(SDL_GetError()))

    def _play_next(self, notused, stream, len):
        length = self._length.value
        numbytes = min(len, length - self._bufpos)
        for i in range(0, numbytes):
            stream[i] = self._buf[self._bufpos + i]
        self._bufpos += numbytes

        # If not enough bytes in buffer, add silence
        rest = min(0, len - numbytes)
        for i in range(0, rest):
            stream[i] = 0

        # Are we done playing sound?
        if self._bufpos == length:
            self.done = True


def main():
    if SDL_Init(SDL_INIT_AUDIO) != 0:
        raise RuntimeError("Cannot initialize audio system: {}".format(SDL_GetError()))

    sound_file = RESOURCES.get_path("tada.wav")
    sound = WavSound(sound_file)
    devid = SDL_OpenAudioDevice(None, 0, sound.spec, None, 0)
    if devid == 0:
        raise RuntimeError("Unable to open audio device: {}".format(SDL_GetError()))

    SDL_PauseAudioDevice(devid, 0)
    while not sound.done:
        SDL_Delay(100)
    SDL_CloseAudioDevice(devid)

    SDL_Quit(SDL_INIT_AUDIO)


if __name__ == '__main__':
    main()

I realize that I could have used SDL2_mixer for probably an easier way to do this, but I found this an interesting learning experience and worth sharing.

Update: I tried SDL2_mixer and it was much easier.