source: src/mlx/sound.py@ 953:ad2ac5a19fc2

Last change on this file since 953:ad2ac5a19fc2 was 800:709f86ab4573, checked in by István Váradi <ivaradi@…>, 8 years ago

Fixed sound playback on Linux

File size: 14.8 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), 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, e:
119 print "Failed closing " + alias + ":",
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 self._mci.send("play %s from 0 to %s" % \
140 (alias, lengthBuffer))
141 length = int(lengthBuffer)
142 timeout = time.time() + length / 1000.0
143 with self._requestCondition:
144 self._pending.append((timeout, (alias, finishData)))
145 self._pending.sort()
146 print "Started to play", path
147 except Exception, e:
148 print "Failed to start playing " + path + ":",
149 print utf2unicode(str(e))
150 (finishCallback, extra) = finishData
151 if finishCallback is not None:
152 try:
153 finishCallback(None, extra)
154 except:
155 traceback.print_exc()
156
157 _thread = None
158
159 def preInitializeSound():
160 """Perform any-pre initialization.
161
162 This does nothing on Windows."""
163
164 def initializeSound(soundsDirectory):
165 """Initialize the sound handling with the given directory containing
166 the sound files."""
167 global _thread
168 _thread = SoundThread(soundsDirectory)
169 _thread.start()
170
171 def startSound(name, finishCallback = None, extra = None):
172 """Start playing back the given sound.
173
174 name should be the name of a sound file relative to the sound directory
175 given in initializeSound."""
176 _thread.requestSound(name, finishCallback = finishCallback,
177 extra = extra)
178
179 def finalizeSound():
180 """Finalize the sound handling."""
181 pass
182
183#------------------------------------------------------------------------------
184
185else: # os.name!="nt"
186 from multiprocessing import Process, Queue
187 from threading import Thread, Lock
188
189 COMMAND_STARTSOUND = 1
190 COMMAND_QUIT = 2
191
192 REPLY_FINISHED = 101
193 REPLY_FAILED = 102
194 REPLY_QUIT = 103
195
196 _initialized = False
197 _process = None
198 _thread = None
199 _inQueue = None
200 _outQueue = None
201 _nextReference = 1
202 _ref2Data = {}
203 _lock = Lock()
204
205 def _processFn(inQueue, outQueue):
206 """The function running in the helper process created.
207
208 It tries to load the Gst module. If successful, True is sent back,
209 otherwise False followed by the exception caught and the function
210 quits.
211
212 In case of successful initialization, the directory of the sound files
213 is read, the command reader thread is created and the gobject main loop
214 is executed."""
215 try:
216 import gi.repository
217 gi.require_version("Gst", "1.0")
218 from gi.repository import Gst
219 from gi.repository import GObject as gobject
220
221 Gst.init(None)
222 except Exception, e:
223 outQueue.put(False)
224 outQueue.put(e)
225 return
226
227 outQueue.put(True)
228
229 soundsDirectory = inQueue.get()
230
231 _bins = set()
232
233 mainLoop = None
234
235 def _handlePlayBinMessage(bus, message, bin, reference):
236 """Handle messages related to a playback."""
237 if bin in _bins:
238 if message.type==Gst.MessageType.EOS:
239 _bins.remove(bin)
240 if reference is not None:
241 outQueue.put((REPLY_FINISHED, (reference,)))
242 elif message.type==Gst.MessageType.ERROR:
243 _bins.remove(bin)
244 if reference is not None:
245 outQueue.put((REPLY_FAILED, (reference,)))
246
247 def _handleCommand(command, args):
248 """Handle commands sent to the server."""
249 if command==COMMAND_STARTSOUND:
250 (name, reference) = args
251 try:
252 playBin = Gst.ElementFactory.make("playbin", "player")
253
254 bus = playBin.get_bus()
255 bus.add_signal_watch()
256 bus.connect("message", _handlePlayBinMessage,
257 playBin, reference)
258
259 path = os.path.join(soundsDirectory, name)
260 playBin.set_property( "uri", "file://%s" % (path,))
261
262 playBin.set_state(Gst.State.PLAYING)
263 _bins.add(playBin)
264 except Exception as e:
265 if reference is not None:
266 outQueue.put((REPLY_FAILED, (reference,)))
267 elif command==COMMAND_QUIT:
268 outQueue.put((REPLY_QUIT, None))
269 mainLoop.quit()
270
271 def _processCommands():
272 """Process incoming commands.
273
274 It is to be executed in a separate thread and it reads the incoming
275 queue for commands. The commands with their arguments are added to the
276 idle queue of gobject so that _handleCommand will be called by them.
277
278 If COMMAND_QUIT is received, the thread exits."""
279
280 while True:
281 (command, args) = inQueue.get()
282
283 gobject.idle_add(_handleCommand, command, args)
284 if command==COMMAND_QUIT:
285 break
286
287 commandThread = Thread(target = _processCommands)
288 commandThread.daemon = True
289 commandThread.start()
290
291
292 mainLoop = gobject.MainLoop()
293 mainLoop.run()
294
295 commandThread.join()
296
297 def _handleInQueue():
298 """Handle the incoming queue in the main program.
299
300 It reads the replies sent by the helper process. In case of
301 REPLY_FINISHED and REPLY_FAILED the appropriate callback is called. In
302 case of REPLY_QUIT, the thread quits as well."""
303 while True:
304 (reply, args) = _inQueue.get()
305 if reply==REPLY_FINISHED or reply==REPLY_FAILED:
306 (reference,) = args
307 callback = None
308 extra = None
309 with _lock:
310 (callback, extra) = _ref2Data.get(reference, (None, None))
311 if callback is not None:
312 del _ref2Data[reference]
313 if callback is not None:
314 callback(reply==REPLY_FINISHED, extra)
315 elif reply==REPLY_QUIT:
316 break
317
318 def preInitializeSound():
319 """Start the sound handling process and create the thread handling the
320 incoming queue."""
321 global _thread
322 global _process
323 global _inQueue
324 global _outQueue
325
326 _inQueue = Queue()
327 _outQueue = Queue()
328
329 _process = Process(target = _processFn, args = (_outQueue, _inQueue))
330 _process.start()
331
332 _thread = Thread(target = _handleInQueue)
333 _thread.daemon = True
334
335 def initializeSound(soundsDirectory):
336 """Initialize the sound handling. It reads a boolean from the incoming
337 queue indicating if the libraries could be loaded by the process.
338
339 If the boolean is True, the thread handling the incoming replies is
340 started and the directory containing the sounds file is written to the
341 output queue.
342
343 Otherwise the exception is read from the queue, and printed with an
344 error message."""
345 global _initialized
346 _initialized = _inQueue.get()
347
348 if _initialized:
349 _thread.start()
350 _outQueue.put(soundsDirectory)
351 else:
352 exception = _inQueue.get()
353 print "The Gst library is missing from your system. It is needed for sound playback on Linux:"
354 print exception
355
356 def startSound(name, finishCallback = None, extra = None):
357 """Start playing back the given sound.
358
359 If a callback is given, a new reference is acquired and the callback is
360 registered with it. Then a COMMAND_STARTSOUND command is written to the
361 output queue"""
362 if _initialized:
363 reference = None
364 if finishCallback is not None:
365 with _lock:
366 global _nextReference
367 reference = _nextReference
368 _nextReference += 1
369 _ref2Data[reference] = (finishCallback, extra)
370
371 _outQueue.put((COMMAND_STARTSOUND, (name, reference)))
372
373 def finalizeSound():
374 """Finalize the sound handling.
375
376 COMMAND_QUIT is sent to the helper process, and then it is joined."""
377 if _initialized:
378 _outQueue.put((COMMAND_QUIT, None))
379 _process.join()
380 _thread.join()
381
382#------------------------------------------------------------------------------
383#------------------------------------------------------------------------------
384
385if __name__ == "__main__":
386 import time
387
388 def callback(result, extra):
389 print "callback", result, extra
390
391 preInitializeSound()
392
393 soundsPath = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)),
394 "..", "..", "sounds"))
395 print "soundsPath:", soundsPath
396 initializeSound(soundsPath)
397 startSound("notam.mp3", finishCallback = callback, extra= "notam.mp3")
398 time.sleep(5)
399 startSound("malev.mp3", finishCallback = callback, extra="malev.mp3")
400 time.sleep(5)
401 startSound("ding.wav", finishCallback = callback, extra="ding1.wav")
402 time.sleep(5)
403 startSound("ding.wav", finishCallback = callback, extra="ding2.wav")
404 time.sleep(5)
405 startSound("ding.wav", finishCallback = callback, extra="ding3.wav")
406 time.sleep(5)
407 startSound("dong.wav", finishCallback = callback, extra="dong3.wav")
408 time.sleep(50)
409
410 finalizeSound()
411
412#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.