Pygame
My previous post explained that it’s essential to get even striking of the bells in Sirima, my bell-ringing machine. I am using Pygame to play the sounds, but I was getting dreadful unevenness in timing that was excruciating to listen to. I tried changing the timing algorithm, but this made little difference and after much investigation, I concluded that it was originating in the low-level sound playing system. I had great trouble in resolving this, so I am hoping this blog will be helpful to others.
The pygame mixer function provides a huge amount of control over the sound player, but the documentation is terse, with little in the way of examples, and seems to assume a high level of programming knowledge and insight into how the sound player works. I suspected that the unevenness was to be due to delays in importing the sound from the wav files stored on the Pi’s SD drive, although the documentation warns that having a large buffer can create ‘laggy sound’, whilst having a small buffer can cause scratchy sound and dropouts, whilst not really explaining how the buffer works.
Initialising the mixer
So the first thing I did was to explicitly set the sound mixer as follows:
pygame.mixer.pre_init(44100, -16, 2, 1024)
pygame.init()
pygame.mixer.quit()
pygame.mixer.init(44100, -16, 2, 1024)
What does this do? Standard Microsoft WAV files are strings of bytes recorded at a rate of 44100 samples per second. The second parameter (-16) means that the samples are stored as signed numbers 16 bits long (as standard) and the second shows that there are 2 channels (stereo) and the final parameter is the buffer size. I originally tried a buffer size of 512, which is probably the smallest practicable size. This worked well from the desktop, but was scratchy when run from autoboot. I found a length of 1024 was fine.
I am not sure whether the initialise/de-initialise and re-initialise rigmarole is needed, but some writers did find it was necessary, so I just went with it and haven’t tested that again.
The sound of each of the 12 bells is in a separate WAV file (each between 125 and 250 k long depending on whether it is mono or stereo (which doesn’t seem to make any difference to replay), and lasts about 1.4 seconds. The bells strike at (adjustable) intervals of about 1/3rd of a second, so it takes about 2 seconds for 6 bells to strike. In other words, the repetition rate is about once every 2 seconds.
In order that the sound of one bell doesn’t cut off the sound of the previous bell, I originally put each bell onto a separate channel. I discovered that this was unnecessary because Pygame handles the channel assignment automatically: all I needed to do was to ensure that Pygame had enough channels so that it did not need to cut off one bell sound to play the next (which it does if there are no spare channels). The reverberant part of each sound is essential for a proper sound and in fact can last for several seconds, although the long hum is masked by the following strike. In any case, I could not get explicit channel assignment to work with the sound buffer, although it worked fine when I loaded the wav files each time.
Setting the number of channels
I can have up to 12 bells, so I have set 12 channels for the bells and I use channel 0 for machine announcements. This means that there are 13 channels (it is so easy to forget that numbering starts from 0 in Python!). The command is:
pygame.mixer.set_num_channels(13)
Playing a sound
The simple way to play a sound from a file is with the command:
pygame.mixer.Sound(“/home/pi/Bells/Sound_1.wav”).play()
This is case-sensitive, and don’t forget the () after each function call, otherwise you’ll get a confusing error message.
As you can see, this loads the file each time from the SD card, interprets it and sends it to the low-level sound system which plays it.
I felt sure that this system sometimes took longer than other times, causing unevenness, and it was clearly unnecessary to keep re-loading the same lot of bytes every time a bell sounded.
I noted that sound could be loaded from a ‘buffer object’, rather than directly from the wav file. The terminology is again confusing, because this buffer object is entirely different from the buffer in the init file. Presumably this means the sound data is stored in RAM, but I had a lot of trouble finding how to code this correctly.
Differences in timing running from the desktop vs headless start
One big problem was that sound reproduction when running from the desk top via remote desktop (rdp) differed from running via autoboot. I have already mentioned that the headless start needed a larger buffer, but the sound was louder and clearer than running via rdp. However, rdp running did not have the same problems with timing. It was almost perfect, though perhaps not completely rhythmical when running from the desktop, but sounded dreadfully lumpy when setting it to auto-run on boot-up. This made testing rather difficult, because once I’d got some coding to work promisingly from the desktop, I then had to put it into autostart and reboot, when it might or might not do what I wanted.
This meant that I worked in quite a number of small steps, taking quite a round-about route. Of course, it can now be shortened, but here it is in full, which hopefully makes the procedure clear. Firstly I set up a list called bwav to store the ‘sound objects’ that reference the external wav files, as follows:
bwav = [[‘ ‘] for i in range (13)]
I then loaded the sound objects into the list:
bwav[1] = pygame.mixer.Sound(“/home/pi/Bells/Sound_1.wav”)
bwav[2] = pygame.mixer.Sound(“/home/pi/Bells/Sound_2.wav”)
bwav[3] = pygame.mixer.Sound(“/home/pi/Bells/Sound_3.wav”)
bwav[4] = pygame.mixer.Sound(“/home/pi/Bells/Sound_4.wav”)
bwav[5] = pygame.mixer.Sound(“/home/pi/Bells/Sound_5.wav”)
bwav[6] = pygame.mixer.Sound(“/home/pi/Bells/Sound_6.wav”)
bwav[7] = pygame.mixer.Sound(“/home/pi/Bells/Sound_7.wav”)
bwav[8] = pygame.mixer.Sound(“/home/pi/Bells/Sound_8.wav”)
bwav[9] = pygame.mixer.Sound(“/home/pi/Bells/Sound_9.wav”)
bwav[10]= pygame.mixer.Sound(“/home/pi/Bells/Sound_10.wav”)
bwav now contains a list of ‘sound objects’ rather than mere file references. These contain more than the bytes of the wav file, and are essential for the next step to work.
We now need to extract the raw numerical sound data from the sound objects, so that we can play it. So I set up a new list to contain the raw data:
rawdata = [[‘ ‘] for i in range (13)]
Then for the first 10 bells (I don’t have bells 11 and 12 available at present), I extract the raw data from the Sound object using the get_raw function from the mixer module:
for i in range (10):
rawdata[i+1] = pygame.mixer.Sound.get_raw(bwav[i+1])
Note: it is essential that you only use get_raw on valid sound objects. If you try to extract raw data from one of the empty list elements (by not having the correct range in the for loop) you get another misleading error message!
rawdata now contains the actual sound bytes of each wav file in a form that mixer can play. The raw sound for each bell is stored as an element in the rawdata list. This makes it easy to reference each sound. This list will be stored in RAM so won’t need to be re-loaded from the SD card, and it can be re-used any number of times.
It is now easy to play the sound in the buffer:
pygame.mixer.Sound(buffer=rawdata[bell]).play()
where bell contains the index number of the bell sound to be played. The keyword buffer=rawdata ensures that Sound knows that rawdata contains raw sound rather than a file reference or some such.
This works beautifully well.
Note that I could NOT get the buffer to work with the Channel () command, as I could not get the play() command to accept the buffer information. Maybe there is a fix, but since mixer handles channel assignment, it is not necessary to deal with it.
I have also done some work on the timing algorithm which I will cover in my next blog.