# HG changeset patch # User Emmanuel Gil Peyrot # Date 1374001635 -7200 # Node ID b1248bab2d0f69eb1fe73470044ddb1af798b1b9 # Parent 3a7b36324611020e0bc3ed5fae42f7d69c9e9cf4 Add back music and SFX playback using SDL_mixer instead of pyglet, and add FLAC and Vorbis support. diff --git a/README b/README --- a/README +++ b/README @@ -16,7 +16,7 @@ Running: * Cython * Pyglet * SDL2 - * SDL2_image + * SDL2_image, SDL2_mixer Building sample data: diff --git a/eosd b/eosd --- 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 diff --git a/pytouhou/lib/sdl.pxd b/pytouhou/lib/sdl.pxd --- 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) diff --git a/pytouhou/lib/sdl.pyx b/pytouhou/lib/sdl.pyx --- 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(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() diff --git a/pytouhou/lib/sdl.pyxbld b/pytouhou/lib/sdl.pyxbld --- 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. """ diff --git a/pytouhou/ui/gamerunner.py b/pytouhou/ui/gamerunner.py --- 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() diff --git a/pytouhou/ui/music.py b/pytouhou/ui/music.py --- 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 diff --git a/setup.py b/setup.py --- 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 = []