must
/
backend_sdl2.py
350 строк · 16.4 Кб
1import sys
2import ctypes
3import subprocess
4import backend_base
5import log
6
7
8class SDL2Wrapper(backend_base.BaseWrapper):
9def __init__(self, sdl2_lib: ctypes.CDLL, is_le: bool = True) -> None:
10super().__init__()
11self.lib = sdl2_lib
12if not self.lib:
13raise FileNotFoundError('Failed to load SDL2 library')
14self.SDL_MIX_MAX_VOLUME = 128
15self.SDL_AUDIO_S16LSB = 0x8010
16self.SDL_AUDIO_S16MSB = 0x9010
17self.SDL_AUDIO_F32LSB = 0x8120
18self.SDL_AUDIO_F32MSB = 0x9120
19if is_le:
20self.SDL_AUDIO_F32SYS = self.SDL_AUDIO_F32LSB
21self.SDL_AUDIO_S16SYS = self.SDL_AUDIO_S16LSB
22else:
23self.SDL_AUDIO_F32SYS = self.SDL_AUDIO_F32MSB
24self.SDL_AUDIO_S16SYS = self.SDL_AUDIO_S16MSB
25self.SDL_AUDIO_ALLOW_FREQUENCY_CHANGE = 0x00000001
26self.SDL_AUDIO_ALLOW_FORMAT_CHANGE = 0x00000002
27self.SDL_AUDIO_ALLOW_CHANNELS_CHANGE = 0x00000004
28self.SDL_AUDIO_ALLOW_SAMPLES_CHANGE = 0x00000008
29self.SDL_AUDIO_ALLOW_ANY_CHANGE = (
30self.SDL_AUDIO_ALLOW_FREQUENCY_CHANGE | self.SDL_AUDIO_ALLOW_FORMAT_CHANGE |
31self.SDL_AUDIO_ALLOW_CHANNELS_CHANGE | self.SDL_AUDIO_ALLOW_SAMPLES_CHANGE
32)
33ver_buf = ctypes.c_buffer(3) # Lol why we need struct?
34self.SDL_GetVersion = self.wrap('SDL_GetVersion', args=(ctypes.c_void_p, ))
35self.SDL_GetVersion(ver_buf)
36self.ver = (int.from_bytes(ver_buf[0], 'little'), int.from_bytes(ver_buf[1], 'little'),
37int.from_bytes(ver_buf[2], 'little'))
38self.SDL_AudioInit = self.wrap('SDL_AudioInit', args=(ctypes.c_char_p, ), res=ctypes.c_int)
39self.SDL_AudioQuit = self.wrap('SDL_AudioQuit')
40self.SDL_GetError = self.wrap('SDL_GetError', res=ctypes.c_char_p)
41self.SDL_GetRevision = self.wrap('SDL_GetRevision', res=ctypes.c_char_p)
42self.SDL_free = self.wrap('SDL_free', args=(ctypes.c_void_p, ))
43if self.ver[1] > 0 or self.ver[2] >= 18:
44self.SDL_GetTicks = self.wrap('SDL_GetTicks64', res=ctypes.c_uint64)
45else:
46self.SDL_GetTicks = self.wrap('SDL_GetTicks', res=ctypes.c_uint64)
47self.SDL_GetNumAudioDrivers = self.wrap('SDL_GetNumAudioDrivers', res=ctypes.c_int)
48self.SDL_GetAudioDriver = self.wrap('SDL_GetAudioDriver', args=(ctypes.c_int, ), res=ctypes.c_char_p)
49self.SDL_GetCurrentAudioDriver = self.wrap('SDL_GetCurrentAudioDriver', res=ctypes.c_char_p)
50self.SDL_GetNumAudioDevices = self.wrap('SDL_GetNumAudioDevices', args=(ctypes.c_int, ), res=ctypes.c_int)
51self.SDL_GetAudioDeviceName = self.wrap(
52'SDL_GetAudioDeviceName', args=(ctypes.c_int, ctypes.c_int), res=ctypes.c_char_p
53)
54if self.ver[1] > 0 or self.ver[2] >= 16:
55self.SDL_GetDefaultAudioInfo = self.wrap('SDL_GetDefaultAudioInfo', args=(
56ctypes.POINTER(ctypes.c_char_p), ctypes.c_void_p, ctypes.c_int
57), res=ctypes.c_int)
58else:
59self.SDL_GetDefaultAudioInfo = None
60
61
62class SDL2MixWrapper(backend_base.BaseWrapper):
63def __init__(self, sdl2_mixer_lib: ctypes.CDLL) -> None:
64super().__init__()
65self.lib = sdl2_mixer_lib
66if not self.lib:
67raise FileNotFoundError('Failed to load SDL2_mixer library')
68self.MIX_INIT_FLAC = 0x00000001
69self.MIX_INIT_MOD = 0x00000002
70self.MIX_INIT_MP3 = 0x00000008
71self.MIX_INIT_OGG = 0x00000010
72self.MIX_INIT_MID = 0x00000020
73self.MIX_INIT_OPUS = 0x00000040
74self.MIX_INIT_WAV_PACK = 0x00000080
75self.MIX_DEFAULT_FREQUENCY = 44100
76self.MIX_DEFAULT_CHANNELS = 2
77self.MIX_NO_FADING = 0
78self.MIX_FADING_OUT = 1
79self.MIX_FADING_IN = 2
80self.MUS_NONE = 0
81self.MUS_CMD = 1
82self.MUS_WAV = 2
83self.MUS_MOD = 3
84self.MUS_MID = 4
85self.MUS_OGG = 5
86self.MUS_MP3 = 6
87self.MUS_MP3_MAD_UNUSED = 7
88self.MUS_FLAC = 8
89self.MUS_MOD_PLUG_UNUSED = 9
90self.MUS_OPUS = 10
91self.MUS_WAV_PACK = 11
92self.MUS_GME = 12
93self.type_map = {
94self.MUS_NONE: 'none',
95self.MUS_CMD: 'cmd',
96self.MUS_WAV: 'wav',
97self.MUS_MOD: 'mod',
98self.MUS_MID: 'mid',
99self.MUS_OGG: 'ogg',
100self.MUS_MP3: 'mp3',
101self.MUS_MP3_MAD_UNUSED: 'mp3',
102self.MUS_FLAC: 'flac',
103self.MUS_MOD_PLUG_UNUSED: 'mod',
104self.MUS_OPUS: 'opus',
105self.MUS_WAV_PACK: 'wav_pack',
106self.MUS_GME: 'gme'
107}
108self.Mix_Linked_Version = self.wrap('Mix_Linked_Version', res=ctypes.POINTER(ctypes.c_uint8 * 3))
109self.ver = tuple(self.Mix_Linked_Version().contents[0:3])
110self.Mix_Init = self.wrap('Mix_Init', args=(ctypes.c_int, ), res=ctypes.c_int)
111self.Mix_Quit = self.wrap('Mix_Quit')
112if self.ver[1] > 0 or self.ver[2] >= 2:
113self.Mix_OpenAudio = None
114self.Mix_OpenAudioDevice = self.wrap('Mix_OpenAudioDevice', args=(
115ctypes.c_int, ctypes.c_uint16, ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.c_int
116), res=ctypes.c_int)
117else:
118self.Mix_OpenAudio = self.wrap('Mix_OpenAudio', args=(
119ctypes.c_int, ctypes.c_uint16, ctypes.c_int, ctypes.c_int
120), res=ctypes.c_int)
121self.Mix_OpenAudioDevice = None
122self.Mix_CloseAudio = self.wrap('Mix_CloseAudio')
123self.Mix_QuerySpec = self.wrap('Mix_QuerySpec', args=(
124ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_uint16), ctypes.POINTER(ctypes.c_int)
125))
126self.Mix_AllocateChannels = self.wrap('Mix_AllocateChannels', args=(ctypes.c_int, ), res=ctypes.c_int)
127self.Mix_LoadMUS = self.wrap('Mix_LoadMUS', args=(ctypes.c_char_p, ), res=ctypes.c_void_p)
128self.Mix_FreeMusic = self.wrap('Mix_FreeMusic', args=(ctypes.c_void_p, ))
129self.Mix_GetMusicType = self.wrap('Mix_GetMusicType', args=(ctypes.c_void_p, ), res=ctypes.c_int)
130self.Mix_PlayMusic = self.wrap('Mix_PlayMusic', args=(ctypes.c_void_p, ctypes.c_int), res=ctypes.c_int)
131self.Mix_FadeInMusic = self.wrap('Mix_FadeInMusic', args=(
132ctypes.c_void_p, ctypes.c_int, ctypes.c_int
133), res=ctypes.c_int)
134self.Mix_FadeInMusicPos = self.wrap('Mix_FadeInMusicPos', args=(
135ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_double
136), res=ctypes.c_int)
137self.Mix_FadeOutMusic = self.wrap('Mix_FadeOutMusic', args=(ctypes.c_int, ), res=ctypes.c_int)
138self.Mix_SetMusicPosition = self.wrap('Mix_SetMusicPosition', args=(ctypes.c_double, ), res=ctypes.c_int)
139if self.ver[1] >= 6:
140self.Mix_GetMusicPosition = self.wrap('Mix_GetMusicPosition', args=(ctypes.c_void_p, ), res=ctypes.c_double)
141self.Mix_MusicDuration = self.wrap('Mix_MusicDuration', args=(ctypes.c_void_p, ), res=ctypes.c_double)
142else:
143self.Mix_GetMusicPosition = None
144self.Mix_MusicDuration = None
145self.Mix_PlayingMusic = self.wrap('Mix_PlayingMusic', res=ctypes.c_int)
146self.Mix_PausedMusic = self.wrap('Mix_PausedMusic', res=ctypes.c_int)
147self.Mix_FadingMusic = self.wrap('Mix_FadingMusic', res=ctypes.c_int)
148self.Mix_HaltMusic = self.wrap('Mix_HaltMusic', res=ctypes.c_int)
149self.Mix_VolumeMusic = self.wrap('Mix_VolumeMusic', args=(ctypes.c_void_p, ), res=ctypes.c_int)
150self.Mix_PauseMusic = self.wrap('Mix_PauseMusic')
151self.Mix_ResumeMusic = self.wrap('Mix_ResumeMusic')
152self.Mix_RewindMusic = self.wrap('Mix_RewindMusic')
153
154
155class SDL2Music(backend_base.BaseMusic):
156def __init__(self, app: any, sdl: SDL2Wrapper, mix: SDL2MixWrapper, fp: str, mus: ctypes.c_void_p) -> None:
157super().__init__(fp)
158self.app = app
159self.sdl = sdl
160self.mix = mix
161self.mus = mus
162self.type = self.mix.type_map.get(self.mix.Mix_GetMusicType(self.mus)) or 'none'
163self.play_time_start = 0
164self.pause_time_start = 0
165if self.mix.Mix_MusicDuration:
166self.length = self.mix.Mix_MusicDuration(self.mus)
167if self.length <= 0:
168self.length = 0.0
169log.warn(f'Failed to get music length ({self.app.bts(self.sdl.SDL_GetError())})')
170elif self.app.config['allow_ffmpeg']:
171try:
172result: str = subprocess.check_output([
173'ffprobe', '-i', self.fp, '-show_entries', 'format=duration', '-v', 'quiet'
174], shell=False, encoding=self.app.encoding)
175except Exception as _err:
176raise RuntimeError(str(_err))
177self.length = float(result.split('\n')[1].split('=')[-1])
178
179def play(self) -> None:
180result = self.mix.Mix_PlayMusic(self.mus, 0)
181if result < 0:
182log.warn(f'Failed to play music ({self.app.bts(self.sdl.SDL_GetError())})')
183elif not self.mix.Mix_GetMusicPosition:
184self.play_time_start = self.sdl.SDL_GetTicks()
185
186def set_pos(self, pos: float) -> None:
187if self.mix.Mix_SetMusicPosition(pos) < 0:
188log.warn(f'Failed to set music position ({self.app.bts(self.sdl.SDL_GetError())})')
189elif not self.mix.Mix_GetMusicPosition:
190self.play_time_start = self.sdl.SDL_GetTicks() - int(pos * 1000)
191
192def get_pos(self) -> float:
193if not self.mix.Mix_GetMusicPosition:
194if self.paused:
195return (self.pause_time_start - self.play_time_start) / 1000
196return (self.sdl.SDL_GetTicks() - self.play_time_start) / 1000
197pos = self.mix.Mix_GetMusicPosition(self.mus)
198if pos <= 0:
199pos = 0.0
200log.warn(f'Failed to get music position ({self.app.bts(self.sdl.SDL_GetError())})')
201return pos
202
203def stop(self) -> None:
204self.mix.Mix_HaltMusic()
205
206def is_playing(self) -> bool:
207return self.mix.Mix_PlayingMusic()
208
209def set_paused(self, paused: bool) -> None:
210if paused == self.paused:
211return
212(self.mix.Mix_PauseMusic if paused else self.mix.Mix_ResumeMusic)()
213self.paused = paused
214if not self.mix.Mix_GetMusicPosition:
215if paused:
216self.pause_time_start = self.sdl.SDL_GetTicks()
217else:
218self.play_time_start += self.sdl.SDL_GetTicks() - self.pause_time_start
219
220def rewind(self) -> None:
221if not self.is_playing():
222return
223self.mix.Mix_RewindMusic()
224if not self.mix.Mix_GetMusicPosition:
225self.play_time_start = self.sdl.SDL_GetTicks()
226
227def set_volume(self, volume: float = 1.0) -> None:
228self.mix.Mix_VolumeMusic(int(volume * self.sdl.SDL_MIX_MAX_VOLUME))
229
230def destroy(self) -> None:
231if not self.mix:
232return
233if self.mus:
234self.mix.Mix_FreeMusic(self.mus)
235self.mus = None
236self.mix = None
237self.sdl = None
238self.app = None
239
240
241class SDL2Backend(backend_base.BaseBackend):
242def __init__(self, app: any, libs: dict) -> None:
243super().__init__()
244self.title = 'SDL2_mixer'
245self.app = app
246self.sdl = SDL2Wrapper(libs.get('SDL2'), app.is_le)
247self.mix = SDL2MixWrapper(libs.get('SDL2_mixer'))
248self.default_device_name = ''
249
250def init(self) -> None:
251if self.sdl.SDL_AudioInit(self.app.stb(self.app.config['audio_driver']) or None) < 0:
252raise RuntimeError(f'Failed to init SDL2 audio ({self.app.bts(self.sdl.SDL_GetError())})')
253mix_flags = 0
254if 'mp3' in self.app.config['formats']:
255mix_flags |= self.mix.MIX_INIT_MP3
256if 'ogg' in self.app.config['formats']:
257mix_flags |= self.mix.MIX_INIT_OGG
258if 'opus' in self.app.config['formats']:
259mix_flags |= self.mix.MIX_INIT_OPUS
260if 'mid' in self.app.config['formats']:
261mix_flags |= self.mix.MIX_INIT_MID
262if 'mod' in self.app.config['formats']:
263mix_flags |= self.mix.MIX_INIT_MOD
264if 'flac' in self.app.config['formats']:
265mix_flags |= self.mix.MIX_INIT_FLAC
266mix_init_flags = self.mix.Mix_Init(mix_flags)
267if not self.mix.Mix_Init(mix_flags) and mix_flags:
268raise RuntimeError(f'Failed to init SDL2_mixer ({self.app.bts(self.sdl.SDL_GetError())})')
269elif not mix_flags == mix_init_flags:
270log.warn(f'Failed to init some SDL2_mixer formats ({self.app.bts(self.sdl.SDL_GetError())})')
271if (not self.app.config['freq'] or not self.app.config['channels'] or not self.app.config['device_name'])\
272and self.sdl.SDL_GetDefaultAudioInfo:
273name_buf = ctypes.c_char_p()
274spec_buf = ctypes.c_buffer(32)
275if self.sdl.SDL_GetDefaultAudioInfo(name_buf, spec_buf, 0):
276log.warn(f'Failed to get default device info ({self.app.bts(self.sdl.SDL_GetError())})')
277else:
278if name_buf and name_buf.value:
279self.default_device_name = self.app.bts(name_buf.value)
280self.sdl.SDL_free(name_buf)
281if not self.app.config['freq']:
282self.app.config['freq'] = int.from_bytes(spec_buf[:4], sys.byteorder, signed=True) # noqa
283log.warn('Please set frequency in config to', self.app.config['freq'])
284if not self.app.config['channels']:
285self.app.config['channels'] = int.from_bytes(spec_buf[6], 'little', signed=True) # noqa
286log.warn('Please set channels in config to', self.app.config['channels'])
287if self.mix.Mix_OpenAudioDevice:
288result = self.mix.Mix_OpenAudioDevice(
289self.app.config['freq'],
290self.sdl.SDL_AUDIO_F32SYS if self.app.config['use_float32'] else self.sdl.SDL_AUDIO_S16SYS,
291self.app.config['channels'],
292self.app.config['chunk_size'],
293self.app.stb(self.app.config['device_name']) or None,
294self.sdl.SDL_AUDIO_ALLOW_ANY_CHANGE
295)
296else:
297result = self.mix.Mix_OpenAudio(
298self.app.config['freq'],
299self.sdl.SDL_AUDIO_F32SYS if self.app.config['use_float32'] else self.sdl.SDL_AUDIO_S16SYS,
300self.app.config['channels'],
301self.app.config['chunk_size']
302)
303if result < 0:
304raise RuntimeError(f'Failed to open audio device ({self.app.bts(self.sdl.SDL_GetError())})')
305self.mix.Mix_AllocateChannels(0)
306
307def get_audio_devices_names(self) -> list:
308result = []
309num = self.sdl.SDL_GetNumAudioDevices(0)
310if num < 0:
311num = 10
312log.warn(f'Failed to get number of audio devices, forced to 10 ({self.app.bts(self.sdl.SDL_GetError())})')
313for i in range(num):
314dev_name_bt = self.sdl.SDL_GetAudioDeviceName(i, 0)
315if dev_name_bt is None:
316log.warn(f'Failed to get audio device name with id {i} ({self.app.bts(self.sdl.SDL_GetError())})')
317result.append('')
318result.append(self.app.bts(dev_name_bt))
319return result
320
321def get_current_audio_device_name(self) -> str:
322return self.app.config['device_name'].strip() or self.default_device_name
323
324def open_music(self, fp: str) -> SDL2Music:
325mus = self.mix.Mix_LoadMUS(self.app.stb(fp))
326if not mus:
327raise RuntimeError(f'Failed to open music ({self.app.bts(self.sdl.SDL_GetError())})')
328return SDL2Music(self.app, self.sdl, self.mix, fp, mus)
329
330def quit(self) -> None:
331self.mix.Mix_CloseAudio()
332self.mix.Mix_Quit()
333self.sdl.SDL_AudioQuit()
334
335def destroy(self) -> None:
336self.mix = None
337self.sdl = None
338self.app = None
339
340def get_audio_drivers(self) -> list:
341result = []
342for i in range(self.sdl.SDL_GetNumAudioDrivers()):
343result.append(self.app.bts(self.sdl.SDL_GetAudioDriver(i)))
344return result
345
346def get_current_audio_driver(self) -> str:
347char_name = self.sdl.SDL_GetCurrentAudioDriver()
348if char_name:
349return self.app.bts(char_name)
350return ''
351