source: src/mlx/sound.py@ 970:440095aa63dc

python3
Last change on this file since 970:440095aa63dc was 961:fd26a8f057b1, checked in by István Váradi <ivaradi@…>, 5 years ago

Made MCI access compatible with Python 3 (re #347)

File size: 14.9 KB
Line 
1
2from .util import utf2unicode
3
4import os
5import traceback
6
7#------------------------------------------------------------------------------
8
9## @package mlx.sound
10#
11# Sound playback handling.
12#
13# This is the low level sound playback handling. The \ref initializeSound
14# function should be called to initialize the sound handling with the directory
15# containing the sound files. Then the \ref startSound function should be
16# called to start the playback of a certain sound file. A callback may be
17# called when the playback of a certain file has finished.
18#
19# See also the \ref mlx.soundsched module.
20
21#------------------------------------------------------------------------------
22
23if os.name=="nt":
24 import time
25 import threading
26 from ctypes import windll, c_buffer
27
28 class MCIException(Exception):
29 """MCI exception."""
30 def __init__(self, mci, command, errorCode):
31 """Construct an MCI exception for the given error code."""
32 message = "MCI error: %s: %s" % (command, mci.getErrorString(errorCode))
33 super(MCIException, self).__init__(message)
34
35 class MCI:
36 """Interface for the Media Control Interface."""
37 def __init__(self):
38 """Construct the interface."""
39 self.w32mci = windll.winmm.mciSendStringA
40 self.w32mcierror = windll.winmm.mciGetErrorStringA
41
42 def send(self, command):
43 """Send the given command to the MCI."""
44 buffer = c_buffer(255)
45 errorCode = self.w32mci(str(command).encode(), buffer, 254, 0)
46 if errorCode:
47 raise MCIException(self, command, errorCode)
48 else:
49 return buffer.value
50
51 def getErrorString(self, errorCode):
52 """Get the string representation of the given error code."""
53 buffer = c_buffer(255)
54 self.w32mcierror(int(errorCode), buffer, 254)
55 return buffer.value
56
57 class SoundThread(threading.Thread):
58 """The thread controlling the playback of sounds."""
59 def __init__(self, soundsDirectory):
60 threading.Thread.__init__(self)
61
62 self._soundsDirectory = soundsDirectory
63 self._mci = MCI()
64
65 self._requestCondition = threading.Condition()
66 self._requests = []
67 self._pending = []
68 self._count = 0
69
70 self.daemon = True
71
72 def requestSound(self, name, finishCallback = None, extra = None):
73 """Request the playback of the sound with the given name."""
74 path = name if os.path.isabs(name) \
75 else os.path.join(self._soundsDirectory, name)
76 with self._requestCondition:
77 self._requests.append((path, (finishCallback, extra)))
78 self._requestCondition.notify()
79
80 def run(self):
81 """Perform the operation of the thread.
82
83 It waits for a request or a timeout. If a request is received, that
84 is started to be played back. If a timeout occurs, the file is
85 closed."""
86
87 while True:
88 with self._requestCondition:
89 if not self._requests:
90 if self._pending:
91 timeout = max(self._pending[0][0] - time.time(),
92 0.0)
93 else:
94 timeout = 10.0
95
96 #print "Waiting", timeout
97 self._requestCondition.wait(timeout)
98
99 requests = []
100 for (path, finishData) in self._requests:
101 requests.append((path, finishData, self._count))
102 self._count += 1
103 self._requests = []
104
105 now = time.time()
106 toClose = []
107 while self._pending and \
108 self._pending[0][0]<=now:
109 toClose.append(self._pending[0][1])
110 del self._pending[0]
111
112 for (alias, (finishCallback, extra)) in toClose:
113 success = True
114 try:
115 print("Closing", alias)
116 self._mci.send("close " + alias)
117 print("Closed", alias)
118 except Exception as e:
119 print("Failed closing " + alias + ":", end=' ')
120 print(utf2unicode(str(e)))
121 success = False
122
123 if finishCallback is not None:
124 try:
125 finishCallback(success, extra)
126 except:
127 traceback.print_exc()
128
129 for (path, finishData, counter) in requests:
130 try:
131 alias = "mlxsound%d" % (counter,)
132 print("Starting to play", path, "as", alias)
133 self._mci.send("open \"%s\" alias %s" % \
134 (path, alias))
135 self._mci.send("set %s time format milliseconds" % \
136 (alias,))
137 lengthBuffer = self._mci.send("status %s length" % \
138 (alias,))
139 lengthBuffer = str(lengthBuffer, "ascii")
140 self._mci.send("play %s from 0 to %s" % \
141 (alias, lengthBuffer))
142 length = int(lengthBuffer)
143 timeout = time.time() + length / 1000.0
144 with self._requestCondition:
145 self._pending.append((timeout, (alias, finishData)))
146 self._pending.sort()
147 print("Started to play", path)
148 except Exception as e:
149 print("Failed to start playing " + path + ":", end=' ')
150 print(utf2unicode(str(e)))
151 (finishCallback, extra) = finishData
152 if finishCallback is not None:
153 try:
154 finishCallback(None, extra)
155 except:
156 traceback.print_exc()
157
158 _thread = None
159
160 def preInitializeSound():
161 """Perform any-pre initialization.
162
163 This does nothing on Windows."""
164
165 def initializeSound(soundsDirectory):
166 """Initialize the sound handling with the given directory containing
167 the sound files."""
168 global _thread
169 _thread = SoundThread(soundsDirectory)
170 _thread.start()
171
172 def startSound(name, finishCallback = None, extra = None):
173 """Start playing back the given sound.
174
175 name should be the name of a sound file relative to the sound directory
176 given in initializeSound."""
177 _thread.requestSound(name, finishCallback = finishCallback,
178 extra = extra)
179
180 def finalizeSound():
181 """Finalize the sound handling."""
182 pass
183
184#------------------------------------------------------------------------------
185
186else: # os.name!="nt"
187 from multiprocessing import Process, Queue
188 from threading import Thread, Lock
189
190 COMMAND_STARTSOUND = 1
191 COMMAND_QUIT = 2
192
193 REPLY_FINISHED = 101
194 REPLY_FAILED = 102
195 REPLY_QUIT = 103
196
197 _initialized = False
198 _process = None
199 _thread = None
200 _inQueue = None
201 _outQueue = None
202 _nextReference = 1
203 _ref2Data = {}
204 _lock = Lock()
205
206 def _processFn(inQueue, outQueue):
207 """The function running in the helper process created.
208
209 It tries to load the Gst module. If successful, True is sent back,
210 otherwise False followed by the exception caught and the function
211 quits.
212
213 In case of successful initialization, the directory of the sound files
214 is read, the command reader thread is created and the gobject main loop
215 is executed."""
216 try:
217 import gi.repository
218 gi.require_version("Gst", "1.0")
219 from gi.repository import Gst
220 from gi.repository import GObject as gobject
221
222 Gst.init(None)
223 except Exception as e:
224 outQueue.put(False)
225 outQueue.put(e)
226 return
227
228 outQueue.put(True)
229
230 soundsDirectory = inQueue.get()
231
232 _bins = set()
233
234 mainLoop = None
235
236 def _handlePlayBinMessage(bus, message, bin, reference):
237 """Handle messages related to a playback."""
238 if bin in _bins:
239 if message.type==Gst.MessageType.EOS:
240 _bins.remove(bin)
241 if reference is not None:
242 outQueue.put((REPLY_FINISHED, (reference,)))
243 elif message.type==Gst.MessageType.ERROR:
244 _bins.remove(bin)
245 if reference is not None:
246 outQueue.put((REPLY_FAILED, (reference,)))
247
248 def _handleCommand(command, args):
249 """Handle commands sent to the server."""
250 if command==COMMAND_STARTSOUND:
251 (name, reference) = args
252 try:
253 playBin = Gst.ElementFactory.make("playbin", "player")
254
255 bus = playBin.get_bus()
256 bus.add_signal_watch()
257 bus.connect("message", _handlePlayBinMessage,
258 playBin, reference)
259
260 path = os.path.join(soundsDirectory, name)
261 playBin.set_property( "uri", "file://%s" % (path,))
262
263 playBin.set_state(Gst.State.PLAYING)
264 _bins.add(playBin)
265 except Exception as e:
266 if reference is not None:
267 outQueue.put((REPLY_FAILED, (reference,)))
268 elif command==COMMAND_QUIT:
269 outQueue.put((REPLY_QUIT, None))
270 mainLoop.quit()
271
272 def _processCommands():
273 """Process incoming commands.
274
275 It is to be executed in a separate thread and it reads the incoming
276 queue for commands. The commands with their arguments are added to the
277 idle queue of gobject so that _handleCommand will be called by them.
278
279 If COMMAND_QUIT is received, the thread exits."""
280
281 while True:
282 (command, args) = inQueue.get()
283
284 gobject.idle_add(_handleCommand, command, args)
285 if command==COMMAND_QUIT:
286 break
287
288 commandThread = Thread(target = _processCommands)
289 commandThread.daemon = True
290 commandThread.start()
291
292
293 mainLoop = gobject.MainLoop()
294 mainLoop.run()
295
296 commandThread.join()
297
298 def _handleInQueue():
299 """Handle the incoming queue in the main program.
300
301 It reads the replies sent by the helper process. In case of
302 REPLY_FINISHED and REPLY_FAILED the appropriate callback is called. In
303 case of REPLY_QUIT, the thread quits as well."""
304 while True:
305 (reply, args) = _inQueue.get()
306 if reply==REPLY_FINISHED or reply==REPLY_FAILED:
307 (reference,) = args
308 callback = None
309 extra = None
310 with _lock:
311 (callback, extra) = _ref2Data.get(reference, (None, None))
312 if callback is not None:
313 del _ref2Data[reference]
314 if callback is not None:
315 callback(reply==REPLY_FINISHED, extra)
316 elif reply==REPLY_QUIT:
317 break
318
319 def preInitializeSound():
320 """Start the sound handling process and create the thread handling the
321 incoming queue."""
322 global _thread
323 global _process
324 global _inQueue
325 global _outQueue
326
327 _inQueue = Queue()
328 _outQueue = Queue()
329
330 _process = Process(target = _processFn, args = (_outQueue, _inQueue))
331 _process.start()
332
333 _thread = Thread(target = _handleInQueue)
334 _thread.daemon = True
335
336 def initializeSound(soundsDirectory):
337 """Initialize the sound handling. It reads a boolean from the incoming
338 queue indicating if the libraries could be loaded by the process.
339
340 If the boolean is True, the thread handling the incoming replies is
341 started and the directory containing the sounds file is written to the
342 output queue.
343
344 Otherwise the exception is read from the queue, and printed with an
345 error message."""
346 global _initialized
347 _initialized = _inQueue.get()
348
349 if _initialized:
350 _thread.start()
351 _outQueue.put(soundsDirectory)
352 else:
353 exception = _inQueue.get()
354 print("The Gst library is missing from your system. It is needed for sound playback on Linux:")
355 print(exception)
356
357 def startSound(name, finishCallback = None, extra = None):
358 """Start playing back the given sound.
359
360 If a callback is given, a new reference is acquired and the callback is
361 registered with it. Then a COMMAND_STARTSOUND command is written to the
362 output queue"""
363 if _initialized:
364 reference = None
365 if finishCallback is not None:
366 with _lock:
367 global _nextReference
368 reference = _nextReference
369 _nextReference += 1
370 _ref2Data[reference] = (finishCallback, extra)
371
372 _outQueue.put((COMMAND_STARTSOUND, (name, reference)))
373
374 def finalizeSound():
375 """Finalize the sound handling.
376
377 COMMAND_QUIT is sent to the helper process, and then it is joined."""
378 if _initialized:
379 _outQueue.put((COMMAND_QUIT, None))
380 _process.join()
381 _thread.join()
382
383#------------------------------------------------------------------------------
384#------------------------------------------------------------------------------
385
386if __name__ == "__main__":
387 import time
388
389 def callback(result, extra):
390 print("callback", result, extra)
391
392 preInitializeSound()
393
394 soundsPath = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)),
395 "..", "..", "sounds"))
396 print("soundsPath:", soundsPath)
397 initializeSound(soundsPath)
398 startSound("notam.mp3", finishCallback = callback, extra= "notam.mp3")
399 time.sleep(5)
400 startSound("malev.mp3", finishCallback = callback, extra="malev.mp3")
401 time.sleep(5)
402 startSound("ding.wav", finishCallback = callback, extra="ding1.wav")
403 time.sleep(5)
404 startSound("ding.wav", finishCallback = callback, extra="ding2.wav")
405 time.sleep(5)
406 startSound("ding.wav", finishCallback = callback, extra="ding3.wav")
407 time.sleep(5)
408 startSound("dong.wav", finishCallback = callback, extra="dong3.wav")
409 time.sleep(50)
410
411 finalizeSound()
412
413#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.