diff --git a/pgfw/Audio.py b/pgfw/Audio.py index 2d4c5de..d280957 100644 --- a/pgfw/Audio.py +++ b/pgfw/Audio.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + import os, re, shutil, pygame from .GameChild import * @@ -54,9 +56,20 @@ class Audio(GameChild): # def load_sfx(self, sfx_location=None): for name, sfx_definition in self.get_configuration("sfx").items(): - path, volume = sfx_definition.split(",") - volume = float(volume) - self.load_sfx_file(path, name, True, volume=volume) + 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") @@ -78,16 +91,18 @@ class Audio(GameChild): 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): + 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 %s: %s" % (name, path)) + print("skipping existing sound effect for {}: {}".format(name, path)) else: - print("loading sound effect %s into %s" % (path, name)) - self.sfx[name] = SoundEffect(self, path, volume) + print("loading sound effect {} into {}".format(path, name)) + self.sfx[name] = SoundEffect( + self, path, volume, loops, fade_out, maxtime=maxtime) return True return False @@ -104,21 +119,35 @@ class Audio(GameChild): class SoundEffect(GameChild, pygame.mixer.Sound): - def __init__(self, parent, path, volume=1.0): + 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.initial_volume = volume - self.set_volume(self.initial_volume * self.get_configuration("audio", "sfx-volume")) + 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=0, maxtime=0, fade_ms=0, position=None, x=None): - self.set_volume(self.initial_volume * self.get_configuration("audio", "sfx-volume")) + 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): @@ -126,12 +155,45 @@ class SoundEffect(GameChild, pygame.mixer.Sound): 1 + min(0, ((position - .5) * 2)) def adjust_volume(self, increment): - self.initial_volume += increment - if self.initial_volume > 1.0: - self.initial_volume = 1.0 - elif self.initial_volume < 0: - self.initial_volume = 0 - self.set_volume(self.initial_volume * self.get_configuration("audio", "sfx-volume")) + 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): @@ -141,6 +203,10 @@ class AudioPanel(Animation): 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) @@ -158,7 +224,7 @@ class AudioPanel(Animation): def activate(self): pygame.mouse.set_visible(True) self.active = True - self.build() + # self.build() def deactivate(self): pygame.mouse.set_visible(self.get_configuration("mouse", "visible")) @@ -171,6 +237,8 @@ class AudioPanel(Animation): 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: @@ -180,7 +248,7 @@ class AudioPanel(Animation): def build(self): for row in self.rows: - row.unsubscribe(row.respond, pygame.MOUSEBUTTONDOWN) + row.unsubscribe() del row self.rows = [] for key in sorted(self.parent.sfx): @@ -195,6 +263,7 @@ class AudioPanel(Animation): 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() @@ -206,19 +275,53 @@ class AudioPanel(Animation): class AudioPanelRow(BlinkingSprite): - BACKGROUND = pygame.Color(255, 255, 255, 180) - FOREGROUND = pygame.Color(0, 0, 0, 180) - WIDTH = .6 - FONT_SIZE = 18 + 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) @@ -235,17 +338,24 @@ class AudioPanelRow(BlinkingSprite): 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): - font = pygame.font.Font( - self.get_resource(self.get_configuration("audio", "panel-font")), - self.FONT_SIZE) 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 = font.render(self.key + ":", True, self.FOREGROUND) + 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 @@ -263,7 +373,7 @@ class AudioPanelRow(BlinkingSprite): 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 = font.render(self.get_sound_effect().path, True, self.FOREGROUND) + 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 @@ -273,6 +383,34 @@ class AudioPanelRow(BlinkingSprite): 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): @@ -280,16 +418,10 @@ class AudioPanelFileBrowser(Sprite): HEIGHT = .75 COLORS = pygame.Color(255, 255, 255), pygame.Color(0, 0, 0) HOME, UP = "[HOME]", "[UP]" - FONT_SIZE = 16 - VOLUME_WIDTH = .1 - VOLUME_INDICATOR_HEIGHT = .1 - VOLUME_STEP = .05 def __init__(self, parent): Sprite.__init__(self, parent) - self.font = pygame.font.Font( - self.get_resource(self.get_configuration("audio", "panel-font")), - self.FONT_SIZE) + 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) @@ -297,23 +429,6 @@ class AudioPanelFileBrowser(Sprite): self.background = get_boxed_surface(surface, self.COLORS[0], self.COLORS[1]) self.add_frame(self.background.copy()) self.location.center = dsr.center - self.volume_buttons = [] - for text in "VOL -", "VOL +": - button = Sprite(self) - button.add_frame(render_box( - self.font, text, color=self.COLORS[1], background=self.COLORS[0], - border=self.COLORS[1], width=self.VOLUME_WIDTH * dsr.w)) - button.location.top = self.location.bottom - 1 - self.volume_buttons.append(button) - self.volume_buttons[0].location.left = self.location.left - self.volume_buttons[1].location.right = self.location.right - self.volume_indicator = Sprite(self) - self.volume_indicator.add_frame(pygame.Surface(( - self.location.w - self.volume_buttons[0].location.w * 2, - self.volume_buttons[0].location.h), SRCALPHA)) - self.volume_indicator.get_current_frame().fill(self.COLORS[0]) - self.volume_indicator.location.topleft = ( - self.volume_buttons[0].location.right, self.location.bottom - 1) self.reset() self.subscribe(self.respond, pygame.MOUSEBUTTONDOWN) @@ -324,11 +439,7 @@ class AudioPanelFileBrowser(Sprite): def respond(self, event): if not self.is_hidden(): if event.button == 1: - if self.volume_buttons[0].collide(event.pos): - self.adjust_volume(-self.VOLUME_STEP) - elif self.volume_buttons[1].collide(event.pos): - self.adjust_volume(self.VOLUME_STEP) - elif self.collide(event.pos): + 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) @@ -336,9 +447,9 @@ class AudioPanelFileBrowser(Sprite): 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): - key = self.parent.get_selected().key - if self.get_audio().load_sfx_file(full_path, key, True): - self.update_config(key, full_path) + 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() @@ -350,27 +461,6 @@ class AudioPanelFileBrowser(Sprite): elif event.button == 5: self.row_offset += 1 - def adjust_volume(self, step): - selected = self.parent.get_selected() - sound_effect = selected.get_sound_effect() - print(sound_effect) - sound_effect.adjust_volume(step) - # sound_effect.set_volume(.1) - print(sound_effect.get_volume()) - sound_effect.play() - self.update_config(selected.key, sound_effect.path) - - def update_config(self, key, path): - if not self.get_configuration().has_section("sfx"): - self.get_configuration().add_section("sfx") - volume = self.get_audio().sfx[key].initial_volume - print(self.get_audio().sfx[key]) - self.get_configuration().set("sfx", key, "{}, {}".format(path, volume)) - 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 hide(self): for row in self.parent.rows: row.selected = False @@ -430,12 +520,105 @@ class AudioPanelFileBrowser(Sprite): index += 1 for row in self.rows: row.update() - self.volume_indicator.get_current_frame().fill(self.COLORS[0]) - self.volume_indicator.get_current_frame().fill( - self.COLORS[1], - (0, 0, self.volume_indicator.location.w * self.parent.get_selected().get_sound_effect().initial_volume, - self.volume_indicator.location.h)) - self.volume_indicator.update() - for button in self.volume_buttons: - button.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() diff --git a/pgfw/extension.py b/pgfw/extension.py index 0295350..1b0c2f8 100644 --- a/pgfw/extension.py +++ b/pgfw/extension.py @@ -185,21 +185,25 @@ def get_boxed_surface(surface, background=None, border=None, border_width=1, surface = bordered_surface return surface -def render_box(font, text, antialias=True, color=(0, 0, 0), background=None, border=None, +def render_box(font=None, text=None, antialias=True, color=(0, 0, 0), background=None, border=None, border_width=1, padding=0, width=None, height=None): - surface = font.render(text, antialias, color, background) - if width is not None or height is not None: - if width is None: - width = surface.get_width() - if height is None: - height = surface.get_height() - container = Surface((width, height), SRCALPHA) - if background is not None: - container.fill(background) - text_rect = surface.get_rect() - text_rect.center = container.get_rect().center - container.blit(surface, text_rect) - surface = container + if font is not None: + surface = font.render(text, antialias, color, background) + if width is not None or height is not None: + if width is None: + width = surface.get_width() + if height is None: + height = surface.get_height() + container = Surface((width, height), SRCALPHA) + if background is not None: + container.fill(background) + text_rect = surface.get_rect() + text_rect.center = container.get_rect().center + container.blit(surface, text_rect) + surface = container + else: + surface = pygame.Surface((width, height), SRCALPHA) + surface.fill(background) return get_boxed_surface(surface, background, border, border_width, padding) def get_wrapped_text_surface(font, text, width, antialias, color,