#nvwave.py #A part of NonVisual Desktop Access (NVDA) #Copyright (C) 2006-2008 NVDA Contributors #This file is covered by the GNU General Public License. #See the file COPYING for more details. """Provides a simple Python interface to playing audio using the Windows multimedia waveOut functions, as well as other useful utilities. """ from __future__ import with_statement import threading from ctypes import * from ctypes.wintypes import * __all__ = ( "WavePlayer", "getOutputDeviceNames", "outputDeviceIDToName", "outputDeviceNameToID", ) winmm = windll.winmm kernel32 = windll.kernel32 INFINITE = 0xffffffff HWAVEOUT = HANDLE LPHWAVEOUT = POINTER(HWAVEOUT) class WAVEFORMATEX(Structure): _fields_ = [ ("wFormatTag", WORD), ("nChannels", WORD), ("nSamplesPerSec", DWORD), ("nAvgBytesPerSec", DWORD), ("nBlockAlign", WORD), ("wBitsPerSample", WORD), ("cbSize", WORD) ] LPWAVEFORMATEX = POINTER(WAVEFORMATEX) class WAVEHDR(Structure): pass LPWAVEHDR = POINTER(WAVEHDR) WAVEHDR._fields_ = [ ("lpData", LPSTR), ("dwBufferLength", DWORD), ("dwBytesRecorded", DWORD), ("dwUser", DWORD), ("dwFlags", DWORD), ("dwLoops", DWORD), ("lpNext", LPWAVEHDR), ("reserved", DWORD) ] WHDR_DONE = 1 WAVE_FORMAT_PCM = 1 WAVE_MAPPER = -1 MMSYSERR_NOERROR = 0 CALLBACK_NULL = 0 #CALLBACK_FUNCTION = 0x30000 CALLBACK_EVENT = 0x50000 #waveOutProc = CFUNCTYPE(HANDLE, UINT, DWORD, DWORD, DWORD) #WOM_DONE = 0x3bd MAXPNAMELEN = 32 class WAVEOUTCAPS(Structure): _fields_ = [ ('wMid', WORD), ('wPid', WORD), ('vDriverVersion', c_uint), ('szPname', WCHAR*MAXPNAMELEN), ('dwFormats', DWORD), ('wChannels', WORD), ('wReserved1', WORD), ('dwSupport', DWORD), ] # Set argument types. winmm.waveOutOpen.argtypes = (LPHWAVEOUT, UINT, LPWAVEFORMATEX, DWORD, DWORD, DWORD) # Initialise error checking. def _winmm_errcheck(res, func, args): if res != MMSYSERR_NOERROR: buf = create_unicode_buffer(256) winmm.waveOutGetErrorTextW(res, buf, sizeof(buf)) raise WindowsError(res, buf.value) for func in ( winmm.waveOutOpen, winmm.waveOutPrepareHeader, winmm.waveOutWrite, winmm.waveOutUnprepareHeader, winmm.waveOutPause, winmm.waveOutRestart, winmm.waveOutReset, winmm.waveOutClose, winmm.waveOutGetDevCapsW ): func.errcheck = _winmm_errcheck class WavePlayer(object): """Synchronously play a stream of audio. To use, construct an instance and feed it waveform audio using L{feed}. """ def __init__(self, channels, samplesPerSec, bitsPerSample, outputDevice=WAVE_MAPPER, closeWhenIdle=True): """Constructor. @param channels: The number of channels of audio; e.g. 2 for stereo, 1 for mono. @type channels: int @param samplesPerSec: Samples per second (hz). @type samplesPerSec: int @param bitsPerSample: The number of bits per sample. @type bitsPerSample: int @param outputDevice: The device ID or name of the audio output device to use. @type outputDevice: int or basestring @param closeWhenIdle: If C{True}, close the output device when no audio is being played. @type closeWhenIdle: bool @note: If C{outputDevice} is a name and no such device exists, the default device will be used. @raise WindowsError: If there was an error opening the audio output device. """ self.channels=channels self.samplesPerSec=samplesPerSec self.bitsPerSample=bitsPerSample if isinstance(outputDevice, basestring): outputDevice = outputDeviceNameToID(outputDevice, True) self.outputDeviceID = outputDevice #: If C{True}, close the output device when no audio is being played. #: @type: bool self.closeWhenIdle = closeWhenIdle self._waveout = None self._waveout_event = kernel32.CreateEventW(None, False, False, None) self._waveout_lock = threading.RLock() self._lock = threading.RLock() self.open() def open(self): """Open the output device. This will be called automatically when required. It is not an error if the output device is already open. """ with self._waveout_lock: if self._waveout: return wfx = WAVEFORMATEX() wfx.wFormatTag = WAVE_FORMAT_PCM wfx.nChannels = self.channels wfx.nSamplesPerSec = self.samplesPerSec wfx.wBitsPerSample = self.bitsPerSample wfx.nBlockAlign = self.bitsPerSample / 8 * self.channels wfx.nAvgBytesPerSec = self.samplesPerSec * wfx.nBlockAlign waveout = HWAVEOUT(0) winmm.waveOutOpen(byref(waveout), self.outputDeviceID, LPWAVEFORMATEX(wfx), self._waveout_event, 0, CALLBACK_EVENT) self._waveout = waveout.value self._prev_whdr = None def feed(self, data): """Feed a chunk of audio data to be played. This is normally synchronous. However, synchronisation occurs on the previous chunk, rather than the current chunk; i.e. calling this while no audio is playing will begin playing the chunk but return immediately. This allows for uninterrupted playback as long as a new chunk is fed before the previous chunk has finished playing. @param data: Waveform audio in the format specified when this instance was constructed. @type data: str @raise WindowsError: If there was an error playing the audio. """ whdr = WAVEHDR() whdr.lpData = data whdr.dwBufferLength = len(data) with self._lock: with self._waveout_lock: self.open() winmm.waveOutPrepareHeader(self._waveout, LPWAVEHDR(whdr), sizeof(WAVEHDR)) try: winmm.waveOutWrite(self._waveout, LPWAVEHDR(whdr), sizeof(WAVEHDR)) except WindowsError, e: self.close() raise e self.sync() self._prev_whdr = whdr def sync(self): """Synchronise with playback. This method blocks until the previously fed chunk of audio has finished playing. It is called automatically by L{feed}, so usually need not be called directly by the user. """ with self._lock: if not self._prev_whdr: return assert self._waveout, "waveOut None before wait" while not (self._prev_whdr.dwFlags & WHDR_DONE): kernel32.WaitForSingleObject(self._waveout_event, INFINITE) with self._waveout_lock: assert self._waveout, "waveOut None after wait" winmm.waveOutUnprepareHeader(self._waveout, LPWAVEHDR(self._prev_whdr), sizeof(WAVEHDR)) self._prev_whdr = None def pause(self, switch): """Pause or unpause playback. @param switch: C{True} to pause playback, C{False} to unpause. @type switch: bool """ with self._waveout_lock: if not self._waveout: return if switch: winmm.waveOutPause(self._waveout) else: winmm.waveOutRestart(self._waveout) def idle(self): """Indicate that this player is now idle; i.e. the current continuous segment of audio is complete. This will first call L{sync} to synchronise with playback. If L{closeWhenIdle} is C{True}, the output device will be closed. A subsequent call to L{feed} will reopen it. """ with self._lock: self.sync() with self._waveout_lock: if not self._waveout: return if self.closeWhenIdle: self._close() def stop(self): """Stop playback. """ with self._waveout_lock: if not self._waveout: return try: # Pausing first seems to make waveOutReset respond faster on some systems. winmm.waveOutPause(self._waveout) winmm.waveOutReset(self._waveout) except WindowsError: # waveOutReset seems to fail randomly on some systems. pass # Unprepare the previous buffer and close the output device if appropriate. self.idle() def close(self): """Close the output device. """ self.stop() with self._lock: with self._waveout_lock: if not self._waveout: return self._close() def _close(self): winmm.waveOutClose(self._waveout) self._waveout = None def __del__(self): self.close() kernel32.CloseHandle(self._waveout_event) self._waveout_event = None def _getOutputDevices(): caps = WAVEOUTCAPS() for devID in xrange(-1, winmm.waveOutGetNumDevs()): try: windll.winmm.waveOutGetDevCapsW(devID, byref(caps), sizeof(caps)) yield devID, caps.szPname except WindowsError: # It seems that in certain cases, Windows includes devices which cannot be accessed. pass def getOutputDeviceNames(): """Obtain the names of all audio output devices on the system. @return: The names of all output devices on the system. @rtype: [str, ...] """ return [name for ID, name in _getOutputDevices()] def outputDeviceIDToName(ID): """Obtain the name of an output device given its device ID. @param ID: The device ID. @type ID: int @return: The device name. @rtype: str """ caps = WAVEOUTCAPS() try: windll.winmm.waveOutGetDevCapsW(ID, byref(caps), sizeof(caps)) except WindowsError: raise LookupError("No such device ID") return caps.szPname def outputDeviceNameToID(name, useDefaultIfInvalid=False): """Obtain the device ID of an output device given its name. @param name: The device name. @type name: str @param useDefaultIfInvalid: C{True} to use the default device (wave mapper) if there is no such device, C{False} to raise an exception. @return: The device ID. @rtype: int @raise LookupError: If there is no such device and C{useDefaultIfInvalid} is C{False}. """ for curID, curName in _getOutputDevices(): if curName == name: return curID # No such ID. if useDefaultIfInvalid: return WAVE_MAPPER else: raise LookupError("No such device name")