class BandsAudioEffect(AudioReactiveEffect, GradientEffect): NAME = "Bands" CONFIG_SCHEMA = vol.Schema( { vol.Optional('band_count', description='Number of bands', default=6): vol.All(vol.Coerce(int), vol.Range(min=1, max=16)), vol.Optional('align', description='Alignment of bands', default='left'): vol.In(list(["left", "right", "invert", "center"])), vol.Optional('gradient_name', description='Color gradient to display', default='Spectral'): vol.In(list(GRADIENTS.keys())), vol.Optional('mirror', description='Mirror the effect', default=False): bool }) def config_updated(self, config): # Create the filters used for the effect self._r_filter = self.create_filter(alpha_decay=0.05, alpha_rise=0.999) self.bkg_color = np.array(COLORS["black"], dtype=float) def audio_data_updated(self, data): # Grab the filtered and interpolated melbank data y = data.interpolated_melbank(self.pixel_count, filtered=False) filtered_y = data.interpolated_melbank(self.pixel_count, filtered=True) # Grab the filtered difference between the filtered melbank and the # raw melbank. r = self._r_filter.update(y - filtered_y) out = np.tile(r, (3, 1)).T out_clipped = np.clip(out, 0, 1) out_split = np.array_split(out_clipped, self._config["band_count"], axis=0) for i in range(self._config["band_count"]): band_width = len(out_split[i]) color = self.get_gradient_color(i / self._config["band_count"]) vol = int(out_split[i].max() * band_width) # length (vol) of band out_split[i][:] = self.bkg_color if vol: out_split[i][:vol] = color if self._config["align"] == "center": out_split[i] = np.roll(out_split[i], (band_width - vol) // 2, axis=0) elif self._config["align"] == "invert": out_split[i] = np.roll(out_split[i], -vol // 2, axis=0) elif self._config["align"] == "right": out_split[i] = np.flip(out_split[i], axis=0) elif self._config["align"] == "left": pass self.pixels = np.vstack(out_split)
class BandsMatrixAudioEffect(AudioReactiveEffect, GradientEffect): NAME = "Bands Matrix" CONFIG_SCHEMA = vol.Schema({ vol.Optional("band_count", description="Number of bands", default=6): vol.All(vol.Coerce(int), vol.Range(min=1, max=16)), vol.Optional( "gradient_name", description="Color gradient to display", default="Rainbow", ): vol.In(list(GRADIENTS.keys())), vol.Optional( "mirror", description="Mirror the effect", default=False, ): bool, vol.Optional( "flip_gradient", description="Flip Gradient", default=False, ): bool, }) def config_updated(self, config): # Create the filters used for the effect self._r_filter = self.create_filter(alpha_decay=0.05, alpha_rise=0.999) self.bkg_color = np.array(COLORS["black"], dtype=float) self.flip_gradient = config["flip_gradient"] def audio_data_updated(self, data): # Grab the filtered and interpolated melbank data y = data.interpolated_melbank(self.pixel_count, filtered=False) filtered_y = data.interpolated_melbank(self.pixel_count, filtered=True) # Grab the filtered difference between the filtered melbank and the # raw melbank. r = self._r_filter.update(y - filtered_y) out = np.tile(r, (3, 1)).T out_clipped = np.clip(out, 0, 1) out_split = np.array_split(out_clipped, self._config["band_count"], axis=0) for i in range(self._config["band_count"]): band_width = len(out_split[i]) volume = int(out_split[i].max() * band_width) out_split[i][volume:] = self.bkg_color for p in range(volume): gradient_value = (1 - p / band_width if self.flip_gradient else p / band_width) out_split[i][p] = self.get_gradient_color(gradient_value) if i % 2 != 0: out_split[i] = np.flip(out_split[i], axis=0) self.pixels = np.vstack(out_split)
class EQAudioEffect(AudioReactiveEffect, GradientEffect): NAME = "Equalizer" CONFIG_SCHEMA = vol.Schema( { vol.Optional('align', description='Alignment of bands', default='left'): vol.In(list(["left", "right", "invert", "center"])), vol.Optional('gradient_name', description='Color gradient to display', default='Spectral'): vol.In(list(GRADIENTS.keys())), vol.Optional('gradient_repeat', description='Repeat the gradient into segments', default=6): vol.All(vol.Coerce(int), vol.Range(min=1, max=16)), vol.Optional('mirror', description='Mirror the effect', default=False): bool, }) def config_updated(self, config): # Create the filters used for the effect self._r_filter = self.create_filter(alpha_decay=0.5, alpha_rise=0.1) def audio_data_updated(self, data): # Grab the filtered and interpolated melbank data y = data.interpolated_melbank(self.pixel_count, filtered=False) filtered_y = data.interpolated_melbank(self.pixel_count, filtered=True) # Grab the filtered difference between the filtered melbank and the # raw melbank. r = self._r_filter.update(y - filtered_y) r_clipped = np.clip(r, 0, 1) r_split = np.array_split(r_clipped, self._config["gradient_repeat"]) for i in range(self._config["gradient_repeat"]): band_width = len(r_split[i]) volume = int(r_split[i].sum() * band_width) # length (volume) of band r_split[i][:] = 0 if volume: r_split[i][:volume] = 1 if self._config["align"] == "center": r_split[i] = np.roll(r_split[i], (band_width - volume) // 2, axis=0) elif self._config["align"] == "invert": r_split[i] = np.roll(r_split[i], -volume // 2, axis=0) elif self._config["align"] == "right": r_split[i] = np.flip(r_split[i], axis=0) elif self._config["align"] == "left": pass self.pixels = self.apply_gradient(np.hstack(r_split))
class ColorRainbowEffect(Effect): CONFIG_SCHEMA = vol.Schema({ vol.Optional('gradient_name', description='Preset gradient name', default='Spectral'): vol.In(list(GRADIENTS.keys())), vol.Optional('gradient_roll', description='Amount to shift the gradient', default=0): vol.Coerce(int), vol.Optional('gradient_method', description='Function used to generate gradient', default='cubic_ease'): vol.In(["cubic_ease", "bezier"]), }) def apply_rainbow(self, dt): output = np.zeros(shape=(self.pixel_count, 3)) if dt: rainbow = [] rainbow.append([255, 0, 0]) rainbow.append([255, 165, 0]) rainbow.append([255, 255, 0]) rainbow.append([0, 128, 0]) rainbow.append([0, 0, 255]) rainbow.append([75, 00, 130]) rainbow.append([238, 130, 238]) pixels_per_color = int(self.pixel_count / len(rainbow)) for i in range(self.pixel_count): index = int(i / pixels_per_color) if index >= len(rainbow): index = len(rainbow) - 1 output[i] = [ rainbow[index][0], rainbow[index][1], rainbow[index][2] ] self.colormap[i] = output[i] else: for i in range(len(self.colormap)): for j in range(3): self.colormap[i][j] -= 1 if self.colormap[i][j] < 0: self.colormap[i][j] = 0 output[i] = [ self.colormap[i][0], self.colormap[i][1], self.colormap[i][2] ] return output
class Strobe(AudioReactiveEffect, GradientEffect): NAME = "Real Strobe" CONFIG_SCHEMA = vol.Schema({ vol.Optional( "gradient_name", description="Color scheme for bass strobe to cycle through", default="Dancefloor", ): vol.In(list(GRADIENTS.keys())), vol.Optional( "color_step", description="Amount of color change per bass strobe", default=0.0625, ): vol.All(vol.Coerce(float), vol.Range(min=0, max=0.25)), vol.Optional( "bass_threshold", description="Cutoff for quiet sounds. Higher -> only loud sounds are detected", default=0.4, ): vol.All(vol.Coerce(float), vol.Range(min=0, max=1)), vol.Optional( "bass_strobe_decay_rate", description="Bass strobe decay rate. Higher -> decays faster.", default=0.5, ): vol.All(vol.Coerce(float), vol.Range(min=0, max=1)), vol.Optional( "strobe_color", description="Colour for note strobes", default="white", ): vol.In(list(COLORS.keys())), vol.Optional( "strobe_width", description="Note strobe width, in pixels", default=10, ): vol.All(vol.Coerce(int), vol.Range(min=0, max=1000)), vol.Optional( "strobe_decay_rate", description="Note strobe decay rate. Higher -> decays faster.", default=0.5, ): vol.All(vol.Coerce(float), vol.Range(min=0, max=1)), }) def activate(self, pixel_count): super().activate(pixel_count) self.strobe_overlay = np.zeros(np.shape(self.pixels)) self.bass_strobe_overlay = np.zeros(np.shape(self.pixels)) self.onsets_queue = queue.Queue() def config_updated(self, config): self.bass_threshold = self._config["bass_threshold"] self.color_shift_step = self._config["color_step"] self.strobe_color = np.array(COLORS[self._config["strobe_color"]], dtype=float) self.last_color_shift_time = 0 self.strobe_width = self._config["strobe_width"] self.color_shift_delay_in_seconds = 1 self.color_idx = 0 self.last_strobe_time = 0 self.strobe_wait_time = 0 self.strobe_decay_rate = 1 - self._config["strobe_decay_rate"] self.last_bass_strobe_time = 0 self.bass_strobe_wait_time = 0 self.bass_strobe_decay_rate = (1 - self._config["bass_strobe_decay_rate"]) def get_pixels(self): pixels = np.copy(self.bass_strobe_overlay) if not self.onsets_queue.empty(): self.onsets_queue.get() strobe_width = min(self.strobe_width, self.pixel_count) length_diff = self.pixel_count - strobe_width position = (0 if length_diff == 0 else np.random.randint(self.pixel_count - strobe_width)) self.strobe_overlay[position:position + strobe_width] = self.strobe_color pixels += self.strobe_overlay self.strobe_overlay *= self.strobe_decay_rate self.bass_strobe_overlay *= self.bass_strobe_decay_rate self.pixels = pixels return self.pixels def audio_data_updated(self, data): self._dirty = True currentTime = time.time() if (currentTime - self.last_color_shift_time > self.color_shift_delay_in_seconds): self.color_idx += self.color_shift_step self.color_idx = self.color_idx % 1 self.bass_strobe_color = self.get_gradient_color(self.color_idx) self.last_color_shift_time = currentTime lows_intensity = np.mean(data.melbank_lows()) if (lows_intensity > self.bass_threshold and currentTime - self.last_bass_strobe_time > self.bass_strobe_wait_time): self.bass_strobe_overlay = np.tile(self.bass_strobe_color, (self.pixel_count, 1)) self.last_bass_strobe_time = currentTime onsets = data.onset() if (onsets["high"] and currentTime - self.last_strobe_time > self.strobe_wait_time): self.onsets_queue.put(True) self.last_strobe_time = currentTime
class GradientEffect(Effect): """ Simple effect base class that supplies gradient functionality. This is intended for effect which instead of outputing exact colors output colors based upon some configured color pallet. """ CONFIG_SCHEMA = vol.Schema({ vol.Optional('gradient_name', description='Preset gradient name', default='Spectral'): vol.In(list(GRADIENTS.keys())), vol.Optional('gradient_roll', description='Amount to shift the gradient', default=0): vol.Coerce(int), vol.Optional('gradient_method', description='Function used to generate gradient', default='cubic_ease'): vol.In(["cubic_ease", "bezier"]), }) _gradient_curve = None def _comb(self, N, k): N = int(N) k = int(k) if k > N or N < 0 or k < 0: return 0 M = N + 1 nterms = min(k, N - k) numerator = 1 denominator = 1 for j in range(1, nterms + 1): numerator *= M - j denominator *= j return numerator // denominator def _bernstein_poly(self, i, n, t): """The Bernstein polynomial of n, i as a function of t""" return self._comb(n, i) * (t**(n - i)) * (1 - t)**i def _ease(self, chunk_len, start_val, end_val, slope=1.5): x = np.linspace(0, 1, chunk_len) diff = end_val - start_val pow_x = np.power(x, slope) return diff * pow_x / (pow_x + np.power(1 - x, slope)) + start_val def _color_ease(self, chunk_len, start_color, end_color): """Makes a coloured block easing from start to end colour""" return np.array([ self._ease(chunk_len, start_color[i], end_color[i]) for i in range(3) ]) def _generate_gradient_curve(self, gradient_colors, gradient_method, gradient_length): # Check to see if we have a custom gradient, or a predefined one and # load the colors accordingly if isinstance(gradient_colors, str): gradient_name = gradient_colors gradient_colors = [] if GRADIENTS.get(gradient_name): gradient_colors = GRADIENTS.get(gradient_name).get("colors") gradient_method = GRADIENTS.get(gradient_name).get( "method", gradient_method) elif COLORS.get(gradient_name): gradient_colors = [gradient_name] if not gradient_colors: gradient_colors = GRADIENTS.get('spectral') self.rgb_list = np.array( [COLORS[color.lower()] for color in gradient_colors]).T n_colors = len(self.rgb_list[0]) if gradient_method == "bezier": t = np.linspace(0.0, 1.0, gradient_length) polynomial_array = np.array([ self._bernstein_poly(i, n_colors - 1, t) for i in range(0, n_colors) ]) polynomial_array = np.fliplr(polynomial_array) gradient = np.array([ np.dot(self.rgb_list[0], polynomial_array), np.dot(self.rgb_list[1], polynomial_array), np.dot(self.rgb_list[2], polynomial_array) ]) _LOGGER.info( ('Generating new gradient curve for {}'.format(gradient_colors) )) self._gradient_curve = gradient elif gradient_method == "cubic_ease": t = np.zeros(gradient_length) ease_chunks = np.array_split(t, n_colors - 1) color_pairs = np.array([(self.rgb_list.T[i], self.rgb_list.T[i + 1]) for i in range(n_colors - 1)]) gradient = np.hstack( self._color_ease(len(ease_chunks[i]), *color_pairs[i]) for i in range(n_colors - 1)) _LOGGER.info( ('Generating new gradient curve for {}'.format(gradient_colors) )) self._gradient_curve = gradient else: gradient = np.zeros((gradient_length, 3)) for i in range(gradient_length): rgb_i = i % n_colors gradient[i] = (self.rgb_list[0][rgb_i], self.rgb_list[1][rgb_i], self.rgb_list[2][rgb_i]) self._gradient_curve = gradient.T def _gradient_valid(self): if self._gradient_curve is None: return False # Uninitialized gradient if len(self._gradient_curve[0]) != self.pixel_count: return False # Incorrect size return True def _validate_gradient(self): if not self._gradient_valid(): self._generate_gradient_curve(self._config['gradient_name'], self._config['gradient_method'], self.pixel_count) def _roll_gradient(self): if self._config['gradient_roll'] == 0: return self._gradient_curve = np.roll(self._gradient_curve, self._config['gradient_roll'], axis=1) def get_gradient_color(self, point): self._validate_gradient() return self._gradient_curve[:, point] #n_colors = len(self.rgb_list[0]) #polynomial_array = np.array([self._bernstein_poly(i, n_colors-1, point) for i in range(0, n_colors)]) #return (np.dot(self.rgb_list[0], polynomial_array), # np.dot(self.rgb_list[1], polynomial_array), # np.dot(self.rgb_list[2], polynomial_array)) def config_updated(self, config): """Invalidate the gradient""" self._gradient_curve = None def apply_gradient(self, y): self._validate_gradient() # Apply and roll the gradient if necessary output = (self._gradient_curve[:][::1] * y).T self._roll_gradient() return output
class BarAudioEffect(AudioReactiveEffect, GradientEffect): NAME = "Bar" CONFIG_SCHEMA = vol.Schema({ vol.Optional('gradient_name', description='Color scheme to cycle through', default='Spectral'): vol.In(list(GRADIENTS.keys())), vol.Optional('mode', description='Choose from different animations', default='wipe'): vol.In(list(["bounce", "wipe", "in-out"])), vol.Optional('ease_method', description='Acceleration profile of bar', default='ease_out'): vol.In(list(["ease_in_out", "ease_in", "ease_out", "linear"])), vol.Optional('color_step', description='Amount of color change per beat', default=0.125): vol.All(vol.Coerce(float), vol.Range(min=0.0625, max=0.5)) }) def config_updated(self, config): self.phase = 0 self.color_idx = 0 self.bar_len = 0.3 def audio_data_updated(self, data): # Run linear beat oscillator through easing method beat_oscillator, beat_now = data.oscillator() if self._config["ease_method"] == "ease_in_out": x = 0.5*np.sin(np.pi*(beat_oscillator-0.5))+0.5 elif self._config["ease_method"] == "ease_in": x = beat_oscillator**2 elif self._config["ease_method"] == "ease_out": x = -(beat_oscillator-1)**2+1 elif self._config["ease_method"] == "linear": x = beat_oscillator # Colour change and phase if beat_now: self.phase = 1-self.phase # flip flop 0->1, 1->0 if self.phase == 0: # 8 colours, 4 beats to a bar self.color_idx += self._config["color_step"] self.color_idx = self.color_idx % 1 # loop back to zero # Compute position of bar start and stop if self._config["mode"] == "wipe": if self.phase == 0: bar_end = x bar_start = 0 elif self.phase == 1: bar_end = 1 bar_start = x elif self._config["mode"] == "bounce": x = x*(1-self.bar_len) if self.phase == 0: bar_end = x+self.bar_len bar_start = x elif self.phase == 1: bar_end = 1-x bar_start = 1-(x+self.bar_len) elif self._config["mode"] == "in-out": if self.phase == 0: bar_end = x bar_start = 0 elif self.phase == 1: bar_end = 1-x bar_start = 0 # Construct the bar color = self.get_gradient_color(self.color_idx) image = Image.new("RGB", (self.pixel_count, 1), color=0) d = ImageDraw.Draw(image) d.rectangle(((int(self.pixel_count*bar_start), 0), (int(self.pixel_count*bar_end), 1)), fill=tuple(color.astype('B'))) # Update the pixel values self.pixels = image
class BarAudioEffect(AudioReactiveEffect, GradientEffect): NAME = "Bar" CONFIG_SCHEMA = vol.Schema({ vol.Optional( "gradient_name", description="Color scheme to cycle through", default="Rainbow", ): vol.In(list(GRADIENTS.keys())), vol.Optional( "mode", description="Choose from different animations", default="wipe", ): vol.In(list(["bounce", "wipe", "in-out"])), vol.Optional( "ease_method", description="Acceleration profile of bar", default="ease_out", ): vol.In(list(["ease_in_out", "ease_in", "ease_out", "linear"])), vol.Optional( "color_step", description="Amount of color change per beat", default=0.125, ): vol.All(vol.Coerce(float), vol.Range(min=0.0625, max=0.5)), }) def config_updated(self, config): self.phase = 0 self.color_idx = 0 self.bar_len = 0.3 def audio_data_updated(self, data): # Run linear beat oscillator through easing method beat_oscillator, beat_now = data.oscillator() if self._config["ease_method"] == "ease_in_out": x = 0.5 * np.sin(np.pi * (beat_oscillator - 0.5)) + 0.5 elif self._config["ease_method"] == "ease_in": x = beat_oscillator**2 elif self._config["ease_method"] == "ease_out": x = -((beat_oscillator - 1)**2) + 1 elif self._config["ease_method"] == "linear": x = beat_oscillator # Colour change and phase if beat_now: self.phase = 1 - self.phase # flip flop 0->1, 1->0 if self.phase == 0: # 8 colours, 4 beats to a bar self.color_idx += self._config["color_step"] self.color_idx = self.color_idx % 1 # loop back to zero # Compute position of bar start and stop if self._config["mode"] == "wipe": if self.phase == 0: bar_end = x bar_start = 0 elif self.phase == 1: bar_end = 1 bar_start = x elif self._config["mode"] == "bounce": x = x * (1 - self.bar_len) if self.phase == 0: bar_end = x + self.bar_len bar_start = x elif self.phase == 1: bar_end = 1 - x bar_start = 1 - (x + self.bar_len) elif self._config["mode"] == "in-out": if self.phase == 0: bar_end = x bar_start = 0 elif self.phase == 1: bar_end = 1 - x bar_start = 0 # Construct the bar color = self.get_gradient_color(self.color_idx) p = np.zeros(np.shape(self.pixels)) p[int(self.pixel_count * bar_start):int(self.pixel_count * bar_end), :, ] = color # Update the pixel values self.pixels = p
class MultiBarAudioEffect(AudioReactiveEffect, GradientEffect): NAME = "Multicolor Bar" CONFIG_SCHEMA = vol.Schema({ vol.Optional('gradient_name', description='Color scheme to cycle through', default = 'Spectral'): vol.In(list(GRADIENTS.keys())), vol.Optional('mode', description='Choose from different animations', default = 'wipe'): vol.In(list(["cascade", "wipe"])), vol.Optional('ease_method', description='Acceleration profile of bar', default='linear'): vol.In(list(["ease_in_out", "ease_in", "ease_out", "linear"])), vol.Optional('color_step', description='Amount of color change per beat', default = 0.125): vol.All(vol.Coerce(float), vol.Range(min=0.0625, max=0.5)) }) def config_updated(self, config): self.phase = 0 self.color_idx = 0 def audio_data_updated(self, data): # Run linear beat oscillator through easing method beat_oscillator, beat_now = data.oscillator() if self._config["ease_method"] == "ease_in_out": x = 0.5*np.sin(np.pi*(beat_oscillator-0.5))+0.5 elif self._config["ease_method"] == "ease_in": x = beat_oscillator**2 elif self._config["ease_method"] == "ease_out": x = -(beat_oscillator-1)**2+1 elif self._config["ease_method"] == "linear": x = beat_oscillator # Colour change and phase if beat_now: self.phase = 1-self.phase # flip flop 0->1, 1->0 self.color_idx += self._config["color_step"] # 8 colours, 4 beats to a bar self.color_idx = self.color_idx % 1 # loop back to zero color_fg = self.get_gradient_color(self.color_idx) color_bkg = self.get_gradient_color((self.color_idx+self._config["color_step"])%1) # Compute position of bar start and stop if self._config["mode"] == "wipe": if self.phase == 0: idx = x elif self.phase == 1: idx = 1-x color_fg, color_bkg = color_bkg, color_fg elif self._config["mode"] == "cascade": idx = x # Construct the array p = np.zeros(np.shape(self.pixels)) p[:int(self.pixel_count*idx), :] = color_bkg p[int(self.pixel_count*idx):, :] = color_fg # Update the pixel values self.pixels = p