nvwave.py 10.9 KB
#nvwave.py
#A part of NonVisual Desktop Access (NVDA)
#Copyright (C) 2006-2008 NVDA Contributors <http://www.nvda-project.org/>
#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()