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)
--- a/pytouhou/vm/msgrunner.py
+++ b/pytouhou/vm/msgrunner.py
@@ -128,6 +128,11 @@ class MSGRunner(object):
         self._game.msg_wait = False
 
 
+    @instruction(7)
+    def change_music(self, track):
+        self._game.change_music(track)
+
+
     @instruction(10)
     def freeze(self):
         self.frozen = True