From 35c26ca7bbd83123ca67282e3be7326c06e601ac Mon Sep 17 00:00:00 2001 From: Scott Lawson Date: Mon, 30 Jan 2017 07:17:59 -0800 Subject: [PATCH] Add many small performance optimizations Over a dozen small performance optimizations * Memoization for linspace generation * Removed unnecessary copies * Limited the rate at which information is printed. Excessive `print()` output was causing issues for some SSH users --- python/visualization.py | 98 +++++++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 43 deletions(-) diff --git a/python/visualization.py b/python/visualization.py index 88097bb..18d9486 100644 --- a/python/visualization.py +++ b/python/visualization.py @@ -11,7 +11,7 @@ import led _time_prev = time.time() * 1000.0 """The previous time that the frames_per_second() function was called""" -_fps = dsp.ExpFilter(val=config.FPS, alpha_decay=0.002, alpha_rise=0.002) +_fps = dsp.ExpFilter(val=config.FPS, alpha_decay=0.2, alpha_rise=0.2) """The low-pass filter used to estimate frames-per-second""" @@ -41,6 +41,27 @@ def frames_per_second(): return _fps.update(1000.0 / dt) +def memoize(function): + """Provides a decorator for memoizing functions""" + from functools import wraps + memo = {} + + @wraps(function) + def wrapper(*args): + if args in memo: + return memo[args] + else: + rv = function(*args) + memo[args] = rv + return rv + return wrapper + + +@memoize +def _normalized_linspace(size): + return np.linspace(0, 1, size) + + def interpolate(y, new_length): """Intelligently resizes the array by linearly interpolating the values @@ -60,8 +81,8 @@ def interpolate(y, new_length): """ if len(y) == new_length: return y - x_old = np.linspace(0, 1, len(y)) - x_new = np.linspace(0, 1, new_length) + x_old = _normalized_linspace(len(y)) + x_new = _normalized_linspace(new_length) z = np.interp(x_new, x_old, y) return z @@ -84,15 +105,15 @@ gain = dsp.ExpFilter(np.tile(0.01, config.N_FFT_BINS), def visualize_scroll(y): """Effect that originates in the center and scrolls outwards""" global p - y = np.copy(y)**2.0 + y = y**2.0 gain.update(y) y /= gain.value y *= 255.0 - r = int(max(y[:len(y) // 3])) - g = int(max(y[len(y) // 3: 2 * len(y) // 3])) - b = int(max(y[2 * len(y) // 3:])) + r = int(np.max(y[:len(y) // 3])) + g = int(np.max(y[len(y) // 3: 2 * len(y) // 3])) + b = int(np.max(y[2 * len(y) // 3:])) # Scrolling effect window - p = np.roll(p, 1, axis=1) + p[:, 1:] = p[:, :-1] p *= 0.98 p = gaussian_filter1d(p, sigma=0.2) # Create new color originating at the center @@ -140,22 +161,18 @@ def visualize_spectrum(y): """Effect that maps the Mel filterbank frequencies onto the LED strip""" global _prev_spectrum y = np.copy(interpolate(y, config.N_PIXELS // 2)) - common_mode.update(gaussian_filter1d(y, sigma=2.0)) + common_mode.update(y) diff = y - _prev_spectrum _prev_spectrum = np.copy(y) - r = gaussian_filter1d(y, sigma=0.5) - common_mode.value - # g = gaussian_filter1d(y, sigma=0.5) - common_mode.value - b = gaussian_filter1d(y, sigma=0.0) - common_mode.value - # Update temporal filters - r = r_filt.update(r) - # g = g_filt.update(g) + # Color channel mappings + r = r_filt.update(y - common_mode.value) g = np.abs(diff) - b = b_filt.update(b) + b = b_filt.update(np.copy(y)) # Mirror the color channels for symmetric output - pixel_r = np.concatenate((r[::-1], r)) - pixel_g = np.concatenate((g[::-1], g)) - pixel_b = np.concatenate((b[::-1], b)) - output = np.array([pixel_r, pixel_g, pixel_b]) * 255.0 + r = np.concatenate((r[::-1], r)) + g = np.concatenate((g[::-1], g)) + b = np.concatenate((b[::-1], b)) + output = np.array([r, g,b]) * 255 return output @@ -168,31 +185,21 @@ mel_smoothing = dsp.ExpFilter(np.tile(1e-1, config.N_FFT_BINS), volume = dsp.ExpFilter(config.MIN_VOLUME_THRESHOLD, alpha_decay=0.02, alpha_rise=0.02) fft_window = np.hamming(int(config.MIC_RATE / config.FPS) * config.N_ROLLING_HISTORY) -# Keeps track of the number of buffer overflows -# Lots of buffer overflows could mean that FPS is set too high -buffer_overflows = 1 +prev_fps_update = time.time() -def microphone_update(stream): - global y_roll, prev_rms, prev_exp - # Retrieve and normalize the new audio samples - try: - y = np.fromstring(stream.read(samples_per_frame), dtype=np.int16) - except IOError: - y = y_roll[config.N_ROLLING_HISTORY - 1, :] - global buffer_overflows - print('Buffer overflows: {0}'.format(buffer_overflows)) - buffer_overflows += 1 +def microphone_update(audio_samples): + global y_roll, prev_rms, prev_exp, prev_fps_update # Normalize samples between 0 and 1 - y = y / 2.0**15 + y = audio_samples / 2.0**15 # Construct a rolling window of audio samples y_roll[:-1] = y_roll[1:] y_roll[-1, :] = np.copy(y) - y_data = np.concatenate(y_roll, axis=0) - volume.update(np.nanmean(y_data ** 2)) - - if volume.value < config.MIN_VOLUME_THRESHOLD: - print('No audio input. Volume below threshold. Volume:', volume.value) + y_data = np.concatenate(y_roll, axis=0).astype(np.float32) + + vol = np.max(np.abs(y_data)) + if vol < config.MIN_VOLUME_THRESHOLD: + print('No audio input. Volume below threshold. Volume:', vol) led.pixels = np.tile(0, (3, config.N_PIXELS)) led.update() else: @@ -204,13 +211,14 @@ def microphone_update(stream): y_padded = np.pad(y_data, (0, N_zeros), mode='constant') YS = np.abs(np.fft.rfft(y_padded)[:N // 2]) # Construct a Mel filterbank from the FFT data - mel = np.atleast_2d(np.abs(YS)).T * dsp.mel_y.T + mel = np.atleast_2d(YS).T * dsp.mel_y.T # Scale data to values more suitable for visualization - mel = np.mean(mel, axis=0) + # mel = np.sum(mel, axis=0) + mel = np.sum(mel, axis=0) mel = mel**2.0 # Gain normalization mel_gain.update(np.max(gaussian_filter1d(mel, sigma=1.0))) - mel = mel / mel_gain.value + mel /= mel_gain.value mel = mel_smoothing.update(mel) # Map filterbank output onto LED strip output = visualization_effect(mel) @@ -226,8 +234,12 @@ def microphone_update(stream): b_curve.setData(y=led.pixels[2]) if config.USE_GUI: app.processEvents() + if config.DISPLAY_FPS: - print('FPS {:.0f} / {:.0f}'.format(frames_per_second(), config.FPS)) + fps = frames_per_second() + if time.time() - 0.5 > prev_fps_update: + prev_fps_update = time.time() + print('FPS {:.0f} / {:.0f}'.format(fps, config.FPS)) # Number of audio samples to read every time frame