source: src/mlx/sound.py@ 573:af59e3bbe92e

Last change on this file since 573:af59e3bbe92e was 573:af59e3bbe92e, checked in by István Váradi <ivaradi@…>, 9 years ago

Added function to properly deinitialize sound handling on Linux

File size: 12.2 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 initializeSound(soundsDirectory):
160 """Initialize the sound handling with the given directory containing
161 the sound files."""
162 global _thread
163 _thread = SoundThread(soundsDirectory)
164 _thread.start()
165
166 def startSound(name, finishCallback = None, extra = None):
167 """Start playing back the given sound.
168
169 name should be the name of a sound file relative to the sound directory
170 given in initializeSound."""
171 _thread.requestSound(name, finishCallback = finishCallback,
172 extra = extra)
173
174 def finalizeSound():
175 """Finalize the sound handling."""
176 pass
177
178#------------------------------------------------------------------------------
179
180else: # os.name!="nt"
181 import threading
182 try:
183 import pyglet
184
185 class SoundThread(threading.Thread):
186 """A thread executing the pyglet event loop that directs the
187 playback of the sound files."""
188 class Player(pyglet.media.ManagedSoundPlayer):
189 """Player which handles the end-of-stream condition
190 properly."""
191
192 def __init__(self, finishCallback, extra):
193 """Construct the player with the given data."""
194 super(SoundThread.Player, self).__init__()
195
196 self._finishCallback = finishCallback
197 self._extra = extra
198
199 def _on_eos(self):
200 if self._finishCallback is not None:
201 self._finishCallback(True, self._extra)
202 return super(SoundThread.Player, self)._on_eos()
203
204 class EventLoop(pyglet.app.EventLoop):
205 """Own implementation of the event loop that collects the
206 requested sound files and plays them."""
207
208 def __init__(self, soundsDirectory):
209 """Construct the event loop."""
210 super(SoundThread.EventLoop, self).__init__()
211
212 self._soundsDirectory = soundsDirectory
213
214 self._lock = threading.Lock()
215 self._requestedSounds = []
216
217 def startSound(self, name, finishCallback, extra):
218 """Add the sound with the given name"""
219 with self._lock:
220 path = os.path.join(self._soundsDirectory, name)
221 self._requestedSounds.append( (path,
222 finishCallback, extra) )
223
224 def idle(self):
225 """The idle callback."""
226 with self._lock:
227 requestedSounds = self._requestedSounds
228 self._requestedSounds = []
229
230 for (path, finishCallback, extra) in requestedSounds:
231 try:
232 media = pyglet.media.load(path)
233 player = SoundThread.Player(finishCallback, extra)
234 player.queue(media)
235 player.play()
236 except Exception, e:
237 print "mlx.SoundThread.EventLoop.idle: " + str(e)
238 if finishCallback is not None:
239 finishCallback(False, extra)
240
241 timeout = super(SoundThread.EventLoop, self).idle()
242 return 0.1 if timeout is None else min(timeout, 0.1)
243
244 def __init__(self, soundsDirectory):
245 """Construct the sound playing thread with the given
246 directory."""
247 super(SoundThread, self).__init__()
248
249 self.daemon = False
250 self.eventLoop = SoundThread.EventLoop(soundsDirectory)
251
252 def run(self):
253 """Run the event loop."""
254 self.eventLoop.run()
255
256 def startSound(self, name, finishCallback = None, extra = None):
257 """Start the playback of the given sound."""
258 self.eventLoop.startSound(name, finishCallback, extra)
259
260 _thread = None
261
262 def initializeSound(soundsDirectory):
263 """Initialize the sound handling with the given directory containing
264 the sound files."""
265 global _thread
266 _thread = SoundThread(soundsDirectory)
267 _thread.start()
268
269
270 def startSound(name, finishCallback = None, extra = None):
271 """Start playing back the given sound."""
272 _thread.startSound(name, finishCallback = finishCallback,
273 extra = extra)
274
275 def finalizeSound():
276 """Finalize the sound handling."""
277 pyglet.app.exit()
278
279 except:
280 print "The pyglet library is missing from your system. It is needed for sound playback on Linux"
281 def initializeSound(soundsDirectory):
282 """Initialize the sound handling with the given directory containing
283 the sound files."""
284 pass
285
286 def startSound(name, finishCallback = None, extra = None):
287 """Start playing back the given sound.
288
289 FIXME: it does not do anything currently, but it should."""
290 print "sound.startSound:", name
291
292 def finalizeSound():
293 """Finalize the sound handling."""
294 pass
295
296#------------------------------------------------------------------------------
297#------------------------------------------------------------------------------
298
299if __name__ == "__main__":
300 def callback(result, extra):
301 print "callback", result, extra
302
303 initializeSound("e:\\home\\vi\\tmp")
304 startSound("malev.mp3", finishCallback = callback, extra="malev.mp3")
305 time.sleep(5)
306 startSound("ding.wav", finishCallback = callback, extra="ding1.wav")
307 time.sleep(5)
308 startSound("ding.wav", finishCallback = callback, extra="ding2.wav")
309 time.sleep(5)
310 startSound("ding.wav", finishCallback = callback, extra="ding3.wav")
311 time.sleep(50)
312
313#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.