The Python wiki lists quite a few packages for working with audio, but most of them are overkill for basic audio recording and playback.
For quite some time I had been using PyAudio, which adds Python bindings to the PortAudio project. I really like it because it focuses entirely on recording and playing audio. But, for some reason, when I recently upgraded to Mavericks, it stutters whenever I try to play samples at a sample rate lower than 44.1 KHz. I've emailed the author to try to get to the bottom of it.
In the meantime, I tried a new package, PySDL2, which adds Python bindings to the SDL2 (Simple Directmedia Layer) project.
SDL2 does quite a bit more than basic audio, and I didn't dig into any of that yet. I hit one small issue with PySDL2, but the one-line change in the issue fixes it. Here's the resulting 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
import sdl2 import sys import aifc import threading class ReadAIFF: def __init__(self, fileName): self.a = aifc.open(fileName) self.frameUpto = 0 self.bytesPerFrame = self.a.getnchannels() * self.a.getsampwidth() self.numFrames = self.a.getnframes() self.done = threading.Event() def playNextChunk(self, unused, buf, bufSize): framesInBuffer = bufSize/self.bytesPerFrame framesToRead = min(framesInBuffer, self.numFrames-self.frameUpto) if self.frameUpto == self.numFrames: self.done.set() # TODO: is there a faster way to copy the string into the ctypes # pointer/array? for i, b in enumerate(self.a.readframes(framesToRead)): buf[i] = ord(b) # Play silence after: # TODO: is there a faster way to zero out the array? for i in range(self.bytesPerFrame*framesToRead, self.bytesPerFrame*framesInBuffer): buf[i] = 0 self.frameUpto += framesToRead if sdl2.SDL_Init(sdl2.SDL_INIT_AUDIO) != 0: raise RuntimeError('failed to init audio') p = ReadAIFF(sys.argv) spec = sdl2.SDL_AudioSpec(p.a.getframerate(), sdl2.AUDIO_S16MSB, p.a.getnchannels(), 512, sdl2.SDL_AudioCallback(p.playNextChunk)) # TODO: instead of passing None for the 4th arg, I really should pass # another AudioSpec and then confirm it matched what I asked for: devID = sdl2.SDL_OpenAudioDevice(None, 0, spec, None, 0) if devID == 0: raise RuntimeError('failed to open audio device') # Tell audio device to start playing: sdl2.SDL_PauseAudioDevice(devID, 0) # Wait until all samples are done playing p.done.wait() sdl2.SDL_CloseAudioDevice(devID)
The code is straightforward: it loads an AIFF file, using Python's builtin
aifcmodule, and then creates a callback,
playNextChunkwhich is invoked by
PySDL2when it needs more samples to play. So far it seems to work very well!