Python is known for its easy integration with C libraries and, indeed, there is a lot of Python bindings calling audio libraries written in C from Python. However, it requires the library to be compiled for your specific platform. I chose to avoid dependencies on any C code and instead call Windows WinMM Multimedia API directly with ctypes module.
Thanks to an excellent tutorial by David Overton, here is the pure Python proof of concept that plays standard CD Audio PCM 44.1kHz 16bit Stereo sample from external file to the default sound device.
"""
Implementation of Raw Audio Socket server spec in pure Python
http://code.google.com/p/rainforce/wiki/RawAudioSocket
"""
import sys
#-- CHAPTER 1: CONTINUOUS SOUND PLAYBACK WITH WINDOWS WINMM LIBRARY --
#
# Based on tutorial "Playing Audio in Windows using waveOut Interface"
# by David Overton
import ctypes
from ctypes import wintypes
# 1. Open Sound Device
# --- define necessary data structures from mmsystem.h
HWAVEOUT = wintypes.HANDLE
WAVE_FORMAT_PCM = 0x1
WAVE_MAPPER = -1
CALLBACK_NULL = 0
MMSYSERR_NOERROR = 0
class WAVEFORMATEX(ctypes.Structure):
_fields_ = [
('wFormatTag', wintypes.WORD),
# 0x0001 WAVE_FORMAT_PCM. PCM audio
# 0xFFFE The format is specified in the WAVEFORMATEXTENSIBLE.SubFormat
# Other values are in mmreg.h
('nChannels', wintypes.WORD),
('SamplesPerSec', wintypes.DWORD),
('AvgBytesPerSec', wintypes.DWORD),
# for WAVE_FORMAT_PCM is the product of nSamplesPerSec and nBlockAlign
('nBlockAlign', wintypes.WORD),
# for WAVE_FORMAT_PCM is the product of nChannels and wBitsPerSample
# divided by 8 (bits per byte)
('wBitsPerSample', wintypes.WORD),
# for WAVE_FORMAT_PCM should be equal to 8 or 16
('cbSize', wintypes.WORD)]
# extra format information size, should be 0
# --- /define
# Data must be processes in pieces that are multiple of
# nBlockAlign bytes of data at a time. Written and read
# data from a device must always start at the beginning
# of a block. Playback of PCM data can not be started in
# the middle of a sample on a non-block-aligned boundary.
hwaveout = HWAVEOUT()
wavefx = WAVEFORMATEX(
WAVE_FORMAT_PCM,
2, # nChannels
44100, # SamplesPerSec
705600,# AvgBytesPerSec = 44100 SamplesPerSec * 16 wBitsPerSample
4, # nBlockAlign = 2 nChannels * 16 wBitsPerSample / 8 bits per byte
16, # wBitsPerSample
0
)
# Open default wave device
ret = ctypes.windll.winmm.waveOutOpen(
ctypes.byref(hwaveout), # buffer to receive a handle identifying
# the open waveform-audio output device
WAVE_MAPPER, # constant to point to default wave device
ctypes.byref(wavefx), # identifier for data format sent for device
0, # DWORD_PTR dwCallback - callback mechanizm
0, # DWORD_PTR dwCallbackInstance - user instance data for callback
CALLBACK_NULL # DWORD fdwOpen - flag for opening the device
)
if ret != MMSYSERR_NOERROR:
sys.exit('Error opening default waveform audio device (WAVE_MAPPER)')
print "Default Wave Audio output device is opened successfully"
# 2. Write Audio Blocks to Device
# --- define necessary data structures
PVOID = wintypes.HANDLE
WAVERR_BASE = 32
WAVERR_STILLPLAYING = WAVERR_BASE + 1
class WAVEHDR(ctypes.Structure):
_fields_ = [
('lpData', wintypes.LPSTR), # pointer to waveform buffer
('dwBufferLength', wintypes.DWORD), # in bytes
('dwBytesRecorded', wintypes.DWORD), # when used in input
('dwUser', wintypes.DWORD), # user data
('dwFlags', wintypes.DWORD),
('dwLoops', wintypes.DWORD), # times to loop, for output buffers only
('lpNext', PVOID), # reserved, struct wavehdr_tag *lpNext
('reserved', wintypes.DWORD)] # reserved
# The lpData, dwBufferLength, and dwFlags members must be set before calling
# the waveInPrepareHeader or waveOutPrepareHeader function. (For either
# function, the dwFlags member must be set to zero.)
# --- /define
class AudioWriter(object):
def __init__(self, hwaveout):
self.hwaveout = hwaveout
self.wavehdr = WAVEHDR()
def write(self, data):
self.wavehdr.dwBufferLength = len(data)
self.wavehdr.lpData = data
# Prepare block for playback
if ctypes.windll.winmm.waveOutPrepareHeader(
self.hwaveout, ctypes.byref(self.wavehdr), ctypes.sizeof(self.wavehdr)
) != MMSYSERR_NOERROR:
sys.exit('Error: waveOutPrepareHeader failed')
# Write block, returns immediately unless a synchronous driver is
# used (not often)
if ctypes.windll.winmm.waveOutWrite(
self.hwaveout, ctypes.byref(self.wavehdr), ctypes.sizeof(self.wavehdr)
) != MMSYSERR_NOERROR:
sys.exit('Error: waveOutWrite failed')
# [ ] calculate sleep delay based on sample length
# iii [ ] Measure CPU usage spike during wait without delay
import time
time.sleep(1)
# Wait until playback is finished
while True:
# unpreparing the header fails until the block is played
ret = ctypes.windll.winmm.waveOutUnprepareHeader(
self.hwaveout,
ctypes.byref(self.wavehdr),
ctypes.sizeof(self.wavehdr)
)
if ret == WAVERR_STILLPLAYING:
import time
time.sleep(1)
continue
if ret != MMSYSERR_NOERROR:
sys.exit('Error: waveOutUnprepareHeader failed with code 0x%x' % ret)
break
# [ ] it's no good to read all the PCM data into memory at once
data = open('95672__Corsica_S__frequency_change_approved.raw', 'rb').read()
aw = AudioWriter(hwaveout)
aw.write(data)
# x. Close Sound Device
ctypes.windll.winmm.waveOutClose(hwaveout)
print "Default Wave Audio output device is closed"
#-- /CHAPTER 1 --
Windows also provides DirectSound API, but it looks too complicated for me ATM. The code above is also available from https://bitbucket.org/techtonik/audiosocket and marked with 0.1 tag. You may expect to find further modifications there.
No comments:
Post a Comment