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[1])
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
aifc
module, and then creates a
callback, playNextChunk
which is invoked
by PySDL2
when it needs more samples to play. So far it
seems to work very well!
This comment has been removed by the author.
ReplyDeleteHi Mike,
DeleteYou need the patch that I put on the "one small issue" (look at the link above), or, you can upgrade to the latest (not yet released I think) PySDL2 which has fixed this issue. Finally, you can use ctypes to do the cast (I haven't tried this but it should work).