must

Форк
0
/
backend_sdl2.py 
350 строк · 16.4 Кб
1
import sys
2
import ctypes
3
import subprocess
4
import backend_base
5
import log
6

7

8
class SDL2Wrapper(backend_base.BaseWrapper):
9
    def __init__(self, sdl2_lib: ctypes.CDLL, is_le: bool = True) -> None:
10
        super().__init__()
11
        self.lib = sdl2_lib
12
        if not self.lib:
13
            raise FileNotFoundError('Failed to load SDL2 library')
14
        self.SDL_MIX_MAX_VOLUME = 128
15
        self.SDL_AUDIO_S16LSB = 0x8010
16
        self.SDL_AUDIO_S16MSB = 0x9010
17
        self.SDL_AUDIO_F32LSB = 0x8120
18
        self.SDL_AUDIO_F32MSB = 0x9120
19
        if is_le:
20
            self.SDL_AUDIO_F32SYS = self.SDL_AUDIO_F32LSB
21
            self.SDL_AUDIO_S16SYS = self.SDL_AUDIO_S16LSB
22
        else:
23
            self.SDL_AUDIO_F32SYS = self.SDL_AUDIO_F32MSB
24
            self.SDL_AUDIO_S16SYS = self.SDL_AUDIO_S16MSB
25
        self.SDL_AUDIO_ALLOW_FREQUENCY_CHANGE = 0x00000001
26
        self.SDL_AUDIO_ALLOW_FORMAT_CHANGE = 0x00000002
27
        self.SDL_AUDIO_ALLOW_CHANNELS_CHANGE = 0x00000004
28
        self.SDL_AUDIO_ALLOW_SAMPLES_CHANGE = 0x00000008
29
        self.SDL_AUDIO_ALLOW_ANY_CHANGE = (
30
            self.SDL_AUDIO_ALLOW_FREQUENCY_CHANGE | self.SDL_AUDIO_ALLOW_FORMAT_CHANGE |
31
            self.SDL_AUDIO_ALLOW_CHANNELS_CHANGE | self.SDL_AUDIO_ALLOW_SAMPLES_CHANGE
32
        )
33
        ver_buf = ctypes.c_buffer(3)  # Lol why we need struct?
34
        self.SDL_GetVersion = self.wrap('SDL_GetVersion', args=(ctypes.c_void_p, ))
35
        self.SDL_GetVersion(ver_buf)
36
        self.ver = (int.from_bytes(ver_buf[0], 'little'), int.from_bytes(ver_buf[1], 'little'),
37
                    int.from_bytes(ver_buf[2], 'little'))
38
        self.SDL_AudioInit = self.wrap('SDL_AudioInit', args=(ctypes.c_char_p, ), res=ctypes.c_int)
39
        self.SDL_AudioQuit = self.wrap('SDL_AudioQuit')
40
        self.SDL_GetError = self.wrap('SDL_GetError', res=ctypes.c_char_p)
41
        self.SDL_GetRevision = self.wrap('SDL_GetRevision', res=ctypes.c_char_p)
42
        self.SDL_free = self.wrap('SDL_free', args=(ctypes.c_void_p, ))
43
        if self.ver[1] > 0 or self.ver[2] >= 18:
44
            self.SDL_GetTicks = self.wrap('SDL_GetTicks64', res=ctypes.c_uint64)
45
        else:
46
            self.SDL_GetTicks = self.wrap('SDL_GetTicks', res=ctypes.c_uint64)
47
        self.SDL_GetNumAudioDrivers = self.wrap('SDL_GetNumAudioDrivers', res=ctypes.c_int)
48
        self.SDL_GetAudioDriver = self.wrap('SDL_GetAudioDriver', args=(ctypes.c_int, ), res=ctypes.c_char_p)
49
        self.SDL_GetCurrentAudioDriver = self.wrap('SDL_GetCurrentAudioDriver', res=ctypes.c_char_p)
50
        self.SDL_GetNumAudioDevices = self.wrap('SDL_GetNumAudioDevices', args=(ctypes.c_int, ), res=ctypes.c_int)
51
        self.SDL_GetAudioDeviceName = self.wrap(
52
            'SDL_GetAudioDeviceName', args=(ctypes.c_int, ctypes.c_int), res=ctypes.c_char_p
53
        )
54
        if self.ver[1] > 0 or self.ver[2] >= 16:
55
            self.SDL_GetDefaultAudioInfo = self.wrap('SDL_GetDefaultAudioInfo', args=(
56
                ctypes.POINTER(ctypes.c_char_p), ctypes.c_void_p, ctypes.c_int
57
            ), res=ctypes.c_int)
58
        else:
59
            self.SDL_GetDefaultAudioInfo = None
60

61

62
class SDL2MixWrapper(backend_base.BaseWrapper):
63
    def __init__(self, sdl2_mixer_lib: ctypes.CDLL) -> None:
64
        super().__init__()
65
        self.lib = sdl2_mixer_lib
66
        if not self.lib:
67
            raise FileNotFoundError('Failed to load SDL2_mixer library')
68
        self.MIX_INIT_FLAC = 0x00000001
69
        self.MIX_INIT_MOD = 0x00000002
70
        self.MIX_INIT_MP3 = 0x00000008
71
        self.MIX_INIT_OGG = 0x00000010
72
        self.MIX_INIT_MID = 0x00000020
73
        self.MIX_INIT_OPUS = 0x00000040
74
        self.MIX_INIT_WAV_PACK = 0x00000080
75
        self.MIX_DEFAULT_FREQUENCY = 44100
76
        self.MIX_DEFAULT_CHANNELS = 2
77
        self.MIX_NO_FADING = 0
78
        self.MIX_FADING_OUT = 1
79
        self.MIX_FADING_IN = 2
80
        self.MUS_NONE = 0
81
        self.MUS_CMD = 1
82
        self.MUS_WAV = 2
83
        self.MUS_MOD = 3
84
        self.MUS_MID = 4
85
        self.MUS_OGG = 5
86
        self.MUS_MP3 = 6
87
        self.MUS_MP3_MAD_UNUSED = 7
88
        self.MUS_FLAC = 8
89
        self.MUS_MOD_PLUG_UNUSED = 9
90
        self.MUS_OPUS = 10
91
        self.MUS_WAV_PACK = 11
92
        self.MUS_GME = 12
93
        self.type_map = {
94
            self.MUS_NONE: 'none',
95
            self.MUS_CMD: 'cmd',
96
            self.MUS_WAV: 'wav',
97
            self.MUS_MOD: 'mod',
98
            self.MUS_MID: 'mid',
99
            self.MUS_OGG: 'ogg',
100
            self.MUS_MP3: 'mp3',
101
            self.MUS_MP3_MAD_UNUSED: 'mp3',
102
            self.MUS_FLAC: 'flac',
103
            self.MUS_MOD_PLUG_UNUSED: 'mod',
104
            self.MUS_OPUS: 'opus',
105
            self.MUS_WAV_PACK: 'wav_pack',
106
            self.MUS_GME: 'gme'
107
        }
108
        self.Mix_Linked_Version = self.wrap('Mix_Linked_Version', res=ctypes.POINTER(ctypes.c_uint8 * 3))
109
        self.ver = tuple(self.Mix_Linked_Version().contents[0:3])
110
        self.Mix_Init = self.wrap('Mix_Init', args=(ctypes.c_int, ), res=ctypes.c_int)
111
        self.Mix_Quit = self.wrap('Mix_Quit')
112
        if self.ver[1] > 0 or self.ver[2] >= 2:
113
            self.Mix_OpenAudio = None
114
            self.Mix_OpenAudioDevice = self.wrap('Mix_OpenAudioDevice', args=(
115
                ctypes.c_int, ctypes.c_uint16, ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.c_int
116
            ), res=ctypes.c_int)
117
        else:
118
            self.Mix_OpenAudio = self.wrap('Mix_OpenAudio', args=(
119
                ctypes.c_int, ctypes.c_uint16, ctypes.c_int, ctypes.c_int
120
            ), res=ctypes.c_int)
121
            self.Mix_OpenAudioDevice = None
122
        self.Mix_CloseAudio = self.wrap('Mix_CloseAudio')
123
        self.Mix_QuerySpec = self.wrap('Mix_QuerySpec', args=(
124
            ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_uint16), ctypes.POINTER(ctypes.c_int)
125
        ))
126
        self.Mix_AllocateChannels = self.wrap('Mix_AllocateChannels', args=(ctypes.c_int, ), res=ctypes.c_int)
127
        self.Mix_LoadMUS = self.wrap('Mix_LoadMUS', args=(ctypes.c_char_p, ), res=ctypes.c_void_p)
128
        self.Mix_FreeMusic = self.wrap('Mix_FreeMusic', args=(ctypes.c_void_p, ))
129
        self.Mix_GetMusicType = self.wrap('Mix_GetMusicType', args=(ctypes.c_void_p, ), res=ctypes.c_int)
130
        self.Mix_PlayMusic = self.wrap('Mix_PlayMusic', args=(ctypes.c_void_p, ctypes.c_int), res=ctypes.c_int)
131
        self.Mix_FadeInMusic = self.wrap('Mix_FadeInMusic', args=(
132
            ctypes.c_void_p, ctypes.c_int, ctypes.c_int
133
        ), res=ctypes.c_int)
134
        self.Mix_FadeInMusicPos = self.wrap('Mix_FadeInMusicPos', args=(
135
            ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_double
136
        ), res=ctypes.c_int)
137
        self.Mix_FadeOutMusic = self.wrap('Mix_FadeOutMusic', args=(ctypes.c_int, ), res=ctypes.c_int)
138
        self.Mix_SetMusicPosition = self.wrap('Mix_SetMusicPosition', args=(ctypes.c_double, ), res=ctypes.c_int)
139
        if self.ver[1] >= 6:
140
            self.Mix_GetMusicPosition = self.wrap('Mix_GetMusicPosition', args=(ctypes.c_void_p, ), res=ctypes.c_double)
141
            self.Mix_MusicDuration = self.wrap('Mix_MusicDuration', args=(ctypes.c_void_p, ), res=ctypes.c_double)
142
        else:
143
            self.Mix_GetMusicPosition = None
144
            self.Mix_MusicDuration = None
145
        self.Mix_PlayingMusic = self.wrap('Mix_PlayingMusic', res=ctypes.c_int)
146
        self.Mix_PausedMusic = self.wrap('Mix_PausedMusic', res=ctypes.c_int)
147
        self.Mix_FadingMusic = self.wrap('Mix_FadingMusic', res=ctypes.c_int)
148
        self.Mix_HaltMusic = self.wrap('Mix_HaltMusic', res=ctypes.c_int)
149
        self.Mix_VolumeMusic = self.wrap('Mix_VolumeMusic', args=(ctypes.c_void_p, ), res=ctypes.c_int)
150
        self.Mix_PauseMusic = self.wrap('Mix_PauseMusic')
151
        self.Mix_ResumeMusic = self.wrap('Mix_ResumeMusic')
152
        self.Mix_RewindMusic = self.wrap('Mix_RewindMusic')
153

154

155
class SDL2Music(backend_base.BaseMusic):
156
    def __init__(self, app: any, sdl: SDL2Wrapper, mix: SDL2MixWrapper, fp: str, mus: ctypes.c_void_p) -> None:
157
        super().__init__(fp)
158
        self.app = app
159
        self.sdl = sdl
160
        self.mix = mix
161
        self.mus = mus
162
        self.type = self.mix.type_map.get(self.mix.Mix_GetMusicType(self.mus)) or 'none'
163
        self.play_time_start = 0
164
        self.pause_time_start = 0
165
        if self.mix.Mix_MusicDuration:
166
            self.length = self.mix.Mix_MusicDuration(self.mus)
167
            if self.length <= 0:
168
                self.length = 0.0
169
                log.warn(f'Failed to get music length ({self.app.bts(self.sdl.SDL_GetError())})')
170
        elif self.app.config['allow_ffmpeg']:
171
            try:
172
                result: str = subprocess.check_output([
173
                    'ffprobe', '-i', self.fp, '-show_entries', 'format=duration', '-v', 'quiet'
174
                ], shell=False, encoding=self.app.encoding)
175
            except Exception as _err:
176
                raise RuntimeError(str(_err))
177
            self.length = float(result.split('\n')[1].split('=')[-1])
178

179
    def play(self) -> None:
180
        result = self.mix.Mix_PlayMusic(self.mus, 0)
181
        if result < 0:
182
            log.warn(f'Failed to play music ({self.app.bts(self.sdl.SDL_GetError())})')
183
        elif not self.mix.Mix_GetMusicPosition:
184
            self.play_time_start = self.sdl.SDL_GetTicks()
185

186
    def set_pos(self, pos: float) -> None:
187
        if self.mix.Mix_SetMusicPosition(pos) < 0:
188
            log.warn(f'Failed to set music position ({self.app.bts(self.sdl.SDL_GetError())})')
189
        elif not self.mix.Mix_GetMusicPosition:
190
            self.play_time_start = self.sdl.SDL_GetTicks() - int(pos * 1000)
191

192
    def get_pos(self) -> float:
193
        if not self.mix.Mix_GetMusicPosition:
194
            if self.paused:
195
                return (self.pause_time_start - self.play_time_start) / 1000
196
            return (self.sdl.SDL_GetTicks() - self.play_time_start) / 1000
197
        pos = self.mix.Mix_GetMusicPosition(self.mus)
198
        if pos <= 0:
199
            pos = 0.0
200
            log.warn(f'Failed to get music position ({self.app.bts(self.sdl.SDL_GetError())})')
201
        return pos
202

203
    def stop(self) -> None:
204
        self.mix.Mix_HaltMusic()
205

206
    def is_playing(self) -> bool:
207
        return self.mix.Mix_PlayingMusic()
208

209
    def set_paused(self, paused: bool) -> None:
210
        if paused == self.paused:
211
            return
212
        (self.mix.Mix_PauseMusic if paused else self.mix.Mix_ResumeMusic)()
213
        self.paused = paused
214
        if not self.mix.Mix_GetMusicPosition:
215
            if paused:
216
                self.pause_time_start = self.sdl.SDL_GetTicks()
217
            else:
218
                self.play_time_start += self.sdl.SDL_GetTicks() - self.pause_time_start
219

220
    def rewind(self) -> None:
221
        if not self.is_playing():
222
            return
223
        self.mix.Mix_RewindMusic()
224
        if not self.mix.Mix_GetMusicPosition:
225
            self.play_time_start = self.sdl.SDL_GetTicks()
226

227
    def set_volume(self, volume: float = 1.0) -> None:
228
        self.mix.Mix_VolumeMusic(int(volume * self.sdl.SDL_MIX_MAX_VOLUME))
229

230
    def destroy(self) -> None:
231
        if not self.mix:
232
            return
233
        if self.mus:
234
            self.mix.Mix_FreeMusic(self.mus)
235
            self.mus = None
236
        self.mix = None
237
        self.sdl = None
238
        self.app = None
239

240

241
class SDL2Backend(backend_base.BaseBackend):
242
    def __init__(self, app: any, libs: dict) -> None:
243
        super().__init__()
244
        self.title = 'SDL2_mixer'
245
        self.app = app
246
        self.sdl = SDL2Wrapper(libs.get('SDL2'), app.is_le)
247
        self.mix = SDL2MixWrapper(libs.get('SDL2_mixer'))
248
        self.default_device_name = ''
249

250
    def init(self) -> None:
251
        if self.sdl.SDL_AudioInit(self.app.stb(self.app.config['audio_driver']) or None) < 0:
252
            raise RuntimeError(f'Failed to init SDL2 audio ({self.app.bts(self.sdl.SDL_GetError())})')
253
        mix_flags = 0
254
        if 'mp3' in self.app.config['formats']:
255
            mix_flags |= self.mix.MIX_INIT_MP3
256
        if 'ogg' in self.app.config['formats']:
257
            mix_flags |= self.mix.MIX_INIT_OGG
258
        if 'opus' in self.app.config['formats']:
259
            mix_flags |= self.mix.MIX_INIT_OPUS
260
        if 'mid' in self.app.config['formats']:
261
            mix_flags |= self.mix.MIX_INIT_MID
262
        if 'mod' in self.app.config['formats']:
263
            mix_flags |= self.mix.MIX_INIT_MOD
264
        if 'flac' in self.app.config['formats']:
265
            mix_flags |= self.mix.MIX_INIT_FLAC
266
        mix_init_flags = self.mix.Mix_Init(mix_flags)
267
        if not self.mix.Mix_Init(mix_flags) and mix_flags:
268
            raise RuntimeError(f'Failed to init SDL2_mixer ({self.app.bts(self.sdl.SDL_GetError())})')
269
        elif not mix_flags == mix_init_flags:
270
            log.warn(f'Failed to init some SDL2_mixer formats ({self.app.bts(self.sdl.SDL_GetError())})')
271
        if (not self.app.config['freq'] or not self.app.config['channels'] or not self.app.config['device_name'])\
272
                and self.sdl.SDL_GetDefaultAudioInfo:
273
            name_buf = ctypes.c_char_p()
274
            spec_buf = ctypes.c_buffer(32)
275
            if self.sdl.SDL_GetDefaultAudioInfo(name_buf, spec_buf, 0):
276
                log.warn(f'Failed to get default device info ({self.app.bts(self.sdl.SDL_GetError())})')
277
            else:
278
                if name_buf and name_buf.value:
279
                    self.default_device_name = self.app.bts(name_buf.value)
280
                    self.sdl.SDL_free(name_buf)
281
                if not self.app.config['freq']:
282
                    self.app.config['freq'] = int.from_bytes(spec_buf[:4], sys.byteorder, signed=True)  # noqa
283
                    log.warn('Please set frequency in config to', self.app.config['freq'])
284
                if not self.app.config['channels']:
285
                    self.app.config['channels'] = int.from_bytes(spec_buf[6], 'little', signed=True)  # noqa
286
                    log.warn('Please set channels in config to', self.app.config['channels'])
287
        if self.mix.Mix_OpenAudioDevice:
288
            result = self.mix.Mix_OpenAudioDevice(
289
                self.app.config['freq'],
290
                self.sdl.SDL_AUDIO_F32SYS if self.app.config['use_float32'] else self.sdl.SDL_AUDIO_S16SYS,
291
                self.app.config['channels'],
292
                self.app.config['chunk_size'],
293
                self.app.stb(self.app.config['device_name']) or None,
294
                self.sdl.SDL_AUDIO_ALLOW_ANY_CHANGE
295
            )
296
        else:
297
            result = self.mix.Mix_OpenAudio(
298
                self.app.config['freq'],
299
                self.sdl.SDL_AUDIO_F32SYS if self.app.config['use_float32'] else self.sdl.SDL_AUDIO_S16SYS,
300
                self.app.config['channels'],
301
                self.app.config['chunk_size']
302
            )
303
        if result < 0:
304
            raise RuntimeError(f'Failed to open audio device ({self.app.bts(self.sdl.SDL_GetError())})')
305
        self.mix.Mix_AllocateChannels(0)
306

307
    def get_audio_devices_names(self) -> list:
308
        result = []
309
        num = self.sdl.SDL_GetNumAudioDevices(0)
310
        if num < 0:
311
            num = 10
312
            log.warn(f'Failed to get number of audio devices, forced to 10 ({self.app.bts(self.sdl.SDL_GetError())})')
313
        for i in range(num):
314
            dev_name_bt = self.sdl.SDL_GetAudioDeviceName(i, 0)
315
            if dev_name_bt is None:
316
                log.warn(f'Failed to get audio device name with id {i} ({self.app.bts(self.sdl.SDL_GetError())})')
317
                result.append('')
318
            result.append(self.app.bts(dev_name_bt))
319
        return result
320

321
    def get_current_audio_device_name(self) -> str:
322
        return self.app.config['device_name'].strip() or self.default_device_name
323

324
    def open_music(self, fp: str) -> SDL2Music:
325
        mus = self.mix.Mix_LoadMUS(self.app.stb(fp))
326
        if not mus:
327
            raise RuntimeError(f'Failed to open music ({self.app.bts(self.sdl.SDL_GetError())})')
328
        return SDL2Music(self.app, self.sdl, self.mix, fp, mus)
329

330
    def quit(self) -> None:
331
        self.mix.Mix_CloseAudio()
332
        self.mix.Mix_Quit()
333
        self.sdl.SDL_AudioQuit()
334

335
    def destroy(self) -> None:
336
        self.mix = None
337
        self.sdl = None
338
        self.app = None
339

340
    def get_audio_drivers(self) -> list:
341
        result = []
342
        for i in range(self.sdl.SDL_GetNumAudioDrivers()):
343
            result.append(self.app.bts(self.sdl.SDL_GetAudioDriver(i)))
344
        return result
345

346
    def get_current_audio_driver(self) -> str:
347
        char_name = self.sdl.SDL_GetCurrentAudioDriver()
348
        if char_name:
349
            return self.app.bts(char_name)
350
        return ''
351

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.