Merge pull request #3636 from bionoid/audio_sdl2_seqfmt

audio_sdl2: support sequenced formats
This commit is contained in:
Mathieu Virbel 2015-09-20 10:09:51 +02:00
commit ce4759305a
2 changed files with 194 additions and 43 deletions

View File

@ -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)

View File

@ -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()