#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. """ import threading from ctypes import * from ctypes.wintypes import * import time import wx import winKernel import wave import config from logHandler import log __all__ = ( "WavePlayer", "getOutputDeviceNames", "outputDeviceIDToName", "outputDeviceNameToID", ) winmm = windll.winmm 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) # Initialize 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}. """ #: A lock to prevent WaveOut* functions from being called simultaneously, as this can cause problems even if they are for different HWAVEOUTs. _global_waveout_lock = threading.RLock() _audioDucker=None def __init__(self, channels, samplesPerSec, bitsPerSample, outputDevice=WAVE_MAPPER, closeWhenIdle=True,wantDucking=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 @param wantDucking: if true then background audio will be ducked on Windows 8 and higher @type wantDucking: 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 wantDucking: import audioDucking if audioDucking.isAudioDuckingSupported(): self._audioDucker=audioDucking.AudioDucker() #: If C{True}, close the output device when no audio is being played. #: @type: bool self.closeWhenIdle = closeWhenIdle self._waveout = None self._waveout_event = winKernel.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) with self._global_waveout_lock: 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. """ if self._audioDucker and not self._audioDucker.enable(): return whdr = WAVEHDR() whdr.lpData = data whdr.dwBufferLength = len(data) with self._lock: with self._waveout_lock: self.open() with self._global_waveout_lock: winmm.waveOutPrepareHeader(self._waveout, LPWAVEHDR(whdr), sizeof(WAVEHDR)) try: with self._global_waveout_lock: 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): winKernel.waitForSingleObject(self._waveout_event, winKernel.INFINITE) with self._waveout_lock: assert self._waveout, "waveOut None after wait" with self._global_waveout_lock: 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 """ if self._audioDucker and self._waveout: if switch: self._audioDucker.disable() else: self._audioDucker.enable() with self._waveout_lock: if not self._waveout: return if switch: with self._global_waveout_lock: winmm.waveOutPause(self._waveout) else: with self._global_waveout_lock: 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() if self._audioDucker: self._audioDucker.disable() def stop(self): """Stop playback. """ if self._audioDucker: self._audioDucker.disable() with self._waveout_lock: if not self._waveout: return try: with self._global_waveout_lock: # 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): with self._global_waveout_lock: winmm.waveOutClose(self._waveout) self._waveout = None def __del__(self): self.close() winKernel.kernel32.CloseHandle(self._waveout_event) self._waveout_event = None def _getOutputDevices(): caps = WAVEOUTCAPS() for devID in xrange(-1, winmm.waveOutGetNumDevs()): try: 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: 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") fileWavePlayer = None fileWavePlayerThread=None def playWaveFile(fileName, async=True): """plays a specified wave file. """ global fileWavePlayer, fileWavePlayerThread f = wave.open(fileName,"r") if f is None: raise RuntimeError("can not open file %s"%fileName) if fileWavePlayer is not None: fileWavePlayer.stop() fileWavePlayer = WavePlayer(channels=f.getnchannels(), samplesPerSec=f.getframerate(),bitsPerSample=f.getsampwidth()*8, outputDevice=config.conf["speech"]["outputDevice"],wantDucking=False) fileWavePlayer.feed(f.readframes(f.getnframes())) if async: if fileWavePlayerThread is not None: fileWavePlayerThread.join() fileWavePlayerThread=threading.Thread(target=fileWavePlayer.idle) fileWavePlayerThread.start() else: fileWavePlayer.idle()