Use palace for positional audio rendering

This fixes GH-15.  Sources doesn't seem to be cleaned up properly though.
This commit is contained in:
Nguyễn Gia Phong 2020-04-12 16:35:44 +07:00
parent c326f93bbb
commit 600c72d0d4
7 changed files with 93 additions and 87 deletions

View File

@ -25,8 +25,8 @@ from sys import modules
from .constants import ( from .constants import (
TANGO, HERO_HP, SFX_HEART, HEAL_SPEED, MIN_BEAT, ATTACK_SPEED, ENEMY, TANGO, HERO_HP, SFX_HEART, HEAL_SPEED, MIN_BEAT, ATTACK_SPEED, ENEMY,
ENEMY_SPEED, ENEMY_HP, SFX_SLASH_HERO, MIDDLE, WALL, FIRANGE, AROUND_HERO, ENEMY_SPEED, ENEMY_HP, SFX_SPAWN, SFX_SLASH_HERO, MIDDLE, WALL, FIRANGE,
ADJACENTS, EMPTY, SQRT2, ENEMIES) AROUND_HERO, ADJACENTS, EMPTY, SQRT2, ENEMIES)
from .misc import sign, randsign, regpoly, fill_aapolygon, play from .misc import sign, randsign, regpoly, fill_aapolygon, play
from .weapons import Bullet from .weapons import Bullet
@ -51,7 +51,6 @@ class Hero:
spin_queue (float): frames left to finish spinning spin_queue (float): frames left to finish spinning
wound (float): amount of wound wound (float): amount of wound
wounds (deque of float): wounds in time of an attack (ATTACK_SPEED) 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): def __init__(self, surface, fps, maze_size):
self.surface = surface self.surface = surface
@ -68,8 +67,6 @@ class Hero:
self.spin_queue = self.wound = 0.0 self.spin_queue = self.wound = 0.0
self.wounds = deque([0.0]) self.wounds = deque([0.0])
self.sfx_heart = SFX_HEART
def update(self, fps): def update(self, fps):
"""Update the hero.""" """Update the hero."""
if self.dead: if self.dead:
@ -87,7 +84,7 @@ class Hero:
if self.wound < 0: self.wound = 0.0 if self.wound < 0: self.wound = 0.0
self.wounds.append(0.0) self.wounds.append(0.0)
if self.next_beat <= 0: if self.next_beat <= 0:
play(self.sfx_heart) play(SFX_HEART)
self.next_beat = MIN_BEAT*(2 - self.wound/HERO_HP) self.next_beat = MIN_BEAT*(2 - self.wound/HERO_HP)
else: else:
self.next_beat -= 1000 / fps self.next_beat -= 1000 / fps
@ -168,7 +165,6 @@ class Enemy:
spin_speed (float): speed of spinning (in frames per slash) spin_speed (float): speed of spinning (in frames per slash)
spin_queue (float): frames left to finish spinning spin_queue (float): frames left to finish spinning
wound (float): amount of wound wound (float): amount of wound
sfx_slash (pygame.mixer.Sound): sound effect of slashed hero
""" """
def __init__(self, maze, x, y, color): def __init__(self, maze, x, y, color):
self.maze = maze self.maze = maze
@ -182,8 +178,6 @@ class Enemy:
self.spin_speed = self.maze.fps / ENEMY_HP self.spin_speed = self.maze.fps / ENEMY_HP
self.spin_queue = self.wound = 0.0 self.spin_queue = self.wound = 0.0
self.sfx_slash = SFX_SLASH_HERO
@property @property
def pos(self): def pos(self):
"""Coordinates (in pixels) of the center of the enemy.""" """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 if self.maze.map[srcx+i//w][srcy+i//u] == WALL: return False
self.awake = True self.awake = True
self.maze.map[self.x][self.y] = ENEMY 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 return True
def fire(self): def fire(self):
@ -317,7 +311,7 @@ class Enemy:
if not self.spin_queue and not self.fire() and not self.move(): if not self.spin_queue and not self.fire() and not self.move():
self.spin_queue = randsign() * self.spin_speed self.spin_queue = randsign() * self.spin_speed
if not self.maze.hero.dead: 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: if round(self.spin_queue) != 0:
self.angle += sign(self.spin_queue) * pi / 2 / self.spin_speed self.angle += sign(self.spin_queue) * pi / 2 / self.spin_speed
self.spin_queue -= sign(self.spin_queue) self.spin_queue -= sign(self.spin_queue)

View File

@ -20,25 +20,23 @@ __doc__ = 'Brutal Maze module for shared constants'
from string import ascii_lowercase from string import ascii_lowercase
from pkg_resources import resource_filename as pkg_file
import pygame import pygame
from pygame.mixer import Sound from pkg_resources import resource_filename as pkg_file
SETTINGS = pkg_file('brutalmaze', 'settings.ini') SETTINGS = pkg_file('brutalmaze', 'settings.ini')
ICON = pygame.image.load(pkg_file('brutalmaze', 'icon.png')) ICON = pygame.image.load(pkg_file('brutalmaze', 'icon.png'))
NOISE = pkg_file('brutalmaze', 'soundfx/noise.ogg')
mixer = pygame.mixer.get_init() SFX_NOISE = pkg_file('brutalmaze', 'soundfx/noise.ogg')
if mixer is None: pygame.mixer.init(frequency=44100) SFX_SPAWN = pkg_file('brutalmaze', 'soundfx/spawn.ogg')
SFX_SPAWN = Sound(pkg_file('brutalmaze', 'soundfx/spawn.ogg')) SFX_SLASH_ENEMY = pkg_file('brutalmaze', 'soundfx/slash-enemy.ogg')
SFX_SLASH_ENEMY = Sound(pkg_file('brutalmaze', 'soundfx/slash-enemy.ogg')) SFX_SLASH_HERO = pkg_file('brutalmaze', 'soundfx/slash-hero.ogg')
SFX_SLASH_HERO = Sound(pkg_file('brutalmaze', 'soundfx/slash-hero.ogg')) SFX_SHOT_ENEMY = pkg_file('brutalmaze', 'soundfx/shot-enemy.ogg')
SFX_SHOT_ENEMY = Sound(pkg_file('brutalmaze', 'soundfx/shot-enemy.ogg')) SFX_SHOT_HERO = pkg_file('brutalmaze', 'soundfx/shot-hero.ogg')
SFX_SHOT_HERO = Sound(pkg_file('brutalmaze', 'soundfx/shot-hero.ogg')) SFX_MISSED = pkg_file('brutalmaze', 'soundfx/missed.ogg')
SFX_MISSED = Sound(pkg_file('brutalmaze', 'soundfx/missed.ogg')) SFX_HEART = pkg_file('brutalmaze', 'soundfx/heart.ogg')
SFX_HEART = Sound(pkg_file('brutalmaze', 'soundfx/heart.ogg')) SFX_LOSE = pkg_file('brutalmaze', 'soundfx/lose.ogg')
SFX_LOSE = Sound(pkg_file('brutalmaze', 'soundfx/lose.ogg')) SFX = (SFX_NOISE, SFX_SPAWN, SFX_SLASH_ENEMY, SFX_SLASH_HERO,
if mixer is None: pygame.mixer.quit() SFX_SHOT_ENEMY, SFX_SHOT_HERO, SFX_MISSED, SFX_HEART, SFX_LOSE)
SQRT2 = 2 ** 0.5 SQRT2 = 2 ** 0.5
INIT_SCORE = 2 INIT_SCORE = 2

View File

@ -32,11 +32,12 @@ from threading import Thread
with redirect_stdout(StringIO()): import pygame with redirect_stdout(StringIO()): import pygame
from pygame import KEYDOWN, MOUSEBUTTONUP, QUIT, VIDEORESIZE from pygame import KEYDOWN, MOUSEBUTTONUP, QUIT, VIDEORESIZE
from pygame.time import Clock, get_ticks from pygame.time import Clock, get_ticks
from palace import free, use_context, Device, Context
from appdirs import AppDirs 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 .maze import Maze
from .misc import sign, deg, join from .misc import sign, deg, join, play, clean_sources
class ConfigReader: class ConfigReader:
@ -104,17 +105,12 @@ class ConfigReader:
class Game: class Game:
"""Object handling main loop and IO.""" """Object handling main loop and IO."""
def __init__(self, config): def __init__(self, config: ConfigReader):
pygame.mixer.pre_init(frequency=44100)
pygame.init() pygame.init()
self.headless = config.headless and config.server self.headless = config.headless and config.server
if config.muted or self.headless: if not self.headless: pygame.display.set_icon(ICON)
pygame.mixer.quit() self.actx = None if self.headless else Context(Device())
else: self._mute = config.muted
pygame.mixer.music.load(NOISE)
pygame.mixer.music.set_volume(config.musicvol)
pygame.mixer.music.play(-1)
pygame.display.set_icon(ICON)
if config.server: if config.server:
self.server = socket() self.server = socket()
@ -128,8 +124,7 @@ class Game:
else: else:
self.server = self.sockinp = None 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, config.max_fps
self.max_fps, self.fps = config.max_fps, float(config.max_fps)
self.musicvol = config.musicvol self.musicvol = config.musicvol
self.touch = config.touch self.touch = config.touch
self.key, self.mouse = config.key, config.mouse self.key, self.mouse = config.key, config.mouse
@ -138,7 +133,35 @@ class Game:
self.hero = self.maze.hero self.hero = self.maze.hero
self.clock, self.paused = Clock(), False 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): def export_txt(self):
"""Export maze data to string.""" """Export maze data to string."""
@ -161,13 +184,7 @@ class Game:
self.maze.resize((event.w, event.h)) self.maze.resize((event.w, event.h))
elif event.type == KEYDOWN: elif event.type == KEYDOWN:
if event.key == self.key['mute']: if event.key == self.key['mute']:
if pygame.mixer.get_init() is None: self.mute ^= 1
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()
elif not self.server: elif not self.server:
if event.key == self.key['new']: if event.key == self.key['new']:
self.maze.reinit() self.maze.reinit()
@ -196,6 +213,7 @@ class Game:
if not self.paused: self.maze.update(self.fps) if not self.paused: self.maze.update(self.fps)
if not self.headless: self.maze.draw() if not self.headless: self.maze.draw()
self.clock.tick(self.fps) self.clock.tick(self.fps)
clean_sources()
return True return True
def move(self, x=0, y=0): def move(self, x=0, y=0):
@ -308,11 +326,6 @@ class Game:
slashing = buttons[self.mouse['slash']] slashing = buttons[self.mouse['slash']]
self.control(right, down, angle, firing, slashing) 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(): def main():
"""Start game and main loop.""" """Start game and main loop."""

View File

@ -29,7 +29,7 @@ import pygame
from .characters import Hero, new_enemy from .characters import Hero, new_enemy
from .constants import ( from .constants import (
EMPTY, WALL, HERO, ENEMY, ROAD_WIDTH, WALL_WIDTH, CELL_WIDTH, CELL_NODES, 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, SFX_SLASH_ENEMY, SFX_LOSE, ADJACENTS, TANGO_VALUES, BG_COLOR, FG_COLOR,
COLORS, HERO_HP, ENEMY_HP, ATTACK_SPEED, MAX_WOUND, HERO_SPEED, COLORS, HERO_HP, ENEMY_HP, ATTACK_SPEED, MAX_WOUND, HERO_SPEED,
BULLET_LIFETIME, JSON_SEPARATORS) BULLET_LIFETIME, JSON_SEPARATORS)
@ -63,8 +63,6 @@ class Maze:
glitch (float): time that the maze remain flashing colors (in ms) glitch (float): time that the maze remain flashing colors (in ms)
next_slashfx (float): time until next slash effect of the hero (in ms) next_slashfx (float): time until next slash effect of the hero (in ms)
slashd (float): minimum distance for slashes to be effective 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 (list of defaultdict): records of game states
export_dir (str): directory containing records of game states export_dir (str): directory containing records of game states
export_rate (float): milliseconds per snapshot export_rate (float): milliseconds per snapshot
@ -293,7 +291,7 @@ class Maze:
if d > 0: if d > 0:
wound = d * SQRT2 / self.distance wound = d * SQRT2 / self.distance
if self.next_slashfx <= 0: 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 self.next_slashfx = ATTACK_SPEED
enemy.hit(wound / self.hero.spin_speed) enemy.hit(wound / self.hero.spin_speed)
if enemy.wound >= ENEMY_HP: if enemy.wound >= ENEMY_HP:
@ -323,7 +321,7 @@ class Maze:
enemy = new_enemy(self, gridx, gridy) enemy = new_enemy(self, gridx, gridy)
enemy.awake = True enemy.awake = True
self.map[gridx][gridy] = ENEMY 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) enemy.hit(wound)
self.enemies.append(enemy) self.enemies.append(enemy)
continue continue
@ -334,17 +332,17 @@ class Maze:
self.score += enemy.wound self.score += enemy.wound
enemy.die() enemy.die()
self.add_enemy() self.add_enemy()
play(bullet.sfx_hit, wound, bullet.angle) play(bullet.sfx_hit, gridx, gridy, wound)
fallen.append(i) fallen.append(i)
break break
elif bullet.get_distance(self.x, self.y) < self.distance: elif bullet.get_distance(self.x, self.y) < self.distance:
if block: if block:
self.hero.next_strike = (abs(self.hero.spin_queue/self.fps) self.hero.next_strike = (abs(self.hero.spin_queue/self.fps)
+ ATTACK_SPEED) + ATTACK_SPEED)
play(bullet.sfx_missed, wound, bullet.angle + pi) play(SFX_MISSED, gain=wound)
else: else:
self.hit_hero(wound, bullet.color) self.hit_hero(wound, bullet.color)
play(bullet.sfx_hit, wound, bullet.angle + pi) play(bullet.sfx_hit, gain=wound)
fallen.append(i) fallen.append(i)
for i in reversed(fallen): self.bullets.pop(i) for i in reversed(fallen): self.bullets.pop(i)
@ -510,7 +508,7 @@ class Maze:
self.destx = self.desty = MIDDLE self.destx = self.desty = MIDDLE
self.stepx = self.stepy = 0 self.stepx = self.stepy = 0
self.vx = self.vy = 0.0 self.vx = self.vy = 0.0
play(self.sfx_lose) play(SFX_LOSE)
self.dump_records() self.dump_records()
def reinit(self): def reinit(self):

View File

@ -26,8 +26,9 @@ from random import shuffle
import pygame import pygame
from pygame.gfxdraw import filled_polygon, aapolygon 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(): def randsign():
@ -81,29 +82,34 @@ def around(x, y):
return chain(a, c) 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): def json_rec(directory):
"""Return path to JSON file to be created inside the given directory """Return path to JSON file to be created inside the given directory
based on current time local to timezone in ISO 8601 format. based on current time local to timezone in ISO 8601 format.
""" """
return path.join( return path.join(
directory, '{}.json'.format(datetime.now().isoformat()[:19])) 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 = []

View File

@ -21,7 +21,7 @@ __doc__ = 'Brutal Maze module for weapon classes'
from math import cos, sin from math import cos, sin
from .constants import (BULLET_LIFETIME, SFX_SHOT_ENEMY, SFX_SHOT_HERO, 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 from .misc import regpoly, fill_aapolygon
@ -34,8 +34,7 @@ class Bullet:
angle (float): angle of the direction the bullet pointing (in radians) angle (float): angle of the direction the bullet pointing (in radians)
color (str): bullet's color name color (str): bullet's color name
fall_time (int): time until the bullet fall down fall_time (int): time until the bullet fall down
sfx_hit (pygame.mixer.Sound): sound effect indicating target was hit sfx_hit (str): sound effect indicating target was hit
sfx_missed (pygame.mixer.Sound): sound effect indicating a miss shot
""" """
def __init__(self, surface, x, y, angle, color): def __init__(self, surface, x, y, angle, color):
self.surface = surface self.surface = surface
@ -45,7 +44,6 @@ class Bullet:
self.sfx_hit = SFX_SHOT_ENEMY self.sfx_hit = SFX_SHOT_ENEMY
else: else:
self.sfx_hit = SFX_SHOT_HERO self.sfx_hit = SFX_SHOT_HERO
self.sfx_missed = SFX_MISSED
def update(self, fps, distance): def update(self, fps, distance):
"""Update the bullet.""" """Update the bullet."""

View File

@ -7,7 +7,7 @@ module = 'brutalmaze'
author = 'Nguyễn Gia Phong' author = 'Nguyễn Gia Phong'
author-email = 'mcsinyx@disroot.org' author-email = 'mcsinyx@disroot.org'
home-page = 'https://github.com/McSinyx/brutalmaze' home-page = 'https://github.com/McSinyx/brutalmaze'
requires = ['appdirs', 'pygame>=1.9', 'setuptools'] requires = ['appdirs', 'palace', 'pygame>=1.9', 'setuptools']
description-file = 'README.rst' description-file = 'README.rst'
classifiers = [ classifiers = [
'Development Status :: 4 - Beta', 'Development Status :: 4 - Beta',
@ -28,5 +28,4 @@ license = 'AGPLv3+'
brutalmaze = "brutalmaze.game:main" brutalmaze = "brutalmaze.game:main"
[tool.flit.sdist] [tool.flit.sdist]
include = ['wiki']
exclude = ['docs'] exclude = ['docs']