# -*- 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