changeset 421:b1248bab2d0f

Add back music and SFX playback using SDL_mixer instead of pyglet, and add FLAC and Vorbis support.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Tue, 16 Jul 2013 21:07:15 +0200
parents 3a7b36324611
children 52829ebe2561
files README eosd pytouhou/lib/sdl.pxd pytouhou/lib/sdl.pyx pytouhou/lib/sdl.pyxbld pytouhou/ui/gamerunner.py pytouhou/ui/music.py setup.py
diffstat 8 files changed, 159 insertions(+), 109 deletions(-) [+]
line wrap: on
line diff
--- a/README
+++ b/README
@@ -16,7 +16,7 @@ Running:
     * Cython
     * Pyglet
     * SDL2
-    * SDL2_image
+    * SDL2_image, SDL2_mixer
 
 
 Building sample data:
--- a/eosd
+++ b/eosd
@@ -204,7 +204,6 @@ def main(path, data, stage_num, rank, ch
             runner.start()
             break
         except NextStage:
-            game.music.pause()
             if not story or stage_num == (7 if boss_rush else 6 if rank > 0 else 5):
                 break
             stage_num += 1
--- a/pytouhou/lib/sdl.pxd
+++ b/pytouhou/lib/sdl.pxd
@@ -123,3 +123,37 @@ cdef extern from "SDL_image.h":
     int IMG_Init(int flags)
     void IMG_Quit()
     SDL_Surface *IMG_LoadPNG_RW(SDL_RWops *src)
+
+
+cdef extern from "SDL_mixer.h":
+    ctypedef enum:
+        MIX_DEFAULT_FORMAT
+
+    ctypedef struct Mix_Music:
+        pass
+
+    ctypedef struct Mix_Chunk:
+        pass
+
+    int Mix_Init(int flags)
+    void Mix_Quit()
+
+    int Mix_OpenAudio(int frequency, Uint16 format_, int channels, int chunksize)
+    void Mix_CloseAudio()
+
+    int Mix_AllocateChannels(int numchans)
+
+    Mix_Music *Mix_LoadMUS(const char *filename)
+    Mix_Chunk *Mix_LoadWAV_RW(SDL_RWops *src, int freesrc)
+
+    void Mix_FreeMusic(Mix_Music *music)
+    void Mix_FreeChunk(Mix_Chunk *chunk)
+
+    int Mix_PlayMusic(Mix_Music *music, int loops)
+    #int Mix_SetLoopPoints(Mix_Music *music, double start, double end)
+
+    int Mix_Volume(int channel, int volume)
+    int Mix_VolumeChunk(Mix_Chunk *chunk, int volume)
+    int Mix_VolumeMusic(int volume)
+
+    int Mix_PlayChannel(int channel, Mix_Chunk *chunk, int loops)
--- a/pytouhou/lib/sdl.pyx
+++ b/pytouhou/lib/sdl.pyx
@@ -37,6 +37,8 @@ SCANCODE_ESCAPE = SDL_SCANCODE_ESCAPE
 KEYDOWN = SDL_KEYDOWN
 QUIT = SDL_QUIT
 
+DEFAULT_FORMAT = MIX_DEFAULT_FORMAT
+
 
 class SDLError(Exception):
     pass
@@ -97,6 +99,36 @@ cdef class Surface:
             image[3+4*i] = alpha[3*i]
 
 
+cdef class Music:
+    cdef Mix_Music *music
+
+    def __dealloc__(self):
+        if self.music != NULL:
+            Mix_FreeMusic(self.music)
+
+    def play(self, int loops):
+        Mix_PlayMusic(self.music, loops)
+
+    def set_loop_points(self, double start, double end):
+        #Mix_SetLoopPoints(self.music, start, end)
+        pass
+
+
+cdef class Chunk:
+    cdef Mix_Chunk *chunk
+
+    def __dealloc__(self):
+        if self.chunk != NULL:
+            Mix_FreeChunk(self.chunk)
+
+    property volume:
+        def __set__(self, float volume):
+            Mix_VolumeChunk(self.chunk, int(volume * 128))
+
+    def play(self, int channel, int loops):
+        Mix_PlayChannel(channel, self.chunk, loops)
+
+
 def init(Uint32 flags):
     if SDL_Init(flags) < 0:
         raise SDLError(SDL_GetError())
@@ -107,6 +139,11 @@ def img_init(Uint32 flags):
         raise SDLError(SDL_GetError())
 
 
+def mix_init(int flags):
+    if Mix_Init(flags) != flags:
+        raise SDLError(SDL_GetError())
+
+
 def quit():
     SDL_Quit()
 
@@ -115,6 +152,10 @@ def img_quit():
     IMG_Quit()
 
 
+def mix_quit():
+    Mix_Quit()
+
+
 def gl_set_attribute(SDL_GLattr attr, int value):
     if SDL_GL_SetAttribute(attr, value) < 0:
         raise SDLError(SDL_GetError())
@@ -158,6 +199,47 @@ def create_rgb_surface(int width, int he
     return surface
 
 
+def mix_open_audio(int frequency, Uint16 format_, int channels, int chunksize):
+    if Mix_OpenAudio(frequency, format_, channels, chunksize) < 0:
+        raise SDLError(SDL_GetError())
+
+
+def mix_close_audio():
+    Mix_CloseAudio()
+
+
+def mix_allocate_channels(int numchans):
+    if Mix_AllocateChannels(numchans) != numchans:
+        raise SDLError(SDL_GetError())
+
+
+def mix_volume(int channel, float volume):
+    return Mix_Volume(channel, int(volume * 128))
+
+
+def mix_volume_music(float volume):
+    return Mix_VolumeMusic(int(volume * 128))
+
+
+def load_music(const char *filename):
+    music = Music()
+    music.music = Mix_LoadMUS(filename)
+    if music.music == NULL:
+        raise SDLError(SDL_GetError())
+    return music
+
+
+def load_chunk(file_):
+    cdef SDL_RWops *rwops
+    chunk = Chunk()
+    data = file_.read()
+    rwops = SDL_RWFromConstMem(<char*>data, len(data))
+    chunk.chunk = Mix_LoadWAV_RW(rwops, 1)
+    if chunk.chunk == NULL:
+        raise SDLError(SDL_GetError())
+    return chunk
+
+
 def get_ticks():
     return SDL_GetTicks()
 
--- a/pytouhou/lib/sdl.pyxbld
+++ b/pytouhou/lib/sdl.pyxbld
@@ -18,7 +18,7 @@ from distutils.extension import Extensio
 from subprocess import check_output
 
 COMMAND = 'pkg-config'
-LIBRARIES = ['sdl2', 'SDL2_image']
+LIBRARIES = ['sdl2', 'SDL2_image', 'SDL2_mixer']
 
 def make_ext(modname, pyxfilename):
     """ Compile and link with the corrects options. """
--- a/pytouhou/ui/gamerunner.py
+++ b/pytouhou/ui/gamerunner.py
@@ -89,6 +89,8 @@ class GameRunner(GameRenderer):
 
         sdl.init(sdl.INIT_VIDEO)
         sdl.img_init(sdl.INIT_PNG)
+        sdl.mix_init(0)
+
         sdl.gl_set_attribute(sdl.GL_CONTEXT_MAJOR_VERSION, 2)
         sdl.gl_set_attribute(sdl.GL_CONTEXT_MINOR_VERSION, 1)
         sdl.gl_set_attribute(sdl.GL_DOUBLEBUFFER, int(double_buffer))
@@ -101,6 +103,9 @@ class GameRunner(GameRenderer):
                               sdl.WINDOW_OPENGL | sdl.WINDOW_SHOWN)
         self.win.gl_create_context()
 
+        sdl.mix_open_audio(44100, sdl.DEFAULT_FORMAT, 2, 4096)
+        sdl.mix_allocate_channels(26) #TODO: make it dependent on the SFX number.
+
         self.fps_limit = fps_limit
         self.use_fixed_pipeline = fixed_pipeline
         self.replay_level = None
@@ -189,6 +194,8 @@ class GameRunner(GameRenderer):
 
         self.win.gl_delete_context()
         self.win.destroy_window()
+        sdl.mix_close_audio()
+        sdl.mix_quit()
         sdl.img_quit()
         sdl.quit()
 
--- a/pytouhou/ui/music.py
+++ b/pytouhou/ui/music.py
@@ -14,79 +14,13 @@
 
 
 from os.path import join
-
-from pyglet.media import AudioData, AudioFormat, StaticSource, Player
-from pyglet.media.riff import WaveSource
-
-
+from glob import glob
+from pytouhou.lib import sdl
 from pytouhou.utils.helpers import get_logger
 
 logger = get_logger(__name__)
 
 
-class InfiniteWaveSource(WaveSource):
-    def __init__(self, filename, start, end, file=None):
-        WaveSource.__init__(self, filename, file)
-
-        self._start = self.audio_format.bytes_per_sample * start
-        self._end = self.audio_format.bytes_per_sample * end
-
-        if self._end > self._max_offset:
-            raise Exception('Music ends after the end of the file.')
-
-        self._duration = None
-
-
-    def _get_audio_data(self, bytes):
-        bytes -= bytes % self.audio_format.bytes_per_sample
-
-        data = b''
-        length = bytes
-        while True:
-            size = min(length, self._end - self._offset)
-            data += self._file.read(size)
-            if size == length:
-                break
-
-            self._offset = self._start
-            self._file.seek(self._offset + self._start_offset)
-            length -= size
-
-        self._offset += length
-
-        timestamp = float(self._offset) / self.audio_format.bytes_per_second
-        duration = float(bytes) / self.audio_format.bytes_per_second
-
-        return AudioData(data, bytes, timestamp, duration)
-
-
-    def seek(self, timestamp):
-        raise NotImplementedError('irrelevant')
-
-
-class ZwavSource(InfiniteWaveSource):
-    def __init__(self, filename, format, file=None):
-        if file is None:
-            file = open(filename, 'rb')
-
-        self._file = file
-
-        magic = self._file.read(4)
-        assert b'ZWAV' == magic
-
-        self.audio_format = AudioFormat(
-            channels=format.wChannels,
-            sample_size=format.wBitsPerSample,
-            sample_rate=format.dwSamplesPerSec)
-
-        self._start_offset = 0
-        self._offset = format.intro
-
-        self._file.seek(self._offset)
-        self._start = format.intro + format.start
-        self._end = format.intro + format.duration
-
-
 class MusicPlayer(object):
     def __init__(self, resource_loader, bgms):
         self.bgms = []
@@ -99,69 +33,63 @@ class MusicPlayer(object):
                 track = resource_loader.get_track(posname)
             except KeyError:
                 self.bgms.append(None)
-                logger.warn('Music description not found: %s', posname)
+                logger.warn(u'Music description “%s” not found.', posname)
                 continue
-            wavname = join(resource_loader.game_dir, bgm[1].replace('.mid', '.wav'))
-            try:
-                source = InfiniteWaveSource(wavname, track.start, track.end)
-            except IOError:
-                source = None
-            self.bgms.append(source)
-
-        self.player = Player()
-
-
-    def pause(self):
-        self.player.pause()
-
+            globname = join(resource_loader.game_dir, bgm[1]).replace('.mid', '.*')
+            filenames = glob(globname)
+            for filename in reversed(filenames):
+                try:
+                    source = sdl.load_music(filename)
+                except sdl.SDLError as error:
+                    logger.debug(u'Music file “%s” unreadable: %s', filename, error)
+                    continue
+                else:
+                    source.set_loop_points(track.start / 44100., track.end / 44100.) #TODO: retrieve the sample rate from the actual track.
+                    self.bgms.append(source)
+                    logger.debug(u'Music file “%s” opened.', filename)
+                    break
+            else:
+                self.bgms.append(None)
+                logger.warn(u'No working music file for “%s”, disabling bgm.', globname)
 
     def play(self, index):
         bgm = self.bgms[index]
-        if self.player.playing:
-            self.player.next()
         if bgm:
-            self.player.queue(bgm)
-        self.player.play()
+            bgm.play(-1)
 
 
 class SFXPlayer(object):
     def __init__(self, loader, volume=.42):
         self.loader = loader
-        self.players = {}
+        self.channels = {}
         self.sounds = {}
         self.volume = volume
-
+        self.next_channel = 0
 
-    def get_player(self, name):
-        if name not in self.players:
-            self.players[name] = Player()
-            self.players[name].volume = self.volume
-        return self.players[name]
-
+    def get_channel(self, name):
+        if name not in self.channels:
+            self.channels[name] = self.next_channel
+            self.next_channel += 1
+        return self.channels[name]
 
     def get_sound(self, name):
         if name not in self.sounds:
             wave_file = self.loader.get_file(name)
-            self.sounds[name] = StaticSource(WaveSource(name, wave_file))
+            self.sounds[name] = sdl.load_chunk(wave_file)
+            self.sounds[name].volume = self.volume
         return self.sounds[name]
 
-
     def play(self, name, volume=None):
         sound = self.get_sound(name)
-        player = self.get_player(name)
+        channel = self.get_channel(name)
         if volume:
-            player.volume = volume
-        if player.playing:
-            player.next()
-        if sound:
-            player.queue(sound)
-        player.play()
+            sdl.mix_volume(channel, volume)
+        sound.play(channel, 0)
 
 
 class NullPlayer(object):
     def __init__(self, loader=None, bgms=None):
         pass
 
-
-    def play(self, name):
+    def play(self, name, volume=None):
         pass
--- a/setup.py
+++ b/setup.py
@@ -18,7 +18,7 @@ except ImportError:
 
 
 COMMAND = 'pkg-config'
-LIBRARIES = ['sdl2', 'SDL2_image']
+LIBRARIES = ['sdl2', 'SDL2_image', 'SDL2_mixer']
 
 packages = []
 extension_names = []