File: audio.py

package info (click to toggle)
displaycal-py3 3.9.16-1
  • links: PTS
  • area: main
  • in suites: forky, sid, trixie
  • size: 29,120 kB
  • sloc: python: 115,777; javascript: 11,540; xml: 598; sh: 257; makefile: 173
file content (558 lines) | stat: -rw-r--r-- 18,323 bytes parent folder | download
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
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
# -*- coding: utf-8 -*-
"""Audio wrapper module.

Can use SDL, pyglet, pyo or wx.
pyglet or SDL will be used by default if available.
pyglet can only be used if version >= 1.2.2 is available.
pyo is still buggy under Linux and has a few quirks under Windows.
wx doesn't support fading, changing volume, multiple concurrent sounds,
and only supports wav format.

Example:
sound = Sound("test.wav", loop=True)
sound.Play(fade_ms=1000)
"""

from ctypes import (
    CFUNCTYPE,
    POINTER,
    Structure,
    c_int,
    c_uint8,
    c_uint16,
    c_uint32,
    c_void_p,
)
import ctypes.util
import os
import sys
import threading
import time

if sys.platform == "win32":
    try:
        import win32api
        import pywintypes
    except ImportError:
        win32api = None

from DisplayCAL.config import pydir
from DisplayCAL.util_os import dlopen, getenvu
from DisplayCAL.util_str import safe_str


_ch = {}
_initialized = False
_lib = None
_lib_version = None
_server = None
_snd = {}
_sounds = {}


def init(lib=None, samplerate=22050, channels=2, buffersize=2048, reinit=False):
    """(Re-)Initialize sound subsystem"""
    # Note on buffer size: Too high values cause crackling during fade, too low
    # values cause choppy playback of ogg files when using pyo (good value for
    # pyo is >= 2048)
    global _initialized, _lib, _lib_version, _server, pyglet, pyo, sdl, wx
    if _initialized and not reinit:
        # To re-initialize, explicitly set reinit to True
        return
    # Select the audio library we're going to use.
    # User choice or SDL > pyglet > pyo > wx
    if not lib:
        if sys.platform in ("darwin", "win32"):
            # Mac OS X, Windows
            libs = ("pyglet", "SDL", "pyo", "wx")
        else:
            # Linux
            libs = ("SDL", "pyglet", "pyo", "wx")

        audio_lib = None
        for lib in libs:
            try:
                audio_lib = init(lib, samplerate, channels, buffersize, reinit)
                break
            except Exception:
                pass

        if not audio_lib:
            raise RuntimeError("No suitable audio library found!")
        else:
            return audio_lib
    elif lib == "pyglet":
        if not getattr(sys, "frozen", False):
            # Use included pyglet
            lib_dir = os.path.join(os.path.dirname(__file__), "lib")
            if lib_dir not in sys.path:
                sys.path.insert(0, lib_dir)
        try:
            import pyglet

            version = []
            for item in pyglet.version.split("."):
                try:
                    version.append(int(item))
                except ValueError:
                    version.append(item)
            if version < [1, 2, 2]:
                raise ImportError("pyglet version %s is too old" % pyglet.version)
            _lib = "pyglet"
        except ImportError:
            _lib = None
        else:
            # Work around localization preventing fallback to RIFFSourceLoader
            pyglet.lib.LibraryLoader.darwin_not_found_error = ""
            pyglet.lib.LibraryLoader.linux_not_found_error = ""
            # Set audio driver preference
            pyglet.options["audio"] = ("pulse", "openal", "directsound", "silent")
            _server = pyglet.media
            _lib_version = pyglet.version
    elif lib == "pyo":
        try:
            import pyo

            _lib = "pyo"
        except ImportError:
            _lib = None
        else:
            if isinstance(_server, pyo.Server):
                _server.reinit(
                    sr=samplerate, nchnls=channels, buffersize=buffersize, duplex=0
                )
            else:
                _server = pyo.Server(
                    sr=samplerate,
                    nchnls=channels,
                    buffersize=buffersize,
                    duplex=0,
                    winhost="asio",
                ).boot()
                _server.start()
                _lib_version = ".".join(str(v) for v in pyo.getVersion())
    elif lib == "SDL":
        SDL_INIT_AUDIO = 16
        AUDIO_S16LSB = 0x8010
        AUDIO_S16MSB = 0x9010
        if sys.byteorder == "little":
            MIX_DEFAULT_FORMAT = AUDIO_S16LSB
        else:
            MIX_DEFAULT_FORMAT = AUDIO_S16MSB
        if sys.platform == "win32":
            pth = getenvu("PATH")
            libpth = os.path.join(pydir, "lib")
            if not pth.startswith(libpth + os.pathsep):
                pth = libpth + os.pathsep + pth
                os.environ["PATH"] = safe_str(pth)
        elif sys.platform == "darwin":
            x_framework_pth = os.getenv("X_DYLD_FALLBACK_FRAMEWORK_PATH")
            if x_framework_pth:
                framework_pth = os.getenv("DYLD_FALLBACK_FRAMEWORK_PATH")
                if framework_pth:
                    x_framework_pth = os.pathsep.join([x_framework_pth, framework_pth])
                os.environ["DYLD_FALLBACK_FRAMEWORK_PATH"] = x_framework_pth
        for libname in ("SDL2", "SDL2_mixer", "SDL", "SDL_mixer"):
            handle = None
            if sys.platform in ("darwin", "win32"):
                libfn = ctypes.util.find_library(libname)
            if sys.platform == "win32":
                if libfn and win32api:
                    # Support for unicode paths
                    libfn = str(libfn)
                    try:
                        handle = win32api.LoadLibrary(libfn)
                    except pywintypes.error:
                        pass
            elif sys.platform != "darwin":
                # Hard-code lib names for Linux
                libfn = f"lib{libname}"
                if libname.startswith("SDL2"):
                    # SDL 2.0
                    libfn += "-2.0.so.0"
                else:
                    # SDL 1.2
                    libfn += "-1.2.so.0"
            dll = dlopen(libfn, handle=handle)
            if dll:
                print(f"{libname}:", libfn)
            if libname.endswith("_mixer"):
                if not dll:
                    continue
                if not sdl:
                    raise RuntimeError("SDL library not loaded")
                sdl.SDL_RWFromFile.restype = POINTER(SDL_RWops)
                _server = dll
                _server.Mix_OpenAudio.argtypes = [c_int, c_uint16, c_int, c_int]
                _server.Mix_LoadWAV_RW.argtypes = [POINTER(SDL_RWops), c_int]
                _server.Mix_LoadWAV_RW.restype = POINTER(Mix_Chunk)
                _server.Mix_PlayChannelTimed.argtypes = [
                    c_int,
                    POINTER(Mix_Chunk),
                    c_int,
                    c_int,
                ]
                _server.Mix_VolumeChunk.argtypes = [POINTER(Mix_Chunk), c_int]
                if _initialized:
                    _server.Mix_Quit()
                    sdl.SDL_Quit()
                sdl.SDL_Init(SDL_INIT_AUDIO)
                _server.Mix_OpenAudio(
                    samplerate, MIX_DEFAULT_FORMAT, channels, buffersize
                )
                _lib = "SDL"
                if libname.startswith("SDL2"):
                    _lib_version = "2.0"
                else:
                    _lib_version = "1.2"
                break
            else:
                sdl = dll
                _server = None
    elif lib == "wx":
        try:
            import wx

            _lib = "wx"
        except ImportError:
            _lib = None
        else:
            _server = wx
            _lib_version = wx.__version__
    if not _lib:
        raise RuntimeError("No audio library available")
    _initialized = True
    return _server


def safe_init(lib=None, samplerate=22050, channels=2, buffersize=2048, reinit=False):
    """Like init(), but catch any exceptions"""
    global _initialized
    try:
        return init(lib, samplerate, channels, buffersize, reinit)
    except Exception as exception:
        # So we can check if initialization failed
        _initialized = exception
        return exception


def Sound(filename, loop=False, raise_exceptions=False):
    """Sound caching mechanism"""
    if (filename, loop) in _sounds:
        # Cache hit
        return _sounds[(filename, loop)]
    else:
        try:
            sound = _Sound(filename, loop)
        except Exception as exception:
            if raise_exceptions:
                raise exception
            print(exception)
            sound = _Sound(None, loop)
        _sounds[(filename, loop)] = sound
        return sound


class DummySound:
    """Dummy sound wrapper class"""

    def __init__(self, filename=None, loop=False):
        pass

    def fade(self, fade_ms, fade_in=None):
        return True

    @property
    def is_playing(self):
        return False

    def play(self, fade_ms=0):
        return True

    @property
    def play_count(self):
        return 0

    def safe_fade(self, fade_ms, fade_in=None):
        return True

    def safe_play(self, fade_ms=0):
        return True

    def safe_stop(self, fade_ms=0):
        return True

    def stop(self, fade_ms=0):
        return True

    volume = 0


class SDL_RWops(Structure):
    pass


class Mix_Chunk(Structure):
    _fields_ = [
        ("allocated", c_int),
        ("abuf", POINTER(c_uint8)),
        ("alen", c_uint32),
        ("volume", c_uint8),
    ]


class _Sound:
    """Sound wrapper class."""

    def __init__(self, filename, loop=False):
        self._filename = filename
        self._is_playing = False
        self._lib = _lib
        self._lib_version = _lib_version
        self._loop = loop
        self._play_timestamp = 0
        self._play_count = 0
        self._thread = -1
        if not _initialized:
            self._server = init()
        else:
            self._server = _server
        if _initialized and not isinstance(_initialized, Exception):
            if not self._lib and _lib:
                self._lib = _lib
                self._lib_version = _lib_version
            if not self._snd and self._filename:
                if self._lib == "pyo":
                    self._snd = pyo.SfPlayer(safe_str(self._filename), loop=self._loop)
                elif self._lib == "pyglet":
                    snd = pyglet.media.load(self._filename, streaming=False)
                    self._ch = pyglet.media.Player()
                    self._snd = snd
                elif self._lib == "SDL":
                    rw = sdl.SDL_RWFromFile(safe_str(self._filename, "UTF-8"), "rb")
                    self._snd = self._server.Mix_LoadWAV_RW(rw, 1)
                elif self._lib == "wx":
                    self._snd = wx.Sound(self._filename)

    @property
    def _ch(self):
        return _ch.get((self._filename, self._loop))

    @_ch.setter
    def _ch(self, ch):
        _ch[(self._filename, self._loop)] = ch

    def _fade(self, fade_ms, fade_in, thread):
        volume = self.volume
        if fade_ms and ((fade_in and volume < 1) or (not fade_in and volume)):
            count = 200
            for i in range(count + 1):
                if fade_in:
                    self.volume = volume + i / float(count) * (1.0 - volume)
                else:
                    self.volume = volume - i / float(count) * volume
                time.sleep(fade_ms / 1000.0 / count)
                if self._thread is not thread:
                    # If we are no longer the current thread, return immediately
                    return
        if not self.volume:
            self.stop()

    @property
    def volume(self):
        volume = 1.0
        if self._snd:
            if self._lib == "pyo":
                volume = self._snd.mul
            elif self._lib == "pyglet":
                volume = self._ch.volume
            elif self._lib == "SDL":
                volume = float(self._server.Mix_VolumeChunk(self._snd, -1)) / 128
        return volume

    @volume.setter
    def volume(self, volume):
        if self._snd and self._lib != "wx":
            if self._lib == "pyo":
                self._snd.mul = volume
            elif self._lib == "pyglet":
                self._ch.volume = volume
            elif self._lib == "SDL":
                self._server.Mix_VolumeChunk(self._snd, int(round(volume * 128)))
            return True
        return False

    @property
    def _snd(self):
        return _snd.get((self._filename, self._loop))

    @_snd.setter
    def _snd(self, snd):
        _snd[(self._filename, self._loop)] = snd

    def fade(self, fade_ms, fade_in=None):
        """Fade in/out.

        If fade_in is None, fade in/out depending on current volume.
        """
        if fade_in is None:
            fade_in = not self.volume
        if fade_in and not self.is_playing:
            return self.play(fade_ms=fade_ms)
        elif self._snd and self._lib != "wx":
            self._thread += 1
            threading.Thread(
                target=self._fade,
                name="AudioFading-%d[%sms]" % (self._thread, fade_ms),
                args=(fade_ms, fade_in, self._thread),
            ).start()
            return True
        return False

    @property
    def is_playing(self):
        if self._lib == "pyo":
            return bool(self._snd and self._snd.isOutputting())
        elif self._lib == "pyglet":
            return bool(
                self._ch
                and self._ch.playing
                and self._ch.source
                and (
                    self._loop
                    or time.time() - self._play_timestamp < self._ch.source.duration
                )
            )
        elif self._lib == "SDL":
            return bool(self._ch is not None and self._server.Mix_Playing(self._ch))
        return self._is_playing

    def play(self, fade_ms=0, stop_already_playing=True):
        if self._snd:
            volume = self.volume
            if stop_already_playing:
                self.stop()
            if self._lib == "pyglet":
                # Can't reuse the player, won't replay the sound under Mac OS X
                # and Linux even when seeking to start position which allows
                # replaying the sound under Windows.
                if stop_already_playing:
                    try:
                        self._ch.delete()
                    except TypeError:
                        pass
                self._ch = pyglet.media.Player()
                if self._lib_version >= "1.4.0":
                    self._ch.loop = self._loop
                self.volume = volume
            if not self.is_playing and fade_ms and volume == 1:
                self.volume = 0
            self._play_timestamp = time.time()
            if self._lib == "pyo":
                self._snd.out()
            elif self._lib == "pyglet":
                if self._loop and self._lib_version < "1.4.0":
                    snd = pyglet.media.SourceGroup(
                        self._snd.audio_format, self._snd.video_format
                    )
                    snd.loop = True
                    snd.queue(self._snd)
                else:
                    snd = self._snd
                self._ch.queue(snd)
                self._ch.play()
            elif self._lib == "SDL":
                self._ch = self._server.Mix_PlayChannelTimed(
                    -1, self._snd, -1 if self._loop else 0, -1
                )
            elif self._lib == "wx" and self._snd.IsOk():
                flags = wx.SOUND_ASYNC
                if self._loop:
                    flags |= wx.SOUND_LOOP
                    # The best we can do is have the correct state reflected
                    # for looping sounds only
                    self._is_playing = True
                # wx.Sound.Play is supposed to return True on success.
                # When I tested this, it always returned False, but still
                # played the sound.
                self._snd.Play(flags)
            if self._lib:
                self._play_count += 1
            if fade_ms and self._lib != "wx":
                self.fade(fade_ms, True)
            return True
        return False

    @property
    def play_count(self):
        return self._play_count

    def safe_fade(self, fade_ms, fade_in=None):
        """Like fade(), but catch any exceptions"""
        if not _initialized:
            safe_init()
        try:
            return self.fade(fade_ms, fade_in)
        except Exception as exception:
            return exception

    def safe_play(self, fade_ms=0):
        """Like play(), but catch any exceptions"""
        if not _initialized:
            safe_init()
        try:
            return self.play(fade_ms)
        except Exception as exception:
            return exception

    def safe_stop(self, fade_ms=0):
        """Like stop(), but catch any exceptions"""
        try:
            return self.stop(fade_ms)
        except Exception as exception:
            return exception

    def stop(self, fade_ms=0):
        if self._snd and self.is_playing:
            if self._lib == "wx":
                self._snd.Stop()
                self._is_playing = False
            elif fade_ms:
                self.fade(fade_ms, False)
            else:
                if self._lib == "pyglet":
                    self._ch.pause()
                elif self._lib == "SDL":
                    self._server.Mix_HaltChannel(self._ch)
                else:
                    self._snd.stop()
            return True
        else:
            return False


if __name__ == "__main__":
    import wx
    from DisplayCAL.config import get_data_path

    sound = Sound(get_data_path("theme/engine_hum_loop.wav"), True)
    app = wx.App(0)
    frame = wx.Frame(None, -1, "Test")
    frame.Bind(
        wx.EVT_CLOSE,
        lambda event: (
            sound.stop(1000) and _lib != "wx" and time.sleep(1),
            event.Skip(),
        ),
    )
    panel = wx.Panel(frame)
    panel.Sizer = wx.BoxSizer()
    button = wx.Button(panel, -1, "Play")
    button.Bind(wx.EVT_BUTTON, lambda event: not sound.is_playing and sound.play(3000))
    panel.Sizer.Add(button, 1)
    button = wx.Button(panel, -1, "Stop")
    button.Bind(wx.EVT_BUTTON, lambda event: sound.is_playing and sound.stop(3000))
    panel.Sizer.Add(button, 1)
    panel.Sizer.SetSizeHints(frame)
    frame.Show()
    app.MainLoop()