diff --git a/kivy/core/audio/audio_sdl2.pyx b/kivy/core/audio/audio_sdl2.pyx index ac93e20de..88d833365 100644 --- a/kivy/core/audio/audio_sdl2.pyx +++ b/kivy/core/audio/audio_sdl2.pyx @@ -6,11 +6,26 @@ This core audio implementation require SDL_mixer library. It might conflict with any other library that are using SDL_mixer, such as ffmpeg-android. -Depending the compilation of SDL2 mixer, it can support wav, ogg, mp3, flac, -and mod, s3m etc (libmikmod). +Native formats: + +* wav, since 1.9.0 + +Depending the compilation of SDL2 mixer and/or installed libraries: + +* ogg since 1.9.1 (mixer needs libvorbis/libogg) +* flac since 1.9.1 (mixer needs libflac) +* mp3 since 1.9.1 (mixer needs libsmpeg/libmad; only use mad for GPL apps) +* sequenced formats since 1.9.1 (midi, mod, s3m, etc. Mixer needs + libmodplug or libmikmod) + +.. Warning:: + + Sequenced formats use the SDL2 Mixer music channel, you can only play + one at a time, and .length will be -1 if music fails to load, and 0 + if loaded successfully (we can't get duration of these formats) ''' -__all__ = ('SoundSDL2', ) +__all__ = ('SoundSDL2', 'MusicSDL2') include "../../../kivy/lib/sdl2.pxi" @@ -32,6 +47,7 @@ cdef mix_init(): cdef unsigned short audio_format = AUDIO_S16SYS cdef int audio_channels = 2 cdef int audio_buffers = 4096 + cdef int want_flags = 0 global mix_is_init global mix_flags @@ -40,14 +56,19 @@ cdef mix_init(): return if SDL_Init(SDL_INIT_AUDIO) < 0: - Logger.critical('AudioSDL2: Unable to initialize SDL') + Logger.critical('AudioSDL2: Unable to initialize SDL: {}'.format( + SDL_GetError())) mix_is_init = -1 return 0 - mix_flags = Mix_Init(MIX_INIT_FLAC|MIX_INIT_MOD|MIX_INIT_MP3|MIX_INIT_OGG) + want_flags = MIX_INIT_FLAC | MIX_INIT_OGG | MIX_INIT_MP3 + want_flags |= MIX_INIT_MOD | MIX_INIT_MODPLUG | MIX_INIT_FLUIDSYNTH + + mix_flags = Mix_Init(want_flags) if Mix_OpenAudio(audio_rate, audio_format, audio_channels, audio_buffers): - Logger.critical('AudioSDL2: Unable to open mixer') + Logger.critical('AudioSDL2: Unable to open mixer: {}'.format( + Mix_GetError())) mix_is_init = -1 return 0 @@ -56,7 +77,8 @@ cdef mix_init(): mix_is_init = 1 return 1 -cdef class MixContainer: +# Container for samples (Mix_LoadWAV) +cdef class ChunkContainer: cdef Mix_Chunk *chunk cdef int channel @@ -71,6 +93,23 @@ cdef class MixContainer: Mix_FreeChunk(self.chunk) self.chunk = NULL +# Container for music (Mix_LoadMUS), one channel only +cdef class MusicContainer: + cdef Mix_Music *music + cdef int playing + + def __init__(self): + self.music = NULL + self.playing = 0 + + def __dealloc__(self): + if self.music != NULL: + # I think FreeMusic halts automatically, probably not needed + if Mix_PlayingMusic() and self.playing: + Mix_HaltMusic() + Mix_FreeMusic(self.music) + self.music = NULL + class SoundSDL2(Sound): @@ -80,8 +119,6 @@ class SoundSDL2(Sound): extensions = ["wav"] if mix_flags & MIX_INIT_FLAC: extensions.append("flac") - if mix_flags & MIX_INIT_MOD: - extensions.append("mod") if mix_flags & MIX_INIT_MP3: extensions.append("mp3") if mix_flags & MIX_INIT_OGG: @@ -89,15 +126,15 @@ class SoundSDL2(Sound): return extensions def __init__(self, **kwargs): - self.mc = MixContainer() + self.cc = ChunkContainer() mix_init() super(SoundSDL2, self).__init__(**kwargs) def _check_play(self, dt): - cdef MixContainer mc = self.mc - if mc.channel == -1 or mc.chunk == NULL: + cdef ChunkContainer cc = self.cc + if cc.channel == -1 or cc.chunk == NULL: return False - if Mix_Playing(mc.channel): + if Mix_Playing(cc.channel): return if self.loop: def do_loop(dt): @@ -108,45 +145,45 @@ class SoundSDL2(Sound): return False def _get_length(self): - cdef MixContainer mc = self.mc + cdef ChunkContainer cc = self.cc cdef int freq, channels cdef unsigned int points, frames cdef unsigned short fmt - if mc.chunk == NULL: + if cc.chunk == NULL: return 0 if not Mix_QuerySpec(&freq, &fmt, &channels): return 0 - points = mc.chunk.alen / ((fmt & 0xFF) / 8) + points = cc.chunk.alen / ((fmt & 0xFF) / 8) frames = points / channels return frames / freq def play(self): - cdef MixContainer mc = self.mc + cdef ChunkContainer cc = self.cc self.stop() - if mc.chunk == NULL: + if cc.chunk == NULL: return - mc.chunk.volume = int(self.volume * 128) - mc.channel = Mix_PlayChannel(-1, mc.chunk, 0) - if mc.channel == -1: - Logger.warning( - 'AudioSDL2: Unable to play %r, no more free channel' % self.filename) + cc.chunk.volume = int(self.volume * 128) + cc.channel = Mix_PlayChannel(-1, cc.chunk, 0) + if cc.channel == -1: + Logger.warning('AudioSDL2: Unable to play {}: {}'.format( + self.filename, Mix_GetError())) return # schedule event to check if the sound is still playing or not Clock.schedule_interval(self._check_play, 0.1) super(SoundSDL2, self).play() def stop(self): - cdef MixContainer mc = self.mc - if mc.chunk == NULL or mc.channel == -1: + cdef ChunkContainer cc = self.cc + if cc.chunk == NULL or cc.channel == -1: return - if Mix_GetChunk(mc.channel) == mc.chunk: - Mix_HaltChannel(mc.channel) - mc.channel = -1 + if Mix_GetChunk(cc.channel) == cc.chunk: + Mix_HaltChannel(cc.channel) + cc.channel = -1 Clock.unschedule(self._check_play) super(SoundSDL2, self).stop() def load(self): - cdef MixContainer mc = self.mc + cdef ChunkContainer cc = self.cc self.unload() if self.filename is None: return @@ -156,22 +193,134 @@ class SoundSDL2(Sound): else: fn = self.filename.encode('UTF-8') - mc.chunk = Mix_LoadWAV(fn) - if mc.chunk == NULL: - Logger.warning('AudioSDL2: Unable to load %r' % self.filename) + cc.chunk = Mix_LoadWAV(fn) + if cc.chunk == NULL: + Logger.warning('AudioSDL2: Unable to load {}: {}'.format( + self.filename, Mix_GetError())) else: - mc.chunk.volume = int(self.volume * 128) + cc.chunk.volume = int(self.volume * 128) def unload(self): - cdef MixContainer mc = self.mc + cdef ChunkContainer cc = self.cc self.stop() - if mc.chunk != NULL: - Mix_FreeChunk(mc.chunk) - mc.chunk = NULL + if cc.chunk != NULL: + Mix_FreeChunk(cc.chunk) + cc.chunk = NULL def on_volume(self, instance, volume): - cdef MixContainer mc = self.mc - if mc.chunk != NULL: - mc.chunk.volume = int(volume * 128) + cdef ChunkContainer cc = self.cc + if cc.chunk != NULL: + cc.chunk.volume = int(volume * 128) + + +# LoadMUS supports OGG, MP3, WAV but we only use it for native midi, +# libmikmod, libmodplug and libfluidsynth to avoid confusion +class MusicSDL2(Sound): + + @staticmethod + def extensions(): + mix_init() + + # Assume native midi support (defaults to enabled), but may use + # modplug, fluidsynth or timidity in reality. It may also be + # disabled completely, in which case loading it will fail + extensions = set(['mid', 'midi']) + + # libmodplug, may be incomplete + if mix_flags & MIX_INIT_MODPLUG: + extensions.update(['669', 'abc', 'amf', 'ams', 'dbm', 'dmf', + 'dsm', 'far', 'it', 'j2b', 'mdl', 'med', + 'mod', 'mt2', 'mtm', 'okt', 'pat', 'psm', + 'ptm', 's3m', 'stm', 'ult', 'umx', 'xm']) + + # libmikmod, may be incomplete + if mix_flags & MIX_INIT_MOD: + extensions.update(['669', 'amf', 'apun', 'dsm', 'far', 'gdm', + 'gt2', 'it', 'med', 'mod', 'mtm', 'okt', + 's3m', 'stm', 'stx', 'ult', 'umx', 'uni', + 'xm']) + return list(extensions) + + def __init__(self, **kwargs): + self.mc = MusicContainer() + mix_init() + super(MusicSDL2, self).__init__(**kwargs) + + def _check_play(self, dt): + cdef MusicContainer mc = self.mc + if mc.music == NULL: + return False + if mc.playing and Mix_PlayingMusic(): + return + if self.loop: + def do_loop(dt): + self.play() + Clock.schedule_once(do_loop) + else: + self.stop() + return False + + # No way to check length; return -1 if music is loaded, 0 otherwise + def _get_length(self): + cdef MusicContainer mc = self.mc + if mc.music == NULL: + return -1 + return 0 + + def play(self): + cdef MusicContainer mc = self.mc + self.stop() + if mc.music == NULL: + return + Mix_VolumeMusic(int(self.volume * 128)) + if Mix_PlayMusic(mc.music, 1) == -1: + Logger.warning('AudioSDL2: Unable to play music {}: {}'.format( + self.filename, Mix_GetError())) + return + mc.playing = 1 + # schedule event to check if the sound is still playing or not + Clock.schedule_interval(self._check_play, 0.1) + super(MusicSDL2, self).play() + + def stop(self): + cdef MusicContainer mc = self.mc + if mc.music == NULL or not mc.playing: + return + Mix_HaltMusic() + mc.playing = 0 + Clock.unschedule(self._check_play) + super(MusicSDL2, self).stop() + + def load(self): + cdef MusicContainer mc = self.mc + self.unload() + if self.filename is None: + return + + if isinstance(self.filename, bytes): + fn = self.filename + else: + fn = self.filename.encode('UTF-8') + + mc.music = Mix_LoadMUS(fn) + if mc.music == NULL: + Logger.warning('AudioSDL2: Unable to load music {}: {}'.format( + self.filename, Mix_GetError())) + else: + Mix_VolumeMusic(int(self.volume * 128)) + + def unload(self): + cdef MusicContainer mc = self.mc + self.stop() + if mc.music != NULL: + Mix_FreeMusic(mc.music) + mc.music = NULL + + def on_volume(self, instance, volume): + cdef MusicContainer mc = self.mc + if mc.music != NULL and mc.playing: + Mix_VolumeMusic(int(volume * 128)) + SoundLoader.register(SoundSDL2) +SoundLoader.register(MusicSDL2) diff --git a/kivy/lib/sdl2.pxi b/kivy/lib/sdl2.pxi index 1f2883ca9..8f1a3afaf 100644 --- a/kivy/lib/sdl2.pxi +++ b/kivy/lib/sdl2.pxi @@ -793,9 +793,10 @@ cdef extern from "SDL_mixer.h": ctypedef enum MIX_InitFlags: MIX_INIT_FLAC = 0x00000001 MIX_INIT_MOD = 0x00000002 - MIX_INIT_MP3 = 0x00000004 - MIX_INIT_OGG = 0x00000008 - MIX_INIT_FLUIDSYNTH = 0x00000010 + MIX_INIT_MODPLUG = 0x00000004 + MIX_INIT_MP3 = 0x00000008 + MIX_INIT_OGG = 0x00000010 + MIX_INIT_FLUIDSYNTH = 0x00000020 cdef int MIX_MAX_VOLUME @@ -877,3 +878,4 @@ cdef extern from "SDL_mixer.h": #cdef int Mix_EachSoundFont(int (*function)( char*, void*), void *data) cdef Mix_Chunk * Mix_GetChunk(int channel) cdef void Mix_CloseAudio() + cdef char * Mix_GetError()