summary refs log tree commit diff homepage
diff options
context:
space:
mode:
authorNguyễn Gia Phong <mcsinyx@disroot.org>2020-04-12 16:35:44 +0700
committerNguyễn Gia Phong <mcsinyx@disroot.org>2020-04-12 16:35:44 +0700
commit600c72d0d4c4a64d452eb6bf4e96ac9436eb57e7 (patch)
tree2f90f61263e8cc7c9a95dc64e4cd514635183c6d
parentc326f93bbb7241d7e4d5b0afa1f50a60efb3af67 (diff)
downloadbrutalmaze-600c72d0d4c4a64d452eb6bf4e96ac9436eb57e7.tar.gz
Use palace for positional audio rendering
This fixes GH-15.  Sources doesn't seem to be cleaned up properly though.
-rw-r--r--brutalmaze/characters.py16
-rw-r--r--brutalmaze/constants.py26
-rw-r--r--brutalmaze/game.py65
-rw-r--r--brutalmaze/maze.py16
-rw-r--r--brutalmaze/misc.py48
-rw-r--r--brutalmaze/weapons.py6
-rw-r--r--pyproject.toml3
7 files changed, 93 insertions, 87 deletions
diff --git a/brutalmaze/characters.py b/brutalmaze/characters.py
index ddb31aa..7e9312e 100644
--- a/brutalmaze/characters.py
+++ b/brutalmaze/characters.py
@@ -25,8 +25,8 @@ from sys import modules
 
 from .constants import (
     TANGO, HERO_HP, SFX_HEART, HEAL_SPEED, MIN_BEAT, ATTACK_SPEED, ENEMY,
-    ENEMY_SPEED, ENEMY_HP, SFX_SLASH_HERO, MIDDLE, WALL, FIRANGE, AROUND_HERO,
-    ADJACENTS, EMPTY, SQRT2, ENEMIES)
+    ENEMY_SPEED, ENEMY_HP, SFX_SPAWN, SFX_SLASH_HERO, MIDDLE, WALL, FIRANGE,
+    AROUND_HERO, ADJACENTS, EMPTY, SQRT2, ENEMIES)
 from .misc import sign, randsign, regpoly, fill_aapolygon, play
 from .weapons import Bullet
 
@@ -51,7 +51,6 @@ class Hero:
         spin_queue (float): frames left to finish spinning
         wound (float): amount of wound
         wounds (deque of float): wounds in time of an attack (ATTACK_SPEED)
-        sfx_heart (pygame.mixer.Sound): heart beat sound effect
     """
     def __init__(self, surface, fps, maze_size):
         self.surface = surface
@@ -68,8 +67,6 @@ class Hero:
         self.spin_queue = self.wound = 0.0
         self.wounds = deque([0.0])
 
-        self.sfx_heart = SFX_HEART
-
     def update(self, fps):
         """Update the hero."""
         if self.dead:
@@ -87,7 +84,7 @@ class Hero:
             if self.wound < 0: self.wound = 0.0
         self.wounds.append(0.0)
         if self.next_beat <= 0:
-            play(self.sfx_heart)
+            play(SFX_HEART)
             self.next_beat = MIN_BEAT*(2 - self.wound/HERO_HP)
         else:
             self.next_beat -= 1000 / fps
@@ -168,7 +165,6 @@ class Enemy:
         spin_speed (float): speed of spinning (in frames per slash)
         spin_queue (float): frames left to finish spinning
         wound (float): amount of wound
-        sfx_slash (pygame.mixer.Sound): sound effect of slashed hero
     """
     def __init__(self, maze, x, y, color):
         self.maze = maze
@@ -182,8 +178,6 @@ class Enemy:
         self.spin_speed = self.maze.fps / ENEMY_HP
         self.spin_queue = self.wound = 0.0
 
-        self.sfx_slash = SFX_SLASH_HERO
-
     @property
     def pos(self):
         """Coordinates (in pixels) of the center of the enemy."""
@@ -227,7 +221,7 @@ class Enemy:
             if self.maze.map[srcx+i//w][srcy+i//u] == WALL: return False
         self.awake = True
         self.maze.map[self.x][self.y] = ENEMY
-        play(self.maze.sfx_spawn, self.spawn_volume, self.get_angle()+pi)
+        play(SFX_SPAWN, self.x, self.y)
         return True
 
     def fire(self):
@@ -317,7 +311,7 @@ class Enemy:
             if not self.spin_queue and not self.fire() and not self.move():
                 self.spin_queue = randsign() * self.spin_speed
                 if not self.maze.hero.dead:
-                    play(self.sfx_slash, self.get_slash(), self.get_angle())
+                    play(SFX_SLASH_HERO, self.x, self.y, self.get_slash())
             if round(self.spin_queue) != 0:
                 self.angle += sign(self.spin_queue) * pi / 2 / self.spin_speed
                 self.spin_queue -= sign(self.spin_queue)
diff --git a/brutalmaze/constants.py b/brutalmaze/constants.py
index 1fab35a..396cc2b 100644
--- a/brutalmaze/constants.py
+++ b/brutalmaze/constants.py
@@ -20,25 +20,23 @@ __doc__ = 'Brutal Maze module for shared constants'
 
 from string import ascii_lowercase
 
-from pkg_resources import resource_filename as pkg_file
 import pygame
-from pygame.mixer import Sound
+from pkg_resources import resource_filename as pkg_file
 
 SETTINGS = pkg_file('brutalmaze', 'settings.ini')
 ICON = pygame.image.load(pkg_file('brutalmaze', 'icon.png'))
-NOISE = pkg_file('brutalmaze', 'soundfx/noise.ogg')
 
-mixer = pygame.mixer.get_init()
-if mixer is None: pygame.mixer.init(frequency=44100)
-SFX_SPAWN = Sound(pkg_file('brutalmaze', 'soundfx/spawn.ogg'))
-SFX_SLASH_ENEMY = Sound(pkg_file('brutalmaze', 'soundfx/slash-enemy.ogg'))
-SFX_SLASH_HERO = Sound(pkg_file('brutalmaze', 'soundfx/slash-hero.ogg'))
-SFX_SHOT_ENEMY = Sound(pkg_file('brutalmaze', 'soundfx/shot-enemy.ogg'))
-SFX_SHOT_HERO = Sound(pkg_file('brutalmaze', 'soundfx/shot-hero.ogg'))
-SFX_MISSED = Sound(pkg_file('brutalmaze', 'soundfx/missed.ogg'))
-SFX_HEART = Sound(pkg_file('brutalmaze', 'soundfx/heart.ogg'))
-SFX_LOSE = Sound(pkg_file('brutalmaze', 'soundfx/lose.ogg'))
-if mixer is None: pygame.mixer.quit()
+SFX_NOISE = pkg_file('brutalmaze', 'soundfx/noise.ogg')
+SFX_SPAWN = pkg_file('brutalmaze', 'soundfx/spawn.ogg')
+SFX_SLASH_ENEMY = pkg_file('brutalmaze', 'soundfx/slash-enemy.ogg')
+SFX_SLASH_HERO = pkg_file('brutalmaze', 'soundfx/slash-hero.ogg')
+SFX_SHOT_ENEMY = pkg_file('brutalmaze', 'soundfx/shot-enemy.ogg')
+SFX_SHOT_HERO = pkg_file('brutalmaze', 'soundfx/shot-hero.ogg')
+SFX_MISSED = pkg_file('brutalmaze', 'soundfx/missed.ogg')
+SFX_HEART = pkg_file('brutalmaze', 'soundfx/heart.ogg')
+SFX_LOSE = pkg_file('brutalmaze', 'soundfx/lose.ogg')
+SFX = (SFX_NOISE, SFX_SPAWN, SFX_SLASH_ENEMY, SFX_SLASH_HERO,
+       SFX_SHOT_ENEMY, SFX_SHOT_HERO, SFX_MISSED, SFX_HEART, SFX_LOSE)
 
 SQRT2 = 2 ** 0.5
 INIT_SCORE = 2
diff --git a/brutalmaze/game.py b/brutalmaze/game.py
index e5670f8..2b7d331 100644
--- a/brutalmaze/game.py
+++ b/brutalmaze/game.py
@@ -32,11 +32,12 @@ from threading import Thread
 with redirect_stdout(StringIO()): import pygame
 from pygame import KEYDOWN, MOUSEBUTTONUP, QUIT, VIDEORESIZE
 from pygame.time import Clock, get_ticks
+from palace import free, use_context, Device, Context
 from appdirs import AppDirs
 
-from .constants import SETTINGS, ICON, NOISE, HERO_SPEED, MIDDLE
+from .constants import SETTINGS, ICON, SFX, SFX_NOISE, HERO_SPEED, MIDDLE
 from .maze import Maze
-from .misc import sign, deg, join
+from .misc import sign, deg, join, play, clean_sources
 
 
 class ConfigReader:
@@ -104,17 +105,12 @@ class ConfigReader:
 
 class Game:
     """Object handling main loop and IO."""
-    def __init__(self, config):
-        pygame.mixer.pre_init(frequency=44100)
+    def __init__(self, config: ConfigReader):
         pygame.init()
         self.headless = config.headless and config.server
-        if config.muted or self.headless:
-            pygame.mixer.quit()
-        else:
-            pygame.mixer.music.load(NOISE)
-            pygame.mixer.music.set_volume(config.musicvol)
-            pygame.mixer.music.play(-1)
-        pygame.display.set_icon(ICON)
+        if not self.headless: pygame.display.set_icon(ICON)
+        self.actx = None if self.headless else Context(Device())
+        self._mute = config.muted
 
         if config.server:
             self.server = socket()
@@ -128,8 +124,7 @@ class Game:
         else:
             self.server = self.sockinp = None
 
-        # self.fps is a float to make sure floordiv won't be used in Python 2
-        self.max_fps, self.fps = config.max_fps, float(config.max_fps)
+        self.max_fps, self.fps = config.max_fps, config.max_fps
         self.musicvol = config.musicvol
         self.touch = config.touch
         self.key, self.mouse = config.key, config.mouse
@@ -138,7 +133,35 @@ class Game:
         self.hero = self.maze.hero
         self.clock, self.paused = Clock(), False
 
-    def __enter__(self): return self
+    def __enter__(self):
+        if self.actx is not None:
+            use_context(self.actx)
+            self.actx.listener.position = MIDDLE, 0, MIDDLE
+            self.actx.listener.gain = not self._mute
+            play(SFX_NOISE)
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        if self.server is not None: self.server.close()
+        if not self.hero.dead: self.maze.dump_records()
+        if self.actx is not None:
+            free(SFX)
+            clean_sources()
+            use_context(None)
+            self.actx.destroy()
+            self.actx.device.close()
+        pygame.quit()
+
+    @property
+    def mute(self):
+        """Mute state."""
+        return getattr(self, '_mute', 1)
+
+    @mute.setter
+    def mute(self, value):
+        """Mute state."""
+        self._mute = int(bool(value))
+        self.actx.listener.gain = not self._mute
 
     def export_txt(self):
         """Export maze data to string."""
@@ -161,13 +184,7 @@ class Game:
                 self.maze.resize((event.w, event.h))
             elif event.type == KEYDOWN:
                 if event.key == self.key['mute']:
-                    if pygame.mixer.get_init() is None:
-                        pygame.mixer.init(frequency=44100)
-                        pygame.mixer.music.load(NOISE)
-                        pygame.mixer.music.set_volume(self.musicvol)
-                        pygame.mixer.music.play(-1)
-                    else:
-                        pygame.mixer.quit()
+                    self.mute ^= 1
                 elif not self.server:
                     if event.key == self.key['new']:
                         self.maze.reinit()
@@ -196,6 +213,7 @@ class Game:
         if not self.paused: self.maze.update(self.fps)
         if not self.headless: self.maze.draw()
         self.clock.tick(self.fps)
+        clean_sources()
         return True
 
     def move(self, x=0, y=0):
@@ -308,11 +326,6 @@ class Game:
             slashing = buttons[self.mouse['slash']]
         self.control(right, down, angle, firing, slashing)
 
-    def __exit__(self, exc_type, exc_value, traceback):
-        if self.server is not None: self.server.close()
-        if not self.hero.dead: self.maze.dump_records()
-        pygame.quit()
-
 
 def main():
     """Start game and main loop."""
diff --git a/brutalmaze/maze.py b/brutalmaze/maze.py
index f7ba6e7..8be20fe 100644
--- a/brutalmaze/maze.py
+++ b/brutalmaze/maze.py
@@ -29,7 +29,7 @@ import pygame
 from .characters import Hero, new_enemy
 from .constants import (
     EMPTY, WALL, HERO, ENEMY, ROAD_WIDTH, WALL_WIDTH, CELL_WIDTH, CELL_NODES,
-    MAZE_SIZE, MIDDLE, INIT_SCORE, ENEMIES, SQRT2, SFX_SPAWN,
+    MAZE_SIZE, MIDDLE, INIT_SCORE, ENEMIES, SQRT2, SFX_SPAWN, SFX_MISSED,
     SFX_SLASH_ENEMY, SFX_LOSE, ADJACENTS, TANGO_VALUES, BG_COLOR, FG_COLOR,
     COLORS, HERO_HP, ENEMY_HP, ATTACK_SPEED, MAX_WOUND, HERO_SPEED,
     BULLET_LIFETIME, JSON_SEPARATORS)
@@ -63,8 +63,6 @@ class Maze:
         glitch (float): time that the maze remain flashing colors (in ms)
         next_slashfx (float): time until next slash effect of the hero (in ms)
         slashd (float): minimum distance for slashes to be effective
-        sfx_slash (pygame.mixer.Sound): sound effect of slashed enemy
-        sfx_lose (pygame.mixer.Sound): sound effect to be played when you lose
         export (list of defaultdict): records of game states
         export_dir (str): directory containing records of game states
         export_rate (float): milliseconds per snapshot
@@ -293,7 +291,7 @@ class Maze:
             if d > 0:
                 wound = d * SQRT2 / self.distance
                 if self.next_slashfx <= 0:
-                    play(self.sfx_slash, wound, enemy.get_angle())
+                    play(SFX_SLASH_ENEMY, enemy.x, enemy.y, wound)
                     self.next_slashfx = ATTACK_SPEED
                 enemy.hit(wound / self.hero.spin_speed)
                 if enemy.wound >= ENEMY_HP:
@@ -323,7 +321,7 @@ class Maze:
                     enemy = new_enemy(self, gridx, gridy)
                     enemy.awake = True
                     self.map[gridx][gridy] = ENEMY
-                    play(self.sfx_spawn, enemy.spawn_volume, enemy.get_angle())
+                    play(SFX_SPAWN, enemy.x, enemy.y)
                     enemy.hit(wound)
                     self.enemies.append(enemy)
                     continue
@@ -334,17 +332,17 @@ class Maze:
                             self.score += enemy.wound
                             enemy.die()
                             self.add_enemy()
-                        play(bullet.sfx_hit, wound, bullet.angle)
+                        play(bullet.sfx_hit, gridx, gridy, wound)
                         fallen.append(i)
                         break
             elif bullet.get_distance(self.x, self.y) < self.distance:
                 if block:
                     self.hero.next_strike = (abs(self.hero.spin_queue/self.fps)
                                              + ATTACK_SPEED)
-                    play(bullet.sfx_missed, wound, bullet.angle + pi)
+                    play(SFX_MISSED, gain=wound)
                 else:
                     self.hit_hero(wound, bullet.color)
-                    play(bullet.sfx_hit, wound, bullet.angle + pi)
+                    play(bullet.sfx_hit, gain=wound)
                 fallen.append(i)
         for i in reversed(fallen): self.bullets.pop(i)
 
@@ -510,7 +508,7 @@ class Maze:
         self.destx = self.desty = MIDDLE
         self.stepx = self.stepy = 0
         self.vx = self.vy = 0.0
-        play(self.sfx_lose)
+        play(SFX_LOSE)
         self.dump_records()
 
     def reinit(self):
diff --git a/brutalmaze/misc.py b/brutalmaze/misc.py
index 94a2c39..92000a4 100644
--- a/brutalmaze/misc.py
+++ b/brutalmaze/misc.py
@@ -26,8 +26,9 @@ from random import shuffle
 
 import pygame
 from pygame.gfxdraw import filled_polygon, aapolygon
+from palace import Buffer, Source
 
-from .constants import ADJACENTS, CORNERS
+from .constants import ADJACENTS, CORNERS, MIDDLE
 
 
 def randsign():
@@ -81,29 +82,34 @@ def around(x, y):
     return chain(a, c)
 
 
-def play(sound, volume=1.0, angle=None):
-    """Play a pygame.mixer.Sound at the given volume."""
-    if pygame.mixer.get_init() is None: return
-    if pygame.mixer.find_channel() is None:
-        pygame.mixer.set_num_channels(pygame.mixer.get_num_channels() + 1)
-
-    channel = sound.play()
-    if angle is None:
-        channel.set_volume(volume)
-    else:
-        delta = cos(angle)
-        volumes = [volume * (1-delta), volume * (1+delta)]
-        for i, v in enumerate(volumes):
-            if v > 1:
-                volumes[i - 1] += v - 1
-                volumes[i] = 1.0
-        sound.set_volume(1.0)
-        channel.set_volume(*volumes)
-
-
 def json_rec(directory):
     """Return path to JSON file to be created inside the given directory
     based on current time local to timezone in ISO 8601 format.
     """
     return path.join(
         directory, '{}.json'.format(datetime.now().isoformat()[:19]))
+
+
+def play(sound: str, x: float = MIDDLE, y: float = MIDDLE,
+         gain: float = 1.0) -> None:
+    """Play a sound at the given position."""
+    buffer = Buffer(sound)
+    source = buffer.play()
+    source.spatialize = True
+    source.position = x, 0, y
+    source.gain = gain
+    sources.append(source)
+
+
+def clean_sources() -> None:
+    """Destroyed stopped sources."""
+    global sources
+    sources, tmp = [], sources
+    for source in tmp:
+        if source.playing:
+            sources.append(source)
+        else:
+            source.destroy()
+
+
+sources = []
diff --git a/brutalmaze/weapons.py b/brutalmaze/weapons.py
index a149841..b66ad80 100644
--- a/brutalmaze/weapons.py
+++ b/brutalmaze/weapons.py
@@ -21,7 +21,7 @@ __doc__ = 'Brutal Maze module for weapon classes'
 from math import cos, sin
 
 from .constants import (BULLET_LIFETIME, SFX_SHOT_ENEMY, SFX_SHOT_HERO,
-                        SFX_MISSED, BULLET_SPEED, ENEMY_HP, TANGO, BG_COLOR)
+                        BULLET_SPEED, ENEMY_HP, TANGO, BG_COLOR)
 from .misc import regpoly, fill_aapolygon
 
 
@@ -34,8 +34,7 @@ class Bullet:
         angle (float): angle of the direction the bullet pointing (in radians)
         color (str): bullet's color name
         fall_time (int): time until the bullet fall down
-        sfx_hit (pygame.mixer.Sound): sound effect indicating target was hit
-        sfx_missed (pygame.mixer.Sound): sound effect indicating a miss shot
+        sfx_hit (str): sound effect indicating target was hit
     """
     def __init__(self, surface, x, y, angle, color):
         self.surface = surface
@@ -45,7 +44,6 @@ class Bullet:
             self.sfx_hit = SFX_SHOT_ENEMY
         else:
             self.sfx_hit = SFX_SHOT_HERO
-        self.sfx_missed = SFX_MISSED
 
     def update(self, fps, distance):
         """Update the bullet."""
diff --git a/pyproject.toml b/pyproject.toml
index bed1ac4..8cbcad4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,7 +7,7 @@ module = 'brutalmaze'
 author = 'Nguyễn Gia Phong'
 author-email = 'mcsinyx@disroot.org'
 home-page = 'https://github.com/McSinyx/brutalmaze'
-requires = ['appdirs', 'pygame>=1.9', 'setuptools']
+requires = ['appdirs', 'palace', 'pygame>=1.9', 'setuptools']
 description-file = 'README.rst'
 classifiers = [
     'Development Status :: 4 - Beta',
@@ -28,5 +28,4 @@ license = 'AGPLv3+'
 brutalmaze = "brutalmaze.game:main"
 
 [tool.flit.sdist]
-include = ['wiki']
 exclude = ['docs']