pgfw/pgfw/Audio.py

626 lines
25 KiB
Python

# -*- coding: utf-8 -*-
import os, re, shutil, pygame
from .GameChild import *
from .Sprite import *
from .Input import *
from .Animation import *
from .extension import *
class Audio(GameChild):
UP, DOWN = .1, -.1
BASE_VOLUME = 1.0
def __init__(self, game):
GameChild.__init__(self, game)
# self.original_volumes = {}
self.volume = self.BASE_VOLUME
if self.check_command_line("-mute"):
self.volume = 0
self.audio_panel = AudioPanel(self)
self.subscribe(self.respond)
self.sfx = {}
self.load_sfx()
def set_volume(self, volume=None, increment=None, mute=False):
if mute:
self.volume = 0
elif increment:
self.volume += increment
if self.volume > 1:
self.volume = 1.0
elif self.volume < 0:
self.volume = 0
else:
self.volume = volume
def respond(self, event):
compare = self.get_game().delegate.compare
if compare(event, "volume-mute"):
self.set_volume(mute=True)
elif compare(event, "volume-up"):
self.set_volume(increment=self.UP)
elif compare(event, "volume-down"):
self.set_volume(increment=self.DOWN)
#
# Loading SFX procedure
#
# - load config file name/path definitions at init
# - check project specific sfx paths at init, load any that don't conflict
# - check default sfx paths at init, load any that don't conflict
# - repository paths are not loaded at init but can replace loaded paths
# and get written to config file
#
def load_sfx(self, sfx_location=None):
for name, sfx_definition in self.get_configuration("sfx").items():
sfx_definition_members = sfx_definition.split(",")
path, volume, fade_out, loops, maxtime = sfx_definition_members[0], 1.0, 0, 0, 0
for ii, member in enumerate(sfx_definition_members[1:]):
if ii == 0:
volume = float(member)
elif ii == 1:
fade_out = float(member)
elif ii == 2:
loops = int(member)
elif ii == 3:
maxtime = float(member)
self.load_sfx_file(
path, name, True, volume=volume, fade_out=fade_out, loops=loops,
maxtime=maxtime)
if sfx_location is None:
sfx_location = self.get_configuration("audio", "sfx-project-path") + \
self.get_configuration("audio", "sfx-default-path")
if isinstance(sfx_location, str):
sfx_location = [sfx_location]
for root in sfx_location:
prefix = ""
root = self.get_resource(root)
print("checking {} for sound effects".format(root))
if root:
if os.path.isfile(root):
self.load_sfx_file(root)
else:
for node, branches, leaves in os.walk(root, followlinks=True):
for leaf in leaves:
prefix = re.sub(root, "", node)
prefix = re.sub("^/", "", prefix)
if prefix:
prefix = re.sub("/", "_", prefix) + "_"
self.load_sfx_file(os.path.join(node, leaf,), prefix=prefix)
def load_sfx_file(self, path, name=None, replace=False, prefix="",
volume=1.0, fade_out=0, loops=0, maxtime=0):
path = self.get_resource(path)
if path and path.split(".")[-1] in self.get_configuration("audio", "sfx-extensions"):
if name is None:
name = prefix + re.sub("\.[^.]*$", "", os.path.basename(path))
if not replace and name in self.sfx:
print("skipping existing sound effect for {}: {}".format(name, path))
else:
print("loading sound effect {} into {}".format(path, name))
self.sfx[name] = SoundEffect(
self, path, volume, loops, fade_out, maxtime=maxtime)
return True
return False
def update(self):
# for ii in range(pygame.mixer.get_num_channels()):
# channel = pygame.mixer.Channel(ii)
# sound = channel.get_sound()
# if sound is not None:
# if sound not in self.original_volumes.keys():
# self.original_volumes[sound] = sound.get_volume()
# sound.set_volume(self.original_volumes[sound] * self.volume)
self.audio_panel.update()
class SoundEffect(GameChild, pygame.mixer.Sound):
def __init__(self, parent, path, volume=1.0, loops=0, fade_out_length=0,
fade_in_length=0, maxtime=0):
self.path = path
GameChild.__init__(self, parent)
pygame.mixer.Sound.__init__(self, path)
self.display_surface = self.get_display_surface()
self.local_volume = volume
self.loops = loops
self.fade_out_length = fade_out_length
self.fade_in_length = fade_in_length
self.maxtime = maxtime
def play(self, loops=None, maxtime=None, fade_ms=None, position=None,
x=None):
self.set_volume(
self.local_volume * self.get_configuration("audio", "sfx-volume"))
if loops is None:
loops = self.loops
if maxtime is None:
maxtime = int(self.maxtime * 1000)
if fade_ms is None:
fade_ms = int(self.fade_in_length * 1000)
channel = pygame.mixer.Sound.play(self, loops, maxtime, fade_ms)
if x is not None:
position = float(x) / self.display_surface.get_width()
if position is not None and channel is not None:
channel.set_volume(*self.get_panning(position))
if self.fade_out_length > 0:
self.fadeout(int(self.fade_out_length * 1000))
return channel
def get_panning(self, position):
return 1 - max(0, ((position - .5) * 2)), \
1 + min(0, ((position - .5) * 2))
def adjust_volume(self, increment):
self.local_volume += increment
if self.local_volume > 1.0:
self.local_volume = 1.0
elif self.local_volume < 0:
self.local_volume = 0
return self.local_volume
def adjust_loop_count(self, increment):
self.loops += increment
if self.loops < -1:
self.loops = -1
return self.loops
def adjust_fade_in_length(self, increment):
self.fade_in_length += increment
limit = self.get_length() * (self.loops + 1)
if self.fade_in_length < 0:
self.fade_in_length = 0
elif self.loops > -1 and self.fade_in_length > limit:
self.fade_in_length = limit
return self.fade_in_length
def adjust_fade_out_length(self, increment):
self.fade_out_length += increment
limit = self.get_length() * (self.loops + 1)
if self.fade_out_length < 0:
self.fade_out_length = 0
elif self.loops > -1 and self.fade_out_length > limit:
self.fade_out_length = limit
return self.fade_out_length
def adjust_maxtime(self, increment):
self.maxtime += increment
limit = self.get_length() * (self.loops + 1)
if self.maxtime < 0:
self.maxtime = 0
elif self.loops > -1 and self.maxtime > limit:
self.maxtime = limit
return self.maxtime
class AudioPanel(Animation):
MARGIN = 6
def __init__(self, parent):
Animation.__init__(self, parent)
self.rows = []
font_path = self.get_resource(self.get_configuration("audio", "panel-font"))
self.font_large = pygame.font.Font(font_path, 15)
self.font_medium = pygame.font.Font(font_path, 12)
self.font_small = pygame.font.Font(font_path, 8)
self.file_browser = AudioPanelFileBrowser(self)
self.subscribe(self.respond)
self.subscribe(self.respond, pygame.MOUSEBUTTONDOWN)
self.reset()
def reset(self):
self.row_offset = 0
self.deactivate()
def get_selected(self):
for row in self.rows:
if row.selected:
return row
def activate(self):
pygame.mouse.set_visible(True)
self.active = True
# self.build()
def deactivate(self):
pygame.mouse.set_visible(self.get_configuration("mouse", "visible"))
self.active = False
def respond(self, event):
if self.get_delegate().compare(event, "toggle-audio-panel") and \
pygame.key.get_mods() & KMOD_CTRL and pygame.key.get_mods() & KMOD_SHIFT and \
self.get_audio().sfx:
if self.active:
self.deactivate()
else:
self.activate()
if not self.rows:
self.build()
elif self.active:
if event.type == pygame.MOUSEBUTTONDOWN and self.file_browser.is_hidden():
if event.button == 5:
self.row_offset += 1
elif event.button == 4:
self.row_offset -= 1
def build(self):
for row in self.rows:
row.unsubscribe()
del row
self.rows = []
for key in sorted(self.parent.sfx):
self.rows.append(AudioPanelRow(self, key))
def update(self):
if self.active:
Animation.update(self)
ds = self.get_display_surface()
dsr = ds.get_rect()
corner = Vector(self.MARGIN, self.MARGIN)
index = self.row_offset
for row in self.rows:
row.location.bottom = 0
row.update()
while corner.y < dsr.height - self.MARGIN:
row = self.rows[index % len(self.rows)]
row.location.topleft = corner.copy()
row.update()
corner.y += row.location.height + self.MARGIN
index += 1
self.file_browser.update()
class AudioPanelRow(BlinkingSprite):
BACKGROUND = pygame.Color(255, 255, 255, 210)
FOREGROUND = pygame.Color(0, 0, 0, 210)
WIDTH = .5
HEIGHT = 30
INDENT = 4
MAX_NAME_WIDTH = .7
SLIDER_W = 60
BUTTON_W = 30
def __init__(self, parent, key):
BlinkingSprite.__init__(self, parent, 500)
self.key = key
self.selected = False
self.font = self.parent.font_large
self.build()
font_medium = self.parent.font_medium
font_small = self.parent.font_small
self.volume_spinner = AudioPanelSpinner(
self, font_medium, font_small, self.SLIDER_W, self.location.h, .05,
self.get_sound_effect().local_volume,
self.get_sound_effect().adjust_volume, self.FOREGROUND,
self.BACKGROUND, 2, "vol")
self.fade_out_spinner = AudioPanelSpinner(
self, font_medium, font_small, self.SLIDER_W, self.location.h, .1,
self.get_sound_effect().fade_out_length,
self.get_sound_effect().adjust_fade_out_length, self.FOREGROUND,
self.BACKGROUND, 1, "fade")
self.loops_spinner = AudioPanelSpinner(
self, font_medium, font_small, self.SLIDER_W, self.location.h, 1,
self.get_sound_effect().loops,
self.get_sound_effect().adjust_loop_count, self.FOREGROUND,
self.BACKGROUND, 0, "loops")
self.maxtime_spinner = AudioPanelSpinner(
self, font_medium, font_small, self.SLIDER_W, self.location.h, .1,
self.get_sound_effect().maxtime,
self.get_sound_effect().adjust_maxtime, self.FOREGROUND,
self.BACKGROUND, 1, "cutoff")
self.play_button = AudioPanelButton(self, self.get_sound_effect().play)
frame = pygame.Surface((self.BUTTON_W, self.location.h), SRCALPHA)
frame.fill(self.BACKGROUND)
stop_button_frame = frame.copy()
w, h = frame.get_size()
pygame.draw.polygon(frame, self.FOREGROUND, ((w * .25, h * .25), (w * .25, h * .75), (w * .75, h * .5)))
self.play_button.add_frame(frame)
self.stop_button = AudioPanelButton(self, self.get_sound_effect().stop)
stop_button_frame.fill(self.FOREGROUND, (w * .25, h * .25, w * .5, h * .5))
self.stop_button.add_frame(stop_button_frame)
self.stop_blinking()
self.subscribe(self.respond, pygame.MOUSEBUTTONDOWN)
def respond(self, event):
if event.button == 1:
if self.parent.file_browser.is_hidden() and self.location.collidepoint(event.pos):
if not self.selected:
self.parent.file_browser.visit(self.parent.file_browser.HOME)
self.selected = True
self.start_blinking()
self.parent.file_browser.unhide()
elif self.parent.file_browser.is_hidden():
if self.selected:
self.selected = False
self.stop_blinking()
def unsubscribe(self, callback=None, kind=None):
if callback is None:
callback = self.respond
kind = pygame.MOUSEBUTTONDOWN
GameChild.unsubscribe(self, self.respond, pygame.MOUSEBUTTONDOWN)
self.play_button.unsubscribe()
self.stop_button.unsubscribe()
for spinner in self.volume_spinner, self.fade_out_spinner, self.loops_spinner, self.maxtime_spinner:
spinner.unsubscribe()
def build(self):
ds = self.get_display_surface()
dsr = ds.get_rect()
surface = pygame.Surface((dsr.w * self.WIDTH, self.HEIGHT), pygame.SRCALPHA)
surface.fill(self.BACKGROUND)
self.add_frame(surface)
name_sprite = Sprite(self)
name = self.font.render(self.key + ":", True, self.FOREGROUND)
if name.get_width() > int(self.location.w * self.MAX_NAME_WIDTH):
crop = pygame.Rect(0, 0, int(self.location.w * self.MAX_NAME_WIDTH), name.get_height())
crop.right = name.get_rect().right
name = name.subsurface(crop)
name_sprite.add_frame(name)
name_sprite.display_surface = surface
name_sprite.location.midleft = self.INDENT, self.location.centery
name_sprite.update()
file_sprite = Sprite(self)
box = get_boxed_surface(
pygame.Surface((self.location.w - name_sprite.location.w - self.INDENT * 3,
self.location.height - 4), pygame.SRCALPHA),
border=self.FOREGROUND)
file_sprite.add_frame(box)
file_sprite.location.midright = self.location.right - self.INDENT, self.location.centery
file_sprite.display_surface = surface
file_name_sprite = Sprite(self)
file_name_text = self.font.render(self.get_sound_effect().path, True, self.FOREGROUND)
file_name_sprite.add_frame(file_name_text)
file_name_sprite.display_surface = box
file_name_sprite.location.midright = file_sprite.location.w - self.INDENT, file_sprite.location.h / 2
file_name_sprite.update()
file_sprite.update()
def get_sound_effect(self):
return self.get_game().get_audio().sfx[self.key]
def update_config(self):
if not self.get_configuration().has_section("sfx"):
self.get_configuration().add_section("sfx")
sound_effect = self.get_sound_effect()
self.get_configuration().set(
"sfx", self.key, "{}, {:.2f}, {:.2f}, {}, {:.2f}".format(
sound_effect.path, sound_effect.local_volume, sound_effect.fade_out_length,
sound_effect.loops, sound_effect.maxtime))
config_path = self.get_configuration().locate_project_config_file()
backup_path = config_path + ".backup"
shutil.copyfile(config_path, backup_path)
self.get_configuration().write(open(config_path, "w"))
def update(self):
self.volume_spinner.location.midleft = self.location.move(5, 0).midright
self.fade_out_spinner.location.midleft = self.volume_spinner.location.midright
self.loops_spinner.location.midleft = self.fade_out_spinner.location.midright
self.maxtime_spinner.location.midleft = self.loops_spinner.location.midright
self.play_button.location.midleft = self.maxtime_spinner.location.move(5, 0).midright
self.stop_button.location.midleft = self.play_button.location.midright
Sprite.update(self)
self.volume_spinner.update()
self.fade_out_spinner.update()
self.loops_spinner.update()
self.maxtime_spinner.update()
self.play_button.update()
self.stop_button.update()
class AudioPanelFileBrowser(Sprite):
WIDTH = .75
HEIGHT = .75
COLORS = pygame.Color(255, 255, 255), pygame.Color(0, 0, 0)
HOME, UP = "[HOME]", "[UP]"
def __init__(self, parent):
Sprite.__init__(self, parent)
self.font = self.parent.font_large
ds = self.get_display_surface()
dsr = ds.get_rect()
surface = pygame.Surface((dsr.w * self.WIDTH - 2, dsr.h * self.HEIGHT - 2), SRCALPHA)
surface.fill(self.COLORS[0])
self.background = get_boxed_surface(surface, self.COLORS[0], self.COLORS[1])
self.add_frame(self.background.copy())
self.location.center = dsr.center
self.reset()
self.subscribe(self.respond, pygame.MOUSEBUTTONDOWN)
def reset(self):
self.visit(self.HOME)
self.hide()
def respond(self, event):
if not self.is_hidden():
if event.button == 1:
if self.collide(event.pos):
for row in self.rows:
if row.collide(Vector(*event.pos).get_moved(-self.location.left, -self.location.top)):
full_path = os.path.join(os.path.sep.join(self.trail), row.path)
if row.path == self.HOME or row.path == self.UP or \
os.path.isdir(full_path) and os.access(full_path, os.R_OK):
self.visit(row.path)
elif os.path.isfile(full_path) and os.access(full_path, os.R_OK):
selected = self.parent.get_selected()
if self.get_audio().load_sfx_file(full_path, selected.key, True):
selected.update_config()
self.hide()
self.get_delegate().cancel_propagation()
self.parent.build()
else:
self.hide()
self.get_delegate().cancel_propagation()
elif event.button == 4:
self.row_offset -= 1
elif event.button == 5:
self.row_offset += 1
def hide(self):
for row in self.parent.rows:
row.selected = False
row.stop_blinking()
Sprite.hide(self)
def visit(self, path):
if path == self.UP and len(self.trail) > 1:
path = self.trail[-2]
self.trail = self.trail[:-2]
self.visit(path)
elif path != self.UP:
self.row_offset = 0
if path == self.HOME:
self.trail = []
self.paths = ["/"]
for option in "sfx-repository-path", "sfx-default-path", "sfx-project-path":
for sfx_location in self.get_configuration("audio", option):
if self.get_resource(sfx_location):
self.paths.append(self.get_resource(sfx_location))
else:
self.paths = [self.HOME]
self.trail.append(path)
if len(self.trail) > 1:
self.paths.append(self.UP)
self.paths.extend(sorted(os.listdir(os.path.sep.join(self.trail))))
self.build()
def build(self):
self.rows = []
for path in self.paths:
row = Sprite(self)
row.path = path
text = self.font.render(path, True, self.COLORS[1])
surface = pygame.Surface((self.location.w, text.get_height()), SRCALPHA)
surface.blit(text, (0, 0))
row.add_frame(surface)
row.display_surface = self.get_current_frame()
row.location.bottom = 0
self.rows.append(row)
def update(self):
self.get_current_frame().blit(self.background, (0, 0))
if not self.is_hidden():
corner = Vector(1, 1)
index = self.row_offset
for row in self.rows:
row.remove_locations()
row.location.bottom = 0
while corner.y < self.location.h:
row = self.rows[index % len(self.rows)]
if index - self.row_offset >= len(self.rows):
row.add_location(corner.copy())
else:
row.location.topleft = corner.copy()
corner.y += row.location.height
index += 1
for row in self.rows:
row.update()
Sprite.update(self)
class AudioPanelSpinner(Sprite):
def __init__(self, parent, font, label_font, width=80, height=48,
magnitude=1, value=0, callback=None,
foreground=pygame.Color(0, 0, 0),
background=pygame.Color(255, 255, 255), precision=0,
label_text=""):
Sprite.__init__(self, parent)
self.magnitude, self.value = magnitude, value
self.background, self.foreground = background, foreground
self.precision = precision
self.callback = callback
self.font = font
self.label_font = label_font
surface = pygame.Surface((width, height), SRCALPHA)
surface.fill(background)
self.add_frame(surface)
self.label = Sprite(self)
self.label.add_frame(self.label_font.render(label_text, True, foreground))
self.label.display_surface = self.get_current_frame()
self.display = Sprite(self)
self.display.display_surface = self.get_current_frame()
self.update_display()
self.up_button = Sprite(self)
self.up_button.add_frame(render_box(
width=width - self.display.location.w - 2, height=int(height * .5) - 2,
color=foreground, border=foreground, font=self.font, text="+"))
self.up_button.location.left = self.display.location.right - 1
self.up_button.display_surface = self.get_current_frame()
self.down_button = Sprite(self)
self.down_button.add_frame(render_box(
width=self.up_button.location.w - 2, height=self.up_button.location.h - 1,
border=foreground, font=self.font, text="-"))
self.down_button.location.topleft = self.display.location.right - 1, \
self.up_button.location.bottom - 1
self.down_button.display_surface = self.get_current_frame()
self.subscribe(self.respond, pygame.MOUSEBUTTONDOWN)
def unsubscribe(self, callback=None, kind=None):
if callback is None:
callback = self.respond
kind = pygame.MOUSEBUTTONDOWN
GameChild.unsubscribe(self, callback, kind)
def update_display(self):
self.display.clear_frames()
self.display.add_frame(render_box(
width=int(self.location.w * .7) - 2,
border=self.foreground, font=self.font, text="{:.{precision}f}".format(
self.value, precision=self.precision)))
self.display.location.bottomleft = 0, self.location.h
def increment(self, up=True):
step = self.magnitude * [-1, 1][up]
self.value += step
if self.callback is not None:
response = self.callback(step)
if response is not None:
self.value = response
self.update_display()
def respond(self, event):
if event.button == 1:
relative_position = Vector(*event.pos).get_moved(
-self.location.left, -self.location.top)
up_collides = self.up_button.collide(relative_position)
down_collides = self.down_button.collide(relative_position)
if up_collides or down_collides:
if up_collides:
self.increment()
else:
self.increment(False)
self.parent.update_config()
def update(self):
self.get_current_frame().fill(self.background)
self.label.update()
self.up_button.update()
self.down_button.update()
self.display.update()
Sprite.update(self)
class AudioPanelButton(Sprite):
def __init__(self, parent, callback):
Sprite.__init__(self, parent)
self.callback = callback
self.subscribe(self.respond, pygame.MOUSEBUTTONDOWN)
def unsubscribe(self, callback=None, kind=None):
if callback is None:
callback = self.respond
kind = pygame.MOUSEBUTTONDOWN
Sprite.unsubscribe(self, callback, kind)
def respond(self, event):
if event.button == 1 and self.collide(event.pos):
self.callback()