pgfw/pgfw/Audio.py

909 lines
37 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.load_sfx()
self.bgm = {}
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
prefix = re.sub(r"{}".format(root), r"", r"{}".format(node))
prefix = re.sub(r"^{}".format(os.path.sep), r"", prefix)
if prefix:
prefix = re.sub(r"{}".format(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