File: audio.py

package info (click to toggle)
python-plyer 2.1.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,808 kB
  • sloc: python: 13,395; sh: 217; makefile: 177
file content (398 lines) | stat: -rw-r--r-- 9,787 bytes parent folder | download | duplicates (3)
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
'''
Documentation:
http://docs.microsoft.com/en-us/windows/desktop/Multimedia

.. versionadded:: 1.4.0
'''

from os.path import join

from ctypes import windll
from ctypes import (
    sizeof, c_void_p, c_ulonglong, c_ulong,
    c_wchar_p, byref, Structure, create_string_buffer
)
from ctypes.wintypes import DWORD, UINT

from plyer.facades import Audio
from plyer.platforms.win.storagepath import WinStoragePath

# DWORD_PTR i.e. ULONG_PTR, 32/64bit
ULONG_PTR = c_ulonglong if sizeof(c_void_p) == 8 else c_ulong

# device specific symbols
MCI_OPEN = 0x803
MCI_OPEN_TYPE = 0x2000
MCI_OPEN_ELEMENT = 512
MCI_RECORD = 0x80F
MCI_STOP = 0x808
MCI_SAVE = 0x813
MCI_PLAY = 0x806
MCI_CLOSE = 0x804

# recorder specific symbols
MCI_FROM = 4
MCI_TO = 8
MCI_WAIT = 2
MCI_SAVE_FILE = 256


class MCI_OPEN_PARMS(Structure):
    '''
    Struct for MCI_OPEN message parameters.

    .. versionadded:: 1.4.0
    '''

    _fields_ = [
        ('mciOpenParms', ULONG_PTR),
        ('wDeviceID', UINT),
        ('lpstrDeviceType', c_wchar_p),
        ('lpstrElementName', c_wchar_p),
        ('lpstrAlias', c_wchar_p)
    ]


class MCI_RECORD_PARMS(Structure):
    '''
    Struct for MCI_RECORD message parameters.

    http://docs.microsoft.com/en-us/windows/desktop/Multimedia/mci-record-parms

    .. versionadded:: 1.4.0
    '''

    _fields_ = [
        ('dwCallback', ULONG_PTR),
        ('dwFrom', DWORD),
        ('dwTo', DWORD)
    ]


class MCI_SAVE_PARMS(Structure):
    '''
    Struct for MCI_SAVE message parameters.

    http://docs.microsoft.com/en-us/windows/desktop/Multimedia/mci-save-parms

    .. versionadded:: 1.4.0
    '''

    _fields_ = [
        ('dwCallback', ULONG_PTR),
        ('lpfilename', c_wchar_p)
    ]


class MCI_PLAY_PARMS(Structure):
    '''
    Struct for MCI_PLAY message parameters.

    http://docs.microsoft.com/en-us/windows/desktop/Multimedia/mci-play-parms

    .. versionadded:: 1.4.0
    '''

    _fields_ = [
        ('dwCallback', ULONG_PTR),
        ('dwFrom', DWORD),
        ('dwTo', DWORD)
    ]


def send_command(device, msg, flags, params):
    '''
    Generic mciSendCommandW() wrapper with error handler.
    All parameters are required as for mciSendCommandW().
    In case of no `params` passed, use `None`, that value
    won't be dereferenced.

    .. versionadded:: 1.4.0
    '''

    multimedia = windll.winmm
    send_command_w = multimedia.mciSendCommandW
    get_error = multimedia.mciGetErrorStringW

    # error text buffer
    # by API specification 128 is max, however the API sometimes
    # kind of does not respect the documented bounds and returns
    # more characters than buffer length...?!
    error_len = 128

    # big enough to prevent API accidentally segfaulting
    error_text = create_string_buffer(error_len * 2)

    # open a recording device with a new file
    error_code = send_command_w(
        device,  # device ID
        msg,
        flags,

        # reference to parameters structure or original value
        # in case of params=False/0/None/...
        byref(params) if params else params
    )

    # handle error messages if any
    if error_code:
        # device did not open, raise an exception
        get_error(error_code, byref(error_text), error_len)
        error_text = error_text.raw.replace(b'\x00', b'').decode('utf-8')

        # either it can close already open device or it will fail because
        # the device is in non-closable state, but the end result is the same
        # and it makes no sense to parse MCI_CLOSE's error in this case
        send_command_w(device, MCI_CLOSE, 0, None)
        raise Exception(error_code, error_text)

    # return params struct because some commands write into it
    # to pass some values out of the local function scope
    return params


class WinRecorder:
    '''
    Generic wrapper for MCI_RECORD handling the filenames and device closing
    in the same approach like it is used for other platforms.

    .. versionadded:: 1.4.0
    '''

    def __init__(self, device, filename):
        self._device = device
        self._filename = filename

    @property
    def device(self):
        '''
        Public property returning device ID.

        .. versionadded:: 1.4.0
        '''
        return self._device

    @property
    def filename(self):
        '''
        Public property returning filename for current recording.

        .. versionadded:: 1.4.0
        '''
        return self._filename

    def record(self):
        '''
        Start recording a WAV sound.

        .. versionadded:: 1.4.0
        '''
        send_command(
            device=self.device,
            msg=MCI_RECORD,
            flags=0,
            params=None
        )

    def stop(self):
        '''
        Stop recording and save the data to a file path
        self.filename. Wait until the file is written.
        Close the device afterwards.

        .. versionadded:: 1.4.0
        '''

        # stop the recording first
        send_command(
            device=self.device,
            msg=MCI_STOP,
            flags=MCI_WAIT,
            params=None
        )

        # choose filename for the WAV file
        save_params = MCI_SAVE_PARMS()
        save_params.lpfilename = self.filename

        # save the sound data to a file and wait
        # until it ends writing to the file
        send_command(
            device=self.device,
            msg=MCI_SAVE,
            flags=MCI_SAVE_FILE | MCI_WAIT,
            params=save_params
        )

        # close the recording device
        send_command(
            device=self.device,
            msg=MCI_CLOSE,
            flags=0,
            params=None
        )


class WinPlayer:
    '''
    Generic wrapper for MCI_PLAY handling the device closing.

    .. versionadded:: 1.4.0
    '''

    def __init__(self, device):
        self._device = device

    @property
    def device(self):
        '''
        Public property returning device ID.

        .. versionadded:: 1.4.0
        '''
        return self._device

    def play(self):
        '''
        Start playing a WAV sound.

        .. versionadded:: 1.4.0
        '''
        play_params = MCI_PLAY_PARMS()
        play_params.dwFrom = 0

        send_command(
            device=self.device,
            msg=MCI_PLAY,
            flags=MCI_FROM,
            params=play_params
        )

    def stop(self):
        '''
        Stop playing a WAV sound and close the device.

        .. versionadded:: 1.4.0
        '''
        send_command(
            device=self.device,
            msg=MCI_STOP,
            flags=MCI_WAIT,
            params=None
        )

        # close the playing device
        send_command(
            device=self.device,
            msg=MCI_CLOSE,
            flags=0,
            params=None
        )


class WinAudio(Audio):
    '''
    Windows implementation of audio recording and audio playing.

    .. versionadded:: 1.4.0
    '''

    def __init__(self, file_path=None):
        # default path unless specified otherwise
        default_path = join(
            WinStoragePath().get_music_dir(),
            'audio.wav'
        )
        super().__init__(file_path or default_path)

        self._recorder = None
        self._player = None
        self._current_file = None

    def _start(self):
        '''
        Start recording a WAV sound in the background asynchronously.

        .. versionadded:: 1.4.0
        '''

        # clean everything before recording in case
        # there is a different device open
        self._stop()

        # create structure and set device parameters
        open_params = MCI_OPEN_PARMS()
        open_params.lpstrDeviceType = 'waveaudio'
        open_params.lpstrElementName = ''

        # open a new device for recording
        open_params = send_command(
            device=0,  # device ID before opening
            msg=MCI_OPEN,

            # empty filename in lpstrElementName
            # device type in lpstrDeviceType
            flags=MCI_OPEN_ELEMENT | MCI_OPEN_TYPE,
            params=open_params
        )

        # get recorder with device id and path for saving
        self._recorder = WinRecorder(
            device=open_params.wDeviceID,
            filename=self._file_path
        )
        self._recorder.record()

        # Setting the currently recorded file as current file
        # for using it as a parameter in audio player
        self._current_file = self._recorder.filename

    def _stop(self):
        '''
        Stop recording or playing of a WAV sound.

        .. versionadded:: 1.4.0
        '''

        if self._recorder:
            self._recorder.stop()
            self._recorder = None

        if self._player:
            self._player.stop()
            self._player = None

    def _play(self):
        '''
        Play a WAV sound from a file. Prioritize latest recorded file before
        default file path from WinAudio.

        .. versionadded:: 1.4.0
        '''

        # create structure and set device parameters
        open_params = MCI_OPEN_PARMS()
        open_params.lpstrDeviceType = 'waveaudio'
        open_params.lpstrElementName = self._current_file or self._file_path

        # open a new device for playing
        open_params = send_command(
            device=0,  # device ID before opening
            msg=MCI_OPEN,

            # existing filename in lpstrElementName
            # device type in lpstrDeviceType
            flags=MCI_OPEN_ELEMENT | MCI_OPEN_TYPE,
            params=open_params
        )

        # get recorder with device id and path for saving
        self._player = WinPlayer(device=open_params.wDeviceID)
        self._player.play()


def instance():
    '''
    Instance for facade proxy.
    '''
    return WinAudio()