912 lines
38 KiB
Python
912 lines
38 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import os, re, shutil, pygame, sys, collections
|
|
|
|
from .GameChild import *
|
|
from .Sprite import *
|
|
from .Input import *
|
|
from .Animation import *
|
|
from .extension import *
|
|
|
|
class Audio(Animation):
|
|
|
|
UP, DOWN = .1, -.1
|
|
CONFIG_SEPARATOR = ","
|
|
|
|
def __init__(self, game):
|
|
Animation.__init__(self, game)
|
|
self.current_bgm = None
|
|
self.volume = 1.0
|
|
self.pre_muted_volume = 1.0
|
|
if self.check_command_line("-mute"):
|
|
self.get_configuration().set("audio", "volume", 0)
|
|
self.register(self.play_sfx)
|
|
if self.get_configuration("audio", "panel-enabled"):
|
|
self.audio_panel = AudioPanel(self)
|
|
else:
|
|
self.audio_panel = None
|
|
self.subscribe(self.respond)
|
|
self.sfx = {}
|
|
self.bgm = {}
|
|
if self.get_configuration("audio", "auto-load"):
|
|
self.load_sfx()
|
|
self.load_bgm()
|
|
self.set_volume(self.get_configuration("audio", "volume"))
|
|
|
|
def set_volume(self, volume=None, increment=None, mute=False, unmute=False):
|
|
if mute:
|
|
self.pre_muted_volume = self.volume
|
|
self.volume = 0
|
|
elif unmute and self.pre_muted_volume is not None:
|
|
self.volume = self.pre_muted_volume
|
|
self.pre_muted_volume = None
|
|
elif increment:
|
|
self.volume = clamp(self.volume + increment, 0, 1.0)
|
|
else:
|
|
self.volume = volume
|
|
self.get_configuration().set("audio", "volume", self.volume)
|
|
if pygame.mixer.music.get_busy():
|
|
pygame.mixer.music.set_volume(self.current_bgm.volume * self.volume)
|
|
for ii in range(pygame.mixer.get_num_channels()):
|
|
channel = pygame.mixer.Channel(ii)
|
|
if channel.get_busy():
|
|
channel.set_volume(channel.get_sound().get_volume() * self.volume)
|
|
|
|
def get_volume(self):
|
|
return self.volume
|
|
|
|
def set_channel_volume(self, channel, *args):
|
|
'''
|
|
Set channel volume taking global volume into account. One or two values can be passed into *args.
|
|
A single value will affect both left and right speakers. Two values will be used as the left and right
|
|
speakers separately. This is the behavior of pygame's Channel.set_volume method
|
|
'''
|
|
for ii in range(len(args)):
|
|
args[ii] *= self.volume
|
|
channel.set_volume(*args)
|
|
|
|
def respond(self, event):
|
|
compare = self.get_game().delegate.compare
|
|
if compare(event, "volume-mute"):
|
|
if self.volume > 0:
|
|
self.set_volume(mute=True)
|
|
else:
|
|
self.set_volume(unmute=True)
|
|
elif compare(event, "volume-up"):
|
|
self.set_volume(increment=self.UP)
|
|
elif compare(event, "volume-down"):
|
|
self.set_volume(increment=self.DOWN)
|
|
|
|
def load_sfx(self, sfx_location=None):
|
|
"""
|
|
Load SFX from paths defined in config. This can be run without arguments, and it will attempt to auto find
|
|
SFX following the below procedure. If sfx_location is set, paths for SFX files in the config are overridden.
|
|
|
|
Auto-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
|
|
"""
|
|
for name, sfx_definition in self.get_configuration("sfx").items():
|
|
sfx_definition_members = sfx_definition.split(self.CONFIG_SEPARATOR)
|
|
# default values for everything besides path in case those aren't included in the config definition
|
|
path, volume, fade_out, loops, maxtime = sfx_definition_members[0], 1.0, 0, 0, 0
|
|
# format for an SFX defintion in config is: "name = path[, volume][, fade out][, loops][, maxtime]"
|
|
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)
|
|
# override config definitions of SFX paths
|
|
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)
|
|
if root:
|
|
print("checking {} for sound effects".format(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:
|
|
# use path elements to prepend subdirectories to the SFX name
|
|
print(r"{}".format(re.escape(root)), r"{}".format(re.escape(node)), rf"{root}", rf"{node}")
|
|
prefix = re.sub(r"{}".format(re.escape(root)), r"", r"{}".format(node))
|
|
print(prefix)
|
|
prefix = re.sub(rf"^\{os.path.sep}", r"", prefix)
|
|
if prefix:
|
|
prefix = re.sub(rf"\{os.path.sep}", r"_", 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 self.is_loadable(path):
|
|
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
|
|
else:
|
|
print("couldn't load sound effect, path is not loadable {}".format(path))
|
|
return False
|
|
|
|
def get_sfx(self, name):
|
|
'''
|
|
Get a SoundEffect object (which inherits pygame's Sound) from this object's dictonary of loaded sfx
|
|
'''
|
|
return self.sfx[name]
|
|
|
|
def play_sfx(self, name, loops=None, maxtime=None, fade_ms=None, position=None, x=None):
|
|
return self.sfx[name].play(loops, maxtime, fade_ms, position, x)
|
|
|
|
def load_bgm(self):
|
|
"""
|
|
Loading BGM procedure:
|
|
|
|
- Check project specific BGM paths and load files found in those paths.
|
|
- Load config file name/path definitions, overwriting existing. This means the config file
|
|
definitions have precedence over the automatic loading of files placed in folders.
|
|
|
|
Further editing of BGM while the game is running can be done through the AudioPanel object.
|
|
"""
|
|
# First load BGM files found in the BGM path set in the configuration
|
|
for root in self.get_configuration("audio", "bgm-project-path"):
|
|
# look for path in resource folders
|
|
root = self.get_resource(root)
|
|
if root is not None and os.path.exists(root):
|
|
print("checking {} for background music files".format(root))
|
|
if os.path.isfile(root):
|
|
self.set_bgm(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.set_bgm(os.path.join(node, leaf), prefix=prefix)
|
|
# Next load BGM paths defined in the configuration. If any of these have the same name as
|
|
# BGM loaded by the previous code block, they will be overwritten to give the config file
|
|
# precedence over automatic BGM detection.
|
|
print("checking configuration for background music definitions".format(root))
|
|
for name, bgm_definition in self.get_configuration("bgm").items():
|
|
bgm_definition_members = bgm_definition.split(self.CONFIG_SEPARATOR)
|
|
path, volume = bgm_definition_members[0], 1.0
|
|
for ii, member in enumerate(bgm_definition_members[1:]):
|
|
if ii == 0:
|
|
volume = float(member)
|
|
self.set_bgm(path, name, volume=volume)
|
|
|
|
def set_bgm(self, path, name=None, prefix="", volume=1.0):
|
|
path = self.get_resource(path)
|
|
try:
|
|
pygame.mixer.music.load(path)
|
|
except:
|
|
print("can't load {} as music".format(path))
|
|
return False
|
|
if name is None:
|
|
name = os.path.basename(path).split(".")[0]
|
|
print("setting {} background music to {}".format(name, path))
|
|
self.bgm[prefix + name] = BGM(self, path, volume)
|
|
if self.current_bgm is None:
|
|
self.current_bgm = self.bgm[prefix + name]
|
|
return True
|
|
|
|
def play_bgm(self, name=None, store_as_current=True, start=0):
|
|
if name is None:
|
|
bgm = self.current_bgm
|
|
else:
|
|
bgm = self.bgm[name]
|
|
pygame.mixer.music.load(bgm.get_path())
|
|
try:
|
|
pygame.mixer.music.play(-1, start)
|
|
except pygame.error:
|
|
pygame.mixer.music.play(-1)
|
|
pygame.mixer.music.set_volume(bgm.get_volume() * self.get_configuration("audio", "volume"))
|
|
if store_as_current:
|
|
self.current_bgm = bgm
|
|
|
|
def is_sound_file(self, path):
|
|
return path.split(".")[-1] in self.get_configuration("audio", "sfx-extensions")
|
|
|
|
def is_loadable(self, path):
|
|
try:
|
|
pygame.mixer.Sound(path)
|
|
except:
|
|
return False
|
|
return True
|
|
|
|
def is_streamable(self, path):
|
|
try:
|
|
pygame.mixer.music.load(path)
|
|
except:
|
|
return False
|
|
return True
|
|
|
|
def is_audio_panel_active(self):
|
|
return self.audio_panel and self.audio_panel.active
|
|
|
|
def update(self):
|
|
Animation.update(self)
|
|
if self.audio_panel:
|
|
self.audio_panel.update()
|
|
|
|
|
|
class BGM(GameChild):
|
|
|
|
def __init__(self, parent, path, volume=1.0):
|
|
GameChild.__init__(self, parent)
|
|
self.path = path
|
|
self.volume = volume
|
|
|
|
def get_path(self):
|
|
return self.path
|
|
|
|
def adjust_volume(self, increment):
|
|
self.volume = clamp(self.volume + increment, 0, 1.0)
|
|
if self.parent.current_bgm == self:
|
|
pygame.mixer.music.set_volume(self.volume)
|
|
return self.volume
|
|
|
|
def get_volume(self):
|
|
return self.volume
|
|
|
|
def __eq__(self, other):
|
|
return self.path == other.path
|
|
|
|
|
|
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", "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 = []
|
|
self.bgm_elapsed = None
|
|
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
|
|
if pygame.mixer.music.get_busy():
|
|
self.bgm_elapsed = pygame.mixer.music.get_pos() / 1000
|
|
pygame.mixer.music.stop()
|
|
pygame.mixer.stop()
|
|
# self.build()
|
|
|
|
def deactivate(self):
|
|
pygame.mouse.set_visible(self.get_configuration("mouse", "visible"))
|
|
self.active = False
|
|
if self.bgm_elapsed is not None:
|
|
self.get_audio().play_bgm(start=self.bgm_elapsed)
|
|
self.file_browser.hide()
|
|
|
|
def respond(self, event):
|
|
if self.get_delegate().compare(event, "toggle-audio-panel") 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
|
|
elif event.button == 3:
|
|
self.deactivate()
|
|
|
|
def build(self):
|
|
for row in self.rows:
|
|
row.unsubscribe()
|
|
del row
|
|
self.rows = []
|
|
for key in sorted(self.parent.bgm):
|
|
self.rows.append(AudioPanelRow(self, key, True))
|
|
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()
|
|
ds.fill((0, 0, 0))
|
|
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(128, 192, 255, 255)
|
|
FOREGROUND = pygame.Color(0, 0, 0, 255)
|
|
WIDTH = .5
|
|
HEIGHT = 30
|
|
INDENT = 4
|
|
MAX_NAME_WIDTH = .7
|
|
SLIDER_W = 60
|
|
BUTTON_W = 30
|
|
|
|
def __init__(self, parent, key, is_bgm=False):
|
|
BlinkingSprite.__init__(self, parent, 500)
|
|
self.key = key
|
|
self.selected = False
|
|
self.font = self.parent.font_large
|
|
self.is_bgm = is_bgm
|
|
self.build()
|
|
font_medium = self.parent.font_medium
|
|
font_small = self.parent.font_small
|
|
if self.is_bgm:
|
|
volume = self.get_bgm().volume
|
|
volume_function = self.get_bgm().adjust_volume
|
|
else:
|
|
volume = self.get_sound_effect().local_volume
|
|
volume_function = self.get_sound_effect().adjust_volume
|
|
self.volume_spinner = AudioPanelSpinner(
|
|
self, font_medium, font_small, self.SLIDER_W, self.location.h, .05,
|
|
volume, volume_function, self.FOREGROUND, self.BACKGROUND, 2, "vol")
|
|
if not self.is_bgm:
|
|
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")
|
|
if self.is_bgm:
|
|
callback, kwargs = self.get_game().get_audio().play_bgm, {"name": self.key, "store_as_current": False}
|
|
else:
|
|
callback, kwargs = self.get_sound_effect().play, {}
|
|
self.play_button = AudioPanelButton(self, callback, kwargs)
|
|
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)
|
|
if self.is_bgm:
|
|
callback = pygame.mixer.music.stop
|
|
else:
|
|
callback = self.get_sound_effect().stop
|
|
self.stop_button = AudioPanelButton(self, callback)
|
|
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 self.parent.active and 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()
|
|
if not self.is_bgm:
|
|
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)
|
|
if self.is_bgm:
|
|
file_name = self.get_bgm().path
|
|
else:
|
|
file_name = self.get_sound_effect().path
|
|
file_name_text = self.font.render(file_name, 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 get_bgm(self):
|
|
return self.get_game().get_audio().bgm[self.key]
|
|
|
|
def update_config(self):
|
|
if self.is_bgm:
|
|
section_name = "bgm"
|
|
else:
|
|
section_name = "sfx"
|
|
if not self.get_configuration().has_section(section_name):
|
|
self.get_configuration().add_section(section_name)
|
|
if self.is_bgm:
|
|
bgm = self.get_bgm()
|
|
config_value = "{}, {:.2f}".format(bgm.path, bgm.volume)
|
|
else:
|
|
sound_effect = self.get_sound_effect()
|
|
config_value = "{}, {:.2f}, {:.2f}, {}, {:.2f}".format(
|
|
sound_effect.path, sound_effect.local_volume, sound_effect.fade_out_length,
|
|
sound_effect.loops, sound_effect.maxtime)
|
|
self.get_configuration().set(section_name, self.key, config_value)
|
|
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 set_clickable(self, clickable=True):
|
|
self.play_button.set_clickable(clickable)
|
|
self.stop_button.set_clickable(clickable)
|
|
self.volume_spinner.set_clickable(clickable)
|
|
if not self.is_bgm:
|
|
self.fade_out_spinner.set_clickable(clickable)
|
|
self.loops_spinner.set_clickable(clickable)
|
|
self.maxtime_spinner.set_clickable(clickable)
|
|
|
|
def update(self):
|
|
self.play_button.location.midleft = self.location.move(5, 0).midright
|
|
self.stop_button.location.midleft = self.play_button.location.midright
|
|
self.volume_spinner.location.midleft = self.stop_button.location.move(5, 0).midright
|
|
if not self.is_bgm:
|
|
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
|
|
Sprite.update(self)
|
|
self.volume_spinner.update()
|
|
if not self.is_bgm:
|
|
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.rows = []
|
|
self.font = self.parent.font_large
|
|
self.previewing_sound = None
|
|
self.previewing_sound_row = None
|
|
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):
|
|
if self.previewing_sound is not None:
|
|
self.previewing_sound.stop()
|
|
# 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:
|
|
pos = Vector(*event.pos).get_moved(-self.location.left, -self.location.top)
|
|
if (not row.has_child("button") or pos.x < row.get_child("button").location.left) and row.collide(pos):
|
|
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):
|
|
loaded = False
|
|
selected = self.parent.get_selected()
|
|
if selected.is_bgm:
|
|
loaded = self.get_audio().set_bgm(full_path, selected.key)
|
|
else:
|
|
loaded = self.get_audio().load_sfx_file(full_path, selected.key, True)
|
|
if loaded:
|
|
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()
|
|
row.set_clickable(True)
|
|
for row in self.rows:
|
|
if row.has_child("button"):
|
|
row.get_child("button").set_clickable(False)
|
|
if self.previewing_sound is not None:
|
|
self.previewing_sound.stop()
|
|
Sprite.hide(self)
|
|
|
|
def unhide(self):
|
|
for row in self.parent.rows:
|
|
row.set_clickable(False)
|
|
for row in self.rows:
|
|
if row.has_child("button"):
|
|
row.get_child("button").set_clickable()
|
|
Sprite.unhide(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", \
|
|
"bgm-repository-path", "bgm-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):
|
|
for row in self.rows:
|
|
if row.has_child("button"):
|
|
row.get_child("button").unsubscribe()
|
|
del row
|
|
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, (8, 0))
|
|
surface.fill(self.COLORS[1], (0, surface.get_height() - 1, self.location.w, 1))
|
|
row.add_frame(surface)
|
|
row.display_surface = self.get_current_frame()
|
|
row.location.bottom = 0
|
|
self.rows.append(row)
|
|
full_path = os.path.join(os.path.sep.join(self.trail), path)
|
|
if self.get_audio().is_sound_file(full_path):
|
|
button = AudioPanelButton(self, self.preview, {"path": full_path, "row": row}, [row, self])
|
|
row.set_child("button", button)
|
|
frame = pygame.Surface([text.get_height()] * 2, SRCALPHA)
|
|
w, h = frame.get_size()
|
|
pygame.draw.polygon(
|
|
frame, self.COLORS[1], ((w * .25, h * .25), (w * .25, h * .75), (w * .75, h * .5)))
|
|
button.add_frame(frame)
|
|
button.display_surface = row.get_current_frame()
|
|
button.location.right = self.location.w - 10
|
|
|
|
def preview(self, path, row):
|
|
is_bgm = self.parent.get_selected().is_bgm
|
|
audio = self.get_audio()
|
|
if is_bgm and audio.is_streamable(path) or not is_bgm and audio.is_loadable(path):
|
|
if self.previewing_sound is not None:
|
|
self.previewing_sound.stop()
|
|
pygame.mixer.music.stop()
|
|
if is_bgm:
|
|
pygame.mixer.music.load(path)
|
|
pygame.mixer.music.play(-1)
|
|
else:
|
|
self.previewing_sound = SoundEffect(self, path)
|
|
self.previewing_sound.play()
|
|
self.previewing_sound_row = 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()
|
|
for location in row.locations:
|
|
if location.collidepoint(*Vector(*pygame.mouse.get_pos()).get_moved(
|
|
-self.location.left, -self.location.top)) or \
|
|
row == self.previewing_sound_row:
|
|
self.get_current_frame().fill(self.COLORS[1], (
|
|
location.topleft, (6, location.h)))
|
|
self.get_current_frame().fill(self.COLORS[1], (
|
|
location.move(-8, 0).topright, (6, location.h)))
|
|
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.set_clickable()
|
|
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 self.clickable and 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 set_clickable(self, clickable=True):
|
|
self.clickable = clickable
|
|
|
|
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, callback_kwargs={}, containers=[], pass_mods=False):
|
|
Sprite.__init__(self, parent)
|
|
self.callback = callback
|
|
self.callback_kwargs = callback_kwargs
|
|
self.containers = containers
|
|
self.pass_mods = pass_mods
|
|
self.set_clickable()
|
|
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 self.get_audio().audio_panel.active and self.clickable and event.button == 1:
|
|
pos = Vector(*event.pos)
|
|
for container in self.containers:
|
|
pos.move(-container.location.left, -container.location.top)
|
|
if self.collide(pos):
|
|
if self.pass_mods:
|
|
kwargs = collections.ChainMap(self.callback_kwargs, {"mods": pygame.key.get_mods()})
|
|
else:
|
|
kwargs = self.callback_kwargs
|
|
self.callback(**kwargs)
|
|
|
|
def set_clickable(self, clickable=True):
|
|
self.clickable = clickable
|