Mercurial > touhou
changeset 321:61adb5453e46
Implement music playback.
author | Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> |
---|---|
date | Wed, 13 Jun 2012 15:29:43 +0200 |
parents | 1a4ffdda8735 |
children | 4e8192aadcaa |
files | eosd pytouhou/formats/music.py pytouhou/formats/std.py pytouhou/game/game.py pytouhou/games/eosd.py pytouhou/resource/loader.py pytouhou/ui/gamerunner.py pytouhou/ui/music.py pytouhou/vm/msgrunner.py |
diffstat | 9 files changed, 139 insertions(+), 5 deletions(-) [+] |
line wrap: on
line diff
--- a/eosd +++ b/eosd @@ -43,13 +43,14 @@ def main(path, stage_num, rank, characte resource_loader = Loader(path) resource_loader.scan_archives(data) - default_power = [0, 64, 128, 128, 128, 128, 0][stage_num - 1] - game = EoSDGame(resource_loader, [PlayerState(character=character, power=default_power)], stage_num, rank, 16, - prng=prng) # Load stage data stage = resource_loader.get_stage('stage%d.std' % stage_num) + default_power = [0, 64, 128, 128, 128, 128, 0][stage_num - 1] + game = EoSDGame(resource_loader, [PlayerState(character=character, power=default_power)], stage_num, rank, 16, + prng=prng, bgms=stage.bgms) + background_anm_wrapper = resource_loader.get_anm_wrapper(('stg%dbg.anm' % stage_num,)) background = Background(stage, background_anm_wrapper) @@ -62,6 +63,7 @@ pathsep = os.path.pathsep default_data = (pathsep.join(('CM.DAT', 'th06*_CM.DAT', '*CM.DAT', '*cm.dat')), pathsep.join(('ST.DAT', 'th6*ST.DAT', '*ST.DAT', '*st.dat')), pathsep.join(('IN.DAT', 'th6*IN.DAT', '*IN.DAT', '*in.dat')), + pathsep.join(('MD.DAT', 'th6*MD.DAT', '*MD.DAT', '*md.dat')), pathsep.join(('102h.exe', '102*.exe', '東方紅魔郷.exe', '*.exe')))
new file mode 100644 --- /dev/null +++ b/pytouhou/formats/music.py @@ -0,0 +1,29 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2012 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + + +from struct import unpack + + +class Track(object): + def __init__(self): + self.start = 0 + self.end = 0 + + + @classmethod + def read(cls, file): + self = cls() + self.start, self.end = unpack('<II', file.read(8)) + return self
--- a/pytouhou/formats/std.py +++ b/pytouhou/formats/std.py @@ -97,7 +97,8 @@ class Stage(object): bgm_c_path = read_string(file, 128, 'ascii') bgm_d_path = read_string(file, 128, 'ascii') - stage.bgms = [(bgm_a, bgm_a_path), (bgm_b, bgm_b_path), (bgm_c, bgm_c_path), (bgm_d, bgm_d_path)] #TODO: handle ' ' + stage.bgms = [None if bgm[0] == u' ' else bgm + for bgm in ((bgm_a, bgm_a_path), (bgm_b, bgm_b_path), (bgm_c, bgm_c_path), (bgm_d, bgm_d_path))] # Read model definitions offsets = unpack('<%s' % ('I' * nb_models), file.read(4 * nb_models))
--- a/pytouhou/game/game.py +++ b/pytouhou/game/game.py @@ -102,6 +102,14 @@ class Game(object): self.difficulty = self.difficulty_max + def change_music(self, track): + #TODO: don’t crash if the track has already be played. + bgm = self.bgms[track] + if bgm: + self.music.queue(bgm) + self.music.next() + + def enable_spellcard_effect(self): self.spellcard_effect = Effect((-32., -16.), 0, self.spellcard_effect_anm_wrapper) #TODO: find why this offset is necessary.
--- a/pytouhou/games/eosd.py +++ b/pytouhou/games/eosd.py @@ -23,6 +23,9 @@ from pytouhou.game.orb import Orb from pytouhou.game.effect import Effect from pytouhou.game.text import Text +from os.path import join +from pytouhou.ui.music import InfiniteWaveSource + SQ2 = 2. ** 0.5 / 2. @@ -30,7 +33,7 @@ SQ2 = 2. ** 0.5 / 2. class EoSDGame(Game): def __init__(self, resource_loader, player_states, stage, rank, difficulty, bullet_types=None, laser_types=None, item_types=None, - nb_bullets_max=640, width=384, height=448, prng=None): + nb_bullets_max=640, width=384, height=448, prng=None, bgms=None): if not bullet_types: etama3 = resource_loader.get_anm_wrapper(('etama3.anm',)) @@ -93,6 +96,20 @@ class EoSDGame(Game): interface = EoSDInterface(player_states, resource_loader) + self.bgms = [] + for bgm in bgms: + if not bgm: + self.bgms.append(None) + continue + posname = bgm[1].replace('bgm/', '').replace('.mid', '.pos') + track = resource_loader.get_track(posname) + 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) + Game.__init__(self, resource_loader, players, stage, rank, difficulty, bullet_types, laser_types, item_types, nb_bullets_max, width, height, prng, interface)
--- a/pytouhou/resource/loader.py +++ b/pytouhou/resource/loader.py @@ -24,6 +24,7 @@ from pytouhou.formats.anm0 import ANM0 from pytouhou.formats.msg import MSG from pytouhou.formats.sht import SHT from pytouhou.formats.exe import SHT as EoSDSHT, InvalidExeException +from pytouhou.formats.music import Track from pytouhou.resource.anmwrapper import AnmWrapper @@ -106,6 +107,7 @@ class Loader(object): self.instanced_stages = {} self.instanced_msgs = {} self.instanced_shts = {} + self.instanced_tracks = {} def scan_archives(self, paths_lists): @@ -184,6 +186,14 @@ class Loader(object): logger.error("Required game exe not found!") + def get_track(self, name): + posname = name.replace('bgm/', '').replace('.mid', '.pos') + if name not in self.instanced_tracks: + file = self.get_file(posname) + self.instanced_tracks[name] = Track.read(file) #TODO: modular + return self.instanced_tracks[name] + + def get_anm_wrapper(self, names, offsets=None): """Create an AnmWrapper for ANM files “names”.
--- a/pytouhou/ui/gamerunner.py +++ b/pytouhou/ui/gamerunner.py @@ -25,6 +25,8 @@ from pyglet.gl import (glMatrixMode, glL GL_COLOR_ARRAY, GL_VERTEX_ARRAY, GL_TEXTURE_COORD_ARRAY, GL_SCISSOR_TEST) +from pyglet.media import Player as MusicPlayer + from pytouhou.utils.helpers import get_logger from .gamerenderer import GameRenderer @@ -72,6 +74,13 @@ class GameRunner(pyglet.window.Window, G glEnableClientState(GL_VERTEX_ARRAY) glEnableClientState(GL_TEXTURE_COORD_ARRAY) + # Initialize sound + self.game.music = MusicPlayer() + bgm = self.game.bgms[0] + if bgm: + self.game.music.queue(bgm) + self.game.music.play() + # Use our own loop to ensure 60 (for now, 120) fps pyglet.clock.set_fps_limit(120) while not self.has_exit:
new file mode 100644 --- /dev/null +++ b/pytouhou/ui/music.py @@ -0,0 +1,53 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2012 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + + +from pyglet.media import AudioData +from pyglet.media.riff import WaveSource + + +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 #TODO + + self._duration = None + + + def _get_audio_data(self, bytes): + if bytes % self.audio_format.bytes_per_sample != 0: + bytes -= bytes % self.audio_format.bytes_per_sample + + 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)