mirror of https://github.com/kivy/kivy.git
Merge pull request #3636 from bionoid/audio_sdl2_seqfmt
audio_sdl2: support sequenced formats
This commit is contained in:
commit
ce4759305a
|
@ -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 <double>frames / <double>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(<char *><bytes>fn)
|
||||
if mc.chunk == NULL:
|
||||
Logger.warning('AudioSDL2: Unable to load %r' % self.filename)
|
||||
cc.chunk = Mix_LoadWAV(<char *><bytes>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(<char *><bytes>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)
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue