-
Notifications
You must be signed in to change notification settings - Fork 116
/
playsound.py
244 lines (204 loc) · 8.69 KB
/
playsound.py
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
import logging
logger = logging.getLogger(__name__)
class PlaysoundException(Exception):
pass
def _canonicalizePath(path):
"""
Support passing in a pathlib.Path-like object by converting to str.
"""
import sys
if sys.version_info[0] >= 3:
return str(path)
else:
# On earlier Python versions, str is a byte string, so attempting to
# convert a unicode string to str will fail. Leave it alone in this case.
return path
def _playsoundWin(sound, block = True):
'''
Utilizes windll.winmm. Tested and known to work with MP3 and WAVE on
Windows 7 with Python 2.7. Probably works with more file formats.
Probably works on Windows XP thru Windows 10. Probably works with all
versions of Python.
Inspired by (but not copied from) Michael Gundlach <[email protected]>'s mp3play:
https://github.com/michaelgundlach/mp3play
I never would have tried using windll.winmm without seeing his code.
'''
sound = '"' + _canonicalizePath(sound) + '"'
from ctypes import create_unicode_buffer, windll, wintypes
from time import sleep
windll.winmm.mciSendStringW.argtypes = [wintypes.LPCWSTR, wintypes.LPWSTR, wintypes.UINT, wintypes.HANDLE]
windll.winmm.mciGetErrorStringW.argtypes = [wintypes.DWORD, wintypes.LPWSTR, wintypes.UINT]
def winCommand(*command):
bufLen = 600
buf = create_unicode_buffer(bufLen)
command = ' '.join(command)
errorCode = int(windll.winmm.mciSendStringW(command, buf, bufLen - 1, 0)) # use widestring version of the function
if errorCode:
errorBuffer = create_unicode_buffer(bufLen)
windll.winmm.mciGetErrorStringW(errorCode, errorBuffer, bufLen - 1) # use widestring version of the function
exceptionMessage = ('\n Error ' + str(errorCode) + ' for command:'
'\n ' + command +
'\n ' + errorBuffer.value)
logger.error(exceptionMessage)
raise PlaysoundException(exceptionMessage)
return buf.value
try:
logger.debug('Starting')
winCommand(u'open {}'.format(sound))
winCommand(u'play {}{}'.format(sound, ' wait' if block else ''))
logger.debug('Returning')
finally:
try:
winCommand(u'close {}'.format(sound))
except PlaysoundException:
logger.warning(u'Failed to close the file: {}'.format(sound))
# If it fails, there's nothing more that can be done...
pass
def _handlePathOSX(sound):
sound = _canonicalizePath(sound)
if '://' not in sound:
if not sound.startswith('/'):
from os import getcwd
sound = getcwd() + '/' + sound
sound = 'file://' + sound
try:
# Don't double-encode it.
sound.encode('ascii')
return sound.replace(' ', '%20')
except UnicodeEncodeError:
try:
from urllib.parse import quote # Try the Python 3 import first...
except ImportError:
from urllib import quote # Try using the Python 2 import before giving up entirely...
parts = sound.split('://', 1)
return parts[0] + '://' + quote(parts[1].encode('utf-8')).replace(' ', '%20')
def _playsoundOSX(sound, block = True):
'''
Utilizes AppKit.NSSound. Tested and known to work with MP3 and WAVE on
OS X 10.11 with Python 2.7. Probably works with anything QuickTime supports.
Probably works on OS X 10.5 and newer. Probably works with all versions of
Python.
Inspired by (but not copied from) Aaron's Stack Overflow answer here:
http://stackoverflow.com/a/34568298/901641
I never would have tried using AppKit.NSSound without seeing his code.
'''
try:
from AppKit import NSSound
except ImportError:
logger.warning("playsound could not find a copy of AppKit - falling back to using macOS's system copy.")
sys.path.append('/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/PyObjC')
from AppKit import NSSound
from Foundation import NSURL
from time import sleep
sound = _handlePathOSX(sound)
url = NSURL.URLWithString_(sound)
if not url:
raise PlaysoundException('Cannot find a sound with filename: ' + sound)
for i in range(5):
nssound = NSSound.alloc().initWithContentsOfURL_byReference_(url, True)
if nssound:
break
else:
logger.debug('Failed to load sound, although url was good... ' + sound)
else:
raise PlaysoundException('Could not load sound with filename, although URL was good... ' + sound)
nssound.play()
if block:
sleep(nssound.duration())
def _playsoundNix(sound, block = True):
"""Play a sound using GStreamer.
Inspired by this:
https://gstreamer.freedesktop.org/documentation/tutorials/playback/playbin-usage.html
"""
sound = _canonicalizePath(sound)
# pathname2url escapes non-URL-safe characters
from os.path import abspath, exists
try:
from urllib.request import pathname2url
except ImportError:
# python 2
from urllib import pathname2url
import gi
gi.require_version('Gst', '1.0')
from gi.repository import Gst
Gst.init(None)
playbin = Gst.ElementFactory.make('playbin', 'playbin')
if sound.startswith(('http://', 'https://')):
playbin.props.uri = sound
else:
path = abspath(sound)
if not exists(path):
raise PlaysoundException(u'File not found: {}'.format(path))
playbin.props.uri = 'file://' + pathname2url(path)
set_result = playbin.set_state(Gst.State.PLAYING)
if set_result != Gst.StateChangeReturn.ASYNC:
raise PlaysoundException(
"playbin.set_state returned " + repr(set_result))
# FIXME: use some other bus method than poll() with block=False
# https://lazka.github.io/pgi-docs/#Gst-1.0/classes/Bus.html
logger.debug('Starting play')
if block:
bus = playbin.get_bus()
try:
bus.poll(Gst.MessageType.EOS, Gst.CLOCK_TIME_NONE)
finally:
playbin.set_state(Gst.State.NULL)
logger.debug('Finishing play')
def _playsoundAnotherPython(otherPython, sound, block = True, macOS = False):
'''
Mostly written so that when this is run on python3 on macOS, it can invoke
python2 on macOS... but maybe this idea could be useful on linux, too.
'''
from inspect import getsourcefile
from os.path import abspath, exists
from subprocess import check_call
from threading import Thread
sound = _canonicalizePath(sound)
class PropogatingThread(Thread):
def run(self):
self.exc = None
try:
self.ret = self._target(*self._args, **self._kwargs)
except BaseException as e:
self.exc = e
def join(self, timeout = None):
super().join(timeout)
if self.exc:
raise self.exc
return self.ret
# Check if the file exists...
if not exists(abspath(sound)):
raise PlaysoundException('Cannot find a sound with filename: ' + sound)
playsoundPath = abspath(getsourcefile(lambda: 0))
t = PropogatingThread(target = lambda: check_call([otherPython, playsoundPath, _handlePathOSX(sound) if macOS else sound]))
t.start()
if block:
t.join()
from platform import system
system = system()
if system == 'Windows':
playsound = _playsoundWin
elif system == 'Darwin':
playsound = _playsoundOSX
import sys
if sys.version_info[0] > 2:
try:
from AppKit import NSSound
except ImportError:
logger.warning("playsound is relying on a python 2 subprocess. Please use `pip3 install PyObjC` if you want playsound to run more efficiently.")
playsound = lambda sound, block = True: _playsoundAnotherPython('/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python', sound, block, macOS = True)
else:
playsound = _playsoundNix
if __name__ != '__main__': # Ensure we don't infinitely recurse trying to get another python instance.
try:
import gi
gi.require_version('Gst', '1.0')
from gi.repository import Gst
except:
logger.warning("playsound is relying on another python subprocess. Please use `pip install pygobject` if you want playsound to run more efficiently.")
playsound = lambda sound, block = True: _playsoundAnotherPython('/usr/bin/python3', sound, block, macOS = False)
del system
if __name__ == '__main__':
# block is always True if you choose to run this from the command line.
from sys import argv
playsound(argv[1])