diff --git a/SENSORY_BRIDGE_FIRMWARE/GDFT.h b/SENSORY_BRIDGE_FIRMWARE/GDFT.h index 1349a42..9cffcaa 100644 --- a/SENSORY_BRIDGE_FIRMWARE/GDFT.h +++ b/SENSORY_BRIDGE_FIRMWARE/GDFT.h @@ -57,12 +57,17 @@ // Obscure audio magic happens here void IRAM_ATTR process_GDFT() { + float MOOD_VAL = CONFIG.MOOD; + if (CONFIG.LIGHTSHOW_MODE == LIGHT_MODE_BLOOM) { + MOOD_VAL = 1.0; + } + static bool interlace_flip = false; interlace_flip = !interlace_flip; // Switch field every frame on lower notes to save execution time // Reset magnitude caps every frame for (uint8_t i = 0; i < NUM_ZONES; i++) { - max_mags[i] = CONFIG.MAGNITUDE_FLOOR / CONFIG.SENSITIVITY; // Higher than the average noise floor + max_mags[i] = 0.0; // Higher than the average noise floor } // Increment spectrogram history index @@ -74,392 +79,165 @@ void IRAM_ATTR process_GDFT() { // Run GDFT (Goertzel-based Discrete Fourier Transform) with 64 frequencies // Fixed-point code adapted from example here: https://sourceforge.net/p/freetel/code/HEAD/tree/misc/goertzal/goertzal.c for (uint16_t i = 0; i < NUM_FREQS; i++) { // Run 64 times - bool field = bitRead(i, 0); // odd or even - - // If note is part of field being rendered, is >= index 16, or is the first note - if (field == interlace_flip || i >= 16) { - int32_t q0, q1, q2; - int64_t mult; - - q1 = 0; - q2 = 0; - - float window_pos = 0.0; - for (uint16_t n = 0; n < frequencies[i].block_size; n++) { // Run Goertzel for "block_size" iterations - int32_t sample = 0; - //sample = ((int32_t)sample_window[SAMPLE_HISTORY_LENGTH - 1 - n] * (int32_t)window_lookup[uint16_t(window_pos)]) >> 16; - sample = (int32_t)sample_window[SAMPLE_HISTORY_LENGTH - 1 - n]; - mult = (int64_t)frequencies[i].coeff_q14 * (int64_t)q1; - q0 = (sample >> 6) + (mult >> 14) - q2; - q2 = q1; - q1 = q0; - - window_pos += frequencies[i].window_mult; - } + int32_t q0, q1, q2; + int64_t mult; - mult = (int64_t)frequencies[i].coeff_q14 * (int64_t)q1; - magnitudes[i] = q2 * q2 + q1 * q1 - ((int32_t)(mult >> 14)) * q2; // Calculate raw magnitudes + q1 = 0; + q2 = 0; - // Normalize output - magnitudes[i] *= float(frequencies[i].block_size_recip); + float window_pos = 0.0; + for (uint16_t n = 0; n < frequencies[i].block_size; n++) { // Run Goertzel for "block_size" iterations + int32_t sample = 0; + //sample = ((int32_t)sample_window[SAMPLE_HISTORY_LENGTH - 1 - n] * (int32_t)window_lookup[uint16_t(window_pos)]) >> 16; + sample = (int32_t)sample_window[SAMPLE_HISTORY_LENGTH - 1 - n]; + mult = (int32_t)frequencies[i].coeff_q14 * (int32_t)q1; + q0 = (sample >> 6) + (mult >> 14) - q2; + q2 = q1; + q1 = q0; - // Scale magnitudes this way to help even out the response curve further - float prog = i / float(NUM_FREQS); - prog *= prog; - if (prog < 0.1) { - prog = 0.1; - } - magnitudes[i] *= prog; + window_pos += frequencies[i].window_mult; + } - if (magnitudes[i] < 0.0) { // Prevent negative values - magnitudes[i] = 0.0; - } + mult = (int32_t)frequencies[i].coeff_q14 * (int32_t)q1; + magnitudes[i] = q2 * q2 + q1 * q1 - ((int32_t)(mult >> 14)) * q2; // Calculate raw magnitudes + + if (magnitudes[i] < 0) { // Prevent negative values + magnitudes[i] = 0; } - } - // When enabled, streams magnitudes[] array over Serial - if (stream_magnitudes == true) { - if (serial_iter >= 2) { // Don't print every frame - serial_iter = 0; - USBSerial.print("sbs((magnitudes="); - for (uint16_t i = 0; i < NUM_FREQS; i++) { - USBSerial.print(uint32_t(magnitudes[i])); - if (i < NUM_FREQS - 1) { - USBSerial.print(','); - } - } - USBSerial.println("))"); + magnitudes[i] = sqrt(magnitudes[i]); + + // Normalizing the magnitude + float normalized_magnitude = magnitudes[i] / float(frequencies[i].block_size / 2.0); + magnitudes_normalized[i] = normalized_magnitude; + + if (frequencies[i].target_freq == 440.0) { + //USBSerial.println(magnitudes_normalized[i]); } + + magnitudes_normalized_avg[i] = (magnitudes_normalized[i] * 0.3) + (magnitudes_normalized_avg[i] * (1.0 - 0.3)); } // Gather noise data if noise_complete == false if (noise_complete == false) { for (uint8_t i = 0; i < NUM_FREQS; i += 1) { - if (magnitudes[i] > noise_samples[i]) { - noise_samples[i] = magnitudes[i]; + if (magnitudes_normalized_avg[i] > noise_samples[i]) { + noise_samples[i] = magnitudes_normalized_avg[i]; } } noise_iterations++; - if (noise_iterations >= 1024) { // Calibration complete + if (noise_iterations >= 256) { // Calibration complete noise_complete = true; USBSerial.println("NOISE CAL COMPLETE"); - CONFIG.DC_OFFSET = dc_offset_sum / 1024.0; // Calculate average DC offset and store it + CONFIG.DC_OFFSET = dc_offset_sum / 256.0; // Calculate average DC offset and store it save_ambient_noise_calibration(); // Save results to noise_cal.bin save_config(); // Save config to config.bin } } - // Apply noise reduction data, estimate max values + // Apply noise reduction data for (uint8_t i = 0; i < NUM_FREQS; i += 1) { if (noise_complete == true) { - magnitudes[i] -= noise_samples[i] * 1.5; // Treat noise 1.5x louder than calibration - if (magnitudes[i] < 0.0) { - magnitudes[i] = 0.0; + magnitudes_normalized_avg[i] -= float(noise_samples[i] * SQ15x16(1.5)); // Treat noise 1.5x louder than calibration + if (magnitudes_normalized_avg[i] < 0.0) { + magnitudes_normalized_avg[i] = 0.0; } } } - for (uint8_t i = 0; i < NUM_FREQS; i++) { - //magnitudes[i] *= magnitude_scale; + memcpy(magnitudes_final, magnitudes_normalized_avg, sizeof(float) * NUM_FREQS); + low_pass_array(magnitudes_final, magnitudes_last, NUM_FREQS, SYSTEM_FPS, 1.0 + (10.0 * MOOD_VAL)); + memcpy(magnitudes_last, magnitudes_final, sizeof(float) * NUM_FREQS); - mag_targets[i] = magnitudes[i]; - if (mag_targets[i] > max_mags[frequencies[i].zone]) { - max_mags[frequencies[i].zone] = mag_targets[i]; - } - } + /* - if (stream_max_mags == true) { - USBSerial.print("sbs((max_mags="); - for (uint8_t i = 0; i < NUM_ZONES; i++) { - USBSerial.print(max_mags[i]); - if (i < NUM_ZONES - 1) { - USBSerial.print(','); - } - } - USBSerial.println("))"); - } - - // Two different algorithms are used to smooth the display output: - // - // Smoothing Type A: "Followers" - // ----------------------------- - // The live magnitudes are stored in an array (mag_targets[]) - // These are the most recent values, and can fluctuate wildly. - // - // A second array of magnitude bins (mag_followers[]) also exists. - // - // On each frame, the difference (delta) between mag_targets[i] - // and mag_followers[i] is calculated. mag_followers[i] is - // incremented or decremented by (delta * amount) - amount being - // a value between 0.0 and 1.0. Scaling the amount that the - // follower is able to change per frame limits its speed and - // makes its approach to the destination value non-linear. - // - // With the "amount" set to only 0.5, the mag_followers[] array - // is only allowed to change by half the amount (on each frame) - // that the targets array could. This slows down rapid transient - // changes, adding a viscous smoothing effect that's slower to - // track on as it gets closer to the target value. - // - // - // Smoothing Type B: "Exponential Averaging" - // ----------------------------------------- - // This one you might have heard of. Here's how it's implemented here, - // with smoothing set to 0.9 (a high amount) as an example: - // - // smooth_frame = new_frame * (1.0-smoothing) + last_frame * smoothing; - // - // This takes the new data, scales it to 10% its original range, - // scales the output of the last frame to 90% its original range, - // and sums the two. Afterwards, the result is stored in "last_frame" - // for the next run. - // - // This method is applied later, in the // Make Spectrogram section, - // and is good for smoothing rapid changes but severly restricts the - // percieved reaction time of the display. - // - // - // The two smoothing algorithms are applied differently based on the MOOD knob position: - // +---------------------------+-----------------+-----------------+----------------------------------------------------+ - // | KNOB POSITION | TYPE A AMOUNT | TYPE B AMOUNT | EFFECT | - // +---------------------------+-----------------+-----------------+----------------------------------------------------+ - // | 0% (all the way left) | 100% | 100% | EXTREMELY SMOOTH, HIGHER LATENCY | - // | 25% | 50% | 100% | | - // | 50% (middle) | 0% | 100% | GOOD BALANCE, LOW LATENCY | - // | 75% | 0% | 50% | | - // | 100% (all the way right) | 0% | 0% | FASTEST LED REACTION TIME, "NO" SMOOTHING, HARSH | - // +---------------------------+-----------------+-----------------+----------------------------------------------------+ - // - // There's also some extra math so that smoothing values don't actually reach 0% (no change allowed per frame) - // (So the 0% - 100% range of this chart is mostly representative) - - // These two upcoming variables are calculated in knobs.h: - // float smoothing_follower - // float smoothing_exp_average - - // Smooth GDFT changes with MOOD knob value ("Follower" algorithm) - for (uint8_t i = 0; i < NUM_FREQS; i += 1) { - if (mag_targets[i] > mag_followers[i]) { - float delta = mag_targets[i] - mag_followers[i]; - mag_followers[i] += delta * (smoothing_follower * 0.65); - } - - else if (mag_targets[i] < mag_followers[i]) { - float delta = mag_followers[i] - mag_targets[i]; - mag_followers[i] -= delta * (smoothing_follower * 0.75); - } - } - - // The max_mags[] array is subject to the same type of smoothing, - // hardcoded to 0.075. - for (uint8_t i = 0; i < NUM_ZONES; i++) { - if (max_mags[i] > max_mags_followers[i]) { - float delta = max_mags[i] - max_mags_followers[i]; - max_mags_followers[i] += delta * 0.0125; - } - if (max_mags[i] < max_mags_followers[i]) { - float delta = max_mags_followers[i] - max_mags[i]; - max_mags_followers[i] -= delta * 0.0125; - } - } - - if (stream_max_mags_followers == true) { - USBSerial.print("sbs((max_mags_followers="); - for (uint8_t i = 0; i < NUM_ZONES; i++) { - USBSerial.print(max_mags_followers[i]); - if (i < NUM_ZONES - 1) { - USBSerial.print('\t'); + // When enabled, streams magnitudes[] array over Serial + if (stream_magnitudes == true) { + if (serial_iter >= 2) { // Don't print every frame + serial_iter = 0; + USBSerial.print("sbs((magnitudes="); + for (uint16_t i = 0; i < NUM_FREQS; i++) { + USBSerial.print(uint32_t(magnitudes[i])); + if (i < NUM_FREQS - 1) { + USBSerial.print(','); + } } - } - USBSerial.println("))"); - } - - float max_mag = CONFIG.MAGNITUDE_FLOOR / CONFIG.SENSITIVITY; - for (uint8_t i = 0; i < NUM_ZONES; i++) { - if (max_mags_followers[i] > max_mag) { - max_mag = max_mags_followers[i]; + USBSerial.println("))"); } } + */ - // Make Spectrogram from raw magnitudes - for (uint8_t i = 0; i < NUM_FREQS; i += 1) { - // Normalize our frequency bins to 0.0-1.0 range, which acts like an audio compressor at the same time - float max_mag = interpolate(i / float(NUM_FREQS), max_mags_followers, NUM_ZONES); - float mag_float = mag_followers[i] / max_mag; - - //mag_float *= 2.0; - - // Restrict range, allowing for clipped values at peaks and valleys - if (mag_float < 0.0) { - mag_float = 0.0; - } else if (mag_float > 1.0) { - mag_float = 1.0; - } - - // Smooth spectrogram changes with MOOD knob value (Exp. Avg. smoothing) - mag_float = mag_float * (1.0 - smoothing_exp_average) + mag_float_last[i] * smoothing_exp_average; - mag_float_last[i] = mag_float; + static SQ15x16 goertzel_max_value = 0.0001; + SQ15x16 max_value = 0.00001; - //mag_float *= (CONFIG.GAIN); - if (mag_float > 1.0) { - mag_float = 1.0; + for (uint8_t i = 0; i < NUM_FREQS; i += 1) { // 64 freqs + if (magnitudes_final[i] > max_value) { + max_value = magnitudes_final[i]; } - - mag_float = sqrt(sqrt(mag_float)); - - note_spectrogram[i] = mag_float; // This array is the current value - spectrogram_history[spectrogram_history_index][i] = note_spectrogram[i]; // This array is the value's history } -} - -float calc_punch(uint8_t low_bin, uint8_t high_bin) { - const float push_max = 100000.0; - float punch_pos_decay = 0.0; - float smoothing = 0.128; - float punch_pos = 0.0; + max_value *= SQ15x16(0.995); - float punch_scaling = NUM_FREQS / (high_bin - low_bin); - - for (uint8_t i = low_bin; i < high_bin; i++) { - float delta = spectrogram_history[2][i] * spectrogram_history[2][i] - spectrogram_history[0][i] * spectrogram_history[0][i]; - if (delta < 0.0) { - delta = 0.0; - } - punch_pos += (delta * punch_scaling); + if (max_value > goertzel_max_value) { + SQ15x16 delta = max_value - goertzel_max_value; + goertzel_max_value += delta * SQ15x16(0.0050); + } else if (goertzel_max_value > max_value) { + SQ15x16 delta = goertzel_max_value - max_value; + goertzel_max_value -= delta * SQ15x16(0.0025); } - if (punch_pos > 1.0) { - punch_pos = 1.0; + if (goertzel_max_value < 4.0) { + goertzel_max_value = 4.0; } - float punch_pos_smooth = 0.0; - punch_pos_smooth = punch_pos * (smoothing) + punch_pos_smooth * (1.0 - smoothing); - - punch_pos_decay *= 0.9; + // Normalize output using goertzel_max_val + SQ15x16 multiplier = SQ15x16(1.0) / goertzel_max_value; + //multiplier += SQ15x16(0.10); // Overshoot by 10% - if (punch_pos_smooth > punch_pos_decay) { - punch_pos_decay = punch_pos_smooth; + for (uint16_t i = 0; i < NUM_FREQS; i += 1) { + spectrogram[i] = magnitudes_final[i] * multiplier; } - - float punch = push_max * (punch_pos_decay * punch_pos_decay * punch_pos_decay * punch_pos_decay * punch_pos_decay); - return punch; } -void lookahead_smoothing() { - // This one is weird too, let's talk. - // - // Output to the LEDs is delayed by 2 frames. This is virtually - // invisible to the naked eye at these speeds, and allows this - // function to look ahead and do this: - // - // IF - // The next frame will have a decreased value (or opposite) - // AND - // The frame after that will have an increased value again (or opposite) - // THEN - // The next frame should just become an average of the current - // and the "after next" frame to prevent that rapid change - // from making to to the LEDs. - // - // This reduces (FPS/2)Hz flicker, especially at lower brightness - // frequency bins. - - int16_t past_index = spectrogram_history_index - 2; - if (past_index < 0) { - past_index += spectrogram_history_length; - } - - int16_t look_ahead_1 = spectrogram_history_index - 1; - if (look_ahead_1 < 0) { - look_ahead_1 += spectrogram_history_length; - } +void calculate_novelty(uint32_t t_now) { + static uint32_t iter = 0; + iter++; - int16_t look_ahead_2 = spectrogram_history_index - 0; - if (look_ahead_2 < 0) { - look_ahead_2 += spectrogram_history_length; - } - - for (uint8_t i = 0; i < NUM_FREQS; i += 1) { - bool look_ahead_1_rising = false; - bool look_ahead_2_rising = false; - - if (spectrogram_history[look_ahead_1][i] > spectrogram_history[past_index][i]) { - look_ahead_1_rising = true; - } - if (spectrogram_history[look_ahead_2][i] > spectrogram_history[look_ahead_1][i]) { - look_ahead_2_rising = true; + // Calculate "novelty" (positive change) in this moment by marking the positive changes from the previous frame + // Sum in a column-wise fashion into novelty_now + SQ15x16 novelty_now = 0.0; + for (uint16_t i = 0; i < NUM_FREQS; i++) { + int16_t rounded_index = spectral_history_index - 1; + while (rounded_index < 0) { + rounded_index += SPECTRAL_HISTORY_LENGTH; } + SQ15x16 novelty_bin = spectrogram[i] - spectral_history[rounded_index][i]; - if (look_ahead_1_rising != look_ahead_2_rising) { // if spike, set spike frame to average of neighbors - spectrogram_history[look_ahead_1][i] = (spectrogram_history[past_index][i] + spectrogram_history[look_ahead_2][i]) / 2.0; + if (novelty_bin < 0.0) { + novelty_bin = 0.0; } - note_spectrogram_smooth[i] = spectrogram_history[past_index][i]; - note_spectrogram_long_term[i] = (note_spectrogram_long_term[i] * 0.95) + (note_spectrogram_smooth[i] * 0.05); + novelty_now += novelty_bin; } + novelty_now /= NUM_FREQS; // Normalize result - for (uint8_t i = 0; i < NATIVE_RESOLUTION; i++) { - //note_spectrogram_smooth[i] = (note_spectrogram_smooth[i] + note_spectrogram_smooth_frame_blending[i]) * 0.5; - - if (note_spectrogram_smooth[i] > 1.0) { - note_spectrogram_smooth[i] = 1.0; - } - - note_spectrogram_smooth_frame_blending[i] = note_spectrogram_smooth[i]; + // Append current spectrogram to last place in history: + for (uint16_t b = 0; b < NUM_FREQS; b += 8) { + spectral_history[spectral_history_index][b + 0] = spectrogram[b + 0]; + spectral_history[spectral_history_index][b + 1] = spectrogram[b + 1]; + spectral_history[spectral_history_index][b + 2] = spectrogram[b + 2]; + spectral_history[spectral_history_index][b + 3] = spectrogram[b + 3]; + spectral_history[spectral_history_index][b + 4] = spectrogram[b + 4]; + spectral_history[spectral_history_index][b + 5] = spectrogram[b + 5]; + spectral_history[spectral_history_index][b + 6] = spectrogram[b + 6]; + spectral_history[spectral_history_index][b + 7] = spectrogram[b + 7]; } - // Make Chromagram - chromagram_max_val = 0.0; - for (uint8_t i = 0; i < 12; i++) { - note_chromagram[i] = 0; - } + // Append new novelty measurement to novelty curve history + novelty_curve[spectral_history_index] = sqrt(float(novelty_now)); - for (uint8_t octave = 0; octave < 6; octave++) { - for (uint8_t note = 0; note < 12; note++) { - uint16_t note_index = 12 * octave + note; - if (note_index < NUM_FREQS && note_index < CONFIG.CHROMAGRAM_RANGE) { - if (note_spectrogram_smooth[note_index] > note_chromagram[note]) { - note_chromagram[note] = note_spectrogram_smooth[note_index]; - } - - if (note_chromagram[note] > chromagram_max_val) { - chromagram_max_val = note_chromagram[note]; - } - } - } - } - - // Calculate "punch" of the full frequency range (used by auto color-cycling) - current_punch = calc_punch(0, NUM_FREQS); // (lightshow_modes.h) - - if (stream_spectrogram == true) { - if (serial_iter >= 2) { // Don't print every frame - serial_iter = 0; - USBSerial.print("sbs((spectrogram="); - for (uint16_t i = 0; i < NUM_FREQS; i++) { - uint16_t bin = 999 * note_spectrogram_smooth[i]; - USBSerial.print(bin); - if (i < NUM_FREQS - 1) { - USBSerial.print(','); - } - } - USBSerial.println("))"); - } - } - - if (stream_chromagram == true) { - if (serial_iter >= 2) { // Don't print every frame - serial_iter = 0; - USBSerial.print("sbs((chromagram="); - for (uint16_t i = 0; i < 12; i++) { - uint16_t bin = 999 * note_chromagram[i]; - USBSerial.print(bin); - if (i < 12 - 1) { - USBSerial.print(','); - } - } - USBSerial.println("))"); - } + spectral_history_index++; + if (spectral_history_index >= SPECTRAL_HISTORY_LENGTH) { + spectral_history_index -= SPECTRAL_HISTORY_LENGTH; } } diff --git a/SENSORY_BRIDGE_FIRMWARE/SENSORY_BRIDGE_FIRMWARE.ino b/SENSORY_BRIDGE_FIRMWARE/SENSORY_BRIDGE_FIRMWARE.ino index 82f34ce..21583ed 100644 --- a/SENSORY_BRIDGE_FIRMWARE/SENSORY_BRIDGE_FIRMWARE.ino +++ b/SENSORY_BRIDGE_FIRMWARE/SENSORY_BRIDGE_FIRMWARE.ino @@ -50,7 +50,7 @@ ---------------------------------------------------------------------*/ -#define FIRMWARE_VERSION 30200 // Try "V" on the Serial port for this! +#define FIRMWARE_VERSION 40000 // Try "V" on the Serial port for this! // MmmPP M = Major version, m = Minor version, P = Patch version // (i.e 3.5.4 would be 30504) @@ -58,12 +58,11 @@ enum lightshow_modes { LIGHT_MODE_GDFT, // ------------- GDFT - Goertzel-based Discrete Fourier Transform // (I made this name up. Saved you a search.) - LIGHT_MODE_GDFT_CHROMAGRAM, // -- Chromagram of GDFT - LIGHT_MODE_BLOOM, // -- Slow Bloom Mode - LIGHT_MODE_BLOOM_FAST, // -- Fast Bloom Mode - LIGHT_MODE_VU, // -- Not a real VU for any measurement sake, just a dance-y LED bar - LIGHT_MODE_VU_DOT, // -- Alternate VU display mode - dot with motion blur - LIGHT_MODE_KALEIDOSCOPE, // -- Three color channels 2D Perlin noise affected by the onsets of low, mid and high pitches + LIGHT_MODE_GDFT_CHROMAGRAM, // -- Chromagram of GDFT + LIGHT_MODE_GDFT_CHROMAGRAM_DOTS, // -- Chromagram of GDFT + LIGHT_MODE_BLOOM, // -- Bloom Mode + LIGHT_MODE_VU_DOT, // -- Not a real VU for any measurement sake, just a dance-y LED show + LIGHT_MODE_KALEIDOSCOPE, // -- Three color channels 2D Perlin noise affected by the onsets of low, mid and high pitches NUM_MODES // used to know the length of this list if it changes in the future }; @@ -71,12 +70,15 @@ enum lightshow_modes { // External dependencies ------------------------------------------------------------- #include // Needed for Station Mode #include // P2P wireless communication library (p2p.h below) +#include // RNG Functions #include // Handles LED color data and display #include // Filesystem functions (bridge_fs.h below) #include // LittleFS implementation #include // Scheduled tasks library #include // USB Connection handling #include // Allows firmware updates via USB MSC +#include +#include // Include Sensory Bridge firmware files, sorted high to low, by boringness ;) ------- #include "strings.h" // Strings for printing @@ -85,11 +87,11 @@ enum lightshow_modes { #include "globals.h" // Global variables #include "presets.h" // Configuration presets by name #include "bridge_fs.h" // Filesystem access (save/load configuration) +#include "utilities.h" // Misc. math and other functions #include "i2s_audio.h" // I2S Microphone audio capture #include "led_utilities.h" // LED color/transform utility functions #include "noise_cal.h" // Background noise removal #include "p2p.h" // Sensory Sync handling -#include "utilities.h" // Misc. math and other functions #include "buttons.h" // Watch the status of buttons #include "knobs.h" // Watch the status of knobs... #include "serial_menu.h" // Watch the Serial port... *sigh* @@ -138,13 +140,27 @@ void loop() { run_sweet_spot(); // (led_utilities.h) // Based on the current audio volume, alter the Sweet Spot indicator LEDs + // Calculates audio loudness (VU) using RMS, adjusting for noise floor based on calibration + calculate_vu(); + function_id = 7; process_GDFT(); // (GDFT.h) // Execute GDFT and post-process // (If you're wondering about that weird acronym, check out the source file) + // Watches the rate of change in the Goertzel bins to guide decisions for auto-color shifting + calculate_novelty(t_now); + + if (CONFIG.AUTO_COLOR_SHIFT == true) { // Automatically cycle color based on density of positive spectral changes + // Use the "novelty" findings of the above function to affect color shifting when auto-color shifts are enabled + process_color_shift(); + } else { + hue_position = 0; + hue_shifting_mix = -0.35; + } + function_id = 8; - lookahead_smoothing(); // (GDFT.h) + //lookahead_smoothing(); // (GDFT.h) // Peek at upcoming frames to study/prevent flickering function_id = 8; @@ -162,77 +178,62 @@ void loop() { // Run the lights in their own thread! ------------------------------------------------------------- void led_thread(void* arg) { while (true) { - if (led_thread_halt == false) { - if (noise_complete == true) { // If we're not gathering ambient noise data - if (mode_transition_queued == true || noise_transition_queued == true) { // If transition queued - run_transition_fade(); // (led_utilities.h) Fade to black between modes - } - - if (CONFIG.AUTO_COLOR_SHIFT == true) { // Automatically cycle color based on density of positive spectral changes - hue_shift -= current_punch; - } else { - hue_shift = 0; // Reset hue shift if non-zero - } - - // Based on the value of CONFIG.LIGHTSHOW_MODE, we call a - // different rendering function from lightshow_modes.h: - - if (CONFIG.LIGHTSHOW_MODE == LIGHT_MODE_GDFT) { - light_mode_gdft(); // (lightshow_modes.h) GDFT spectrogram display - } else if (CONFIG.LIGHTSHOW_MODE == LIGHT_MODE_GDFT_CHROMAGRAM) { - light_mode_gdft_chromagram(); // (lightshow_modes.h) GDFT chromagram display - } else if (CONFIG.LIGHTSHOW_MODE == LIGHT_MODE_BLOOM) { - light_mode_bloom(false); // (lightshow_modes.h) Bloom Mode display - } else if (CONFIG.LIGHTSHOW_MODE == LIGHT_MODE_BLOOM_FAST) { - light_mode_bloom(true); // (lightshow_modes.h) Bloom Mode display (faster) - } else if (CONFIG.LIGHTSHOW_MODE == LIGHT_MODE_VU) { - light_mode_vu(); // (lightshow_modes.h) VU Mode display - } else if (CONFIG.LIGHTSHOW_MODE == LIGHT_MODE_VU_DOT) { - light_mode_vu_dot(); // (lightshow_modes.h) VU Mode display (dot version) - } else if (CONFIG.LIGHTSHOW_MODE == LIGHT_MODE_KALEIDOSCOPE) { - light_mode_kaleidoscope(); // (lightshow_modes.h) Kaleidoscope Mode display - } - - if(CONFIG.PRISM_COUNT > 0){ - apply_prism_effect(CONFIG.PRISM_COUNT, 64); - } + if (led_thread_halt == false) { // If we're not gathering ambient noise data + if (mode_transition_queued == true || noise_transition_queued == true) { // If transition queued + run_transition_fade(); // (led_utilities.h) Fade to black between modes + } - if (CONFIG.MIRROR_ENABLED) { // Mirroring logic - // Don't scale Bloom Mode before mirroring - if (CONFIG.LIGHTSHOW_MODE != LIGHT_MODE_BLOOM && CONFIG.LIGHTSHOW_MODE != LIGHT_MODE_BLOOM_FAST) { - scale_image_to_half(leds); // (led_utilities.h) Image is now 50% height - } - shift_leds_up(leds, 64); // (led_utilities.h) Move image up one half - mirror_image_downwards(leds); // (led_utilities.h) Mirror downwards - } + get_smooth_spectrogram(); + make_smooth_chromagram(); + + // Based on the value of CONFIG.LIGHTSHOW_MODE, we call a + // different rendering function from lightshow_modes.h: + + if (CONFIG.LIGHTSHOW_MODE == LIGHT_MODE_GDFT) { + light_mode_gdft(); // (lightshow_modes.h) GDFT spectrogram display + } else if (CONFIG.LIGHTSHOW_MODE == LIGHT_MODE_GDFT_CHROMAGRAM) { + light_mode_chromagram_gradient(); //light_mode_chromagram_gradient(); // (lightshow_modes.h) GDFT chromagram display + } else if (CONFIG.LIGHTSHOW_MODE == LIGHT_MODE_GDFT_CHROMAGRAM_DOTS) { + light_mode_chromagram_dots(); //light_mode_chromagram_dots(); // (lightshow_modes.h) GDFT chromagram display + } else if (CONFIG.LIGHTSHOW_MODE == LIGHT_MODE_BLOOM) { + light_mode_bloom(); // (lightshow_modes.h) Bloom Mode display + } else if (CONFIG.LIGHTSHOW_MODE == LIGHT_MODE_VU_DOT) { + light_mode_vu_dot(); // (lightshow_modes.h) VU Mode display (dot version) + } else if (CONFIG.LIGHTSHOW_MODE == LIGHT_MODE_KALEIDOSCOPE) { + light_mode_kaleidoscope(); // (lightshow_modes.h) Kaleidoscope Mode display + } - // Render bulb filter - if (CONFIG.BULB_OPACITY > 0.00) { - render_bulb_cover(); - } + if (CONFIG.PRISM_COUNT > 0) { + apply_prism_effect(CONFIG.PRISM_COUNT, 0.25); + } - // If forcing monochromatic incandescent output - if (CONFIG.INCANDESCENT_MODE == true) { - force_incandescent_output(); - } + // Render bulb filter + if (CONFIG.BULB_OPACITY > 0.00) { + render_bulb_cover(); + } - // If not forcing incandescent mode, tint the color image with an incandescent LUT to reduce harsh blues - else if (CONFIG.INCANDESCENT_FILTER > 0.0) { - apply_incandescent_filter(); + // If forcing monochromatic incandescent output + if (CONFIG.INCANDESCENT_MODE == true) { + for (uint8_t i = 0; i < NATIVE_RESOLUTION; i++) { + leds_16[i] = adjust_hue_and_saturation(leds_16[i], 0.05, 0.95); } + } - } else { - noise_cal_led_readout(); // (noise_cal.h) Show the noise profile and progress during calibration + if (CONFIG.MIRROR_ENABLED == false) { // Mirroring logic + unmirror(); } show_leds(); // This sends final RGB data to the LEDS (led_utilities.h) LED_FPS = FastLED.getFPS(); } + else{ + vTaskDelay(0); + } if (CONFIG.LED_TYPE == LED_NEOPIXEL) { //vTaskDelay(1); // delay for 1ms to avoid hogging the CPU } else if (CONFIG.LED_TYPE == LED_DOTSTAR) { // More delay to compensate for faster LEDs - vTaskDelay(3); // delay for 3ms to avoid hogging the CPU + //vTaskDelay(3); // delay for 3ms to avoid hogging the CPU } } -} +} \ No newline at end of file diff --git a/SENSORY_BRIDGE_FIRMWARE/bridge_fs.h b/SENSORY_BRIDGE_FIRMWARE/bridge_fs.h index 25f7190..61c8278 100644 --- a/SENSORY_BRIDGE_FIRMWARE/bridge_fs.h +++ b/SENSORY_BRIDGE_FIRMWARE/bridge_fs.h @@ -4,10 +4,17 @@ extern void reboot(); // system.h +void update_config_filename(uint32_t input) { + snprintf(config_filename, 24, "/CONFIG_%05lu.BIN", input); +} + // Restore all defaults defined in globals.h by removing saved data and rebooting void factory_reset() { - USBSerial.print("Deleting config.bin: "); - if (LittleFS.remove("/config.bin")) { + USBSerial.print("Deleting "); + USBSerial.print(config_filename); + USBSerial.print(": "); + + if (LittleFS.remove(config_filename)) { USBSerial.println("file deleted"); } else { USBSerial.println("delete failed"); @@ -25,8 +32,11 @@ void factory_reset() { // Restore only configuration defaults void restore_defaults() { - USBSerial.print("Deleting config.bin: "); - if (LittleFS.remove("/config.bin")) { + USBSerial.print("Deleting "); + USBSerial.print(config_filename); + USBSerial.print(": "); + + if (LittleFS.remove(config_filename)) { USBSerial.println("file deleted"); } else { USBSerial.println("delete failed"); @@ -40,23 +50,27 @@ void save_config() { if (debug_mode) { USBSerial.print("LITTLEFS: "); } - File file = LittleFS.open("/config.bin", FILE_WRITE); + File file = LittleFS.open(config_filename, FILE_WRITE); if (!file) { if (debug_mode) { - USBSerial.println("Failed to open config.bin for writing!"); + USBSerial.print("Failed to open "); + USBSerial.print(config_filename); + USBSerial.println(" for writing!"); } return; } else { file.seek(0); - uint8_t config_buffer[128]; + uint8_t config_buffer[512]; memcpy(config_buffer, &CONFIG, sizeof(CONFIG)); - for (uint8_t i = 0; i < 128; i++) { + for (uint16_t i = 0; i < 512; i++) { file.write(config_buffer[i]); } if (debug_mode) { - USBSerial.println("WROTE CONFIG SUCCESSFULLY"); + USBSerial.print("WROTE "); + USBSerial.print(config_filename); + USBSerial.println(" SUCCESSFULLY"); } } file.close(); @@ -64,7 +78,10 @@ void save_config() { // Save configuration to LittleFS 10 seconds from now void save_config_delayed() { - last_setting_change = millis(); + if(debug_mode == true){ + USBSerial.println("CONFIG SAVE QUEUED"); + } + next_save_time = millis()+5000; settings_updated = true; } @@ -75,16 +92,18 @@ void load_config() { } bool queue_factory_reset = false; - File file = LittleFS.open("/config.bin", FILE_READ); + File file = LittleFS.open(config_filename, FILE_READ); if (!file) { if (debug_mode) { - USBSerial.println("Failed to open config.bin for reading!"); + USBSerial.print("Failed to open "); + USBSerial.print(config_filename); + USBSerial.println(" for reading!"); } return; } else { file.seek(0); - uint8_t config_buffer[256]; - for (uint8_t i = 0; i < sizeof(CONFIG); i++) { + uint8_t config_buffer[512]; + for (uint16_t i = 0; i < sizeof(CONFIG); i++) { config_buffer[i] = file.read(); } @@ -118,7 +137,7 @@ void save_ambient_noise_calibration() { file.seek(0); for (uint16_t i = 0; i < NUM_FREQS; i++) { - float in_val = noise_samples[i]; + float in_val = float(noise_samples[i]); temp.long_val_float = in_val; @@ -156,7 +175,7 @@ void load_ambient_noise_calibration() { temp.bytes[2] = file.read(); temp.bytes[3] = file.read(); - noise_samples[i] = temp.long_val_float; + noise_samples[i] = SQ15x16(temp.long_val_float); } file.close(); @@ -170,6 +189,8 @@ void init_fs() { USBSerial.print("INIT FILESYSTEM: "); USBSerial.println(LittleFS.begin(true) == true ? PASS : FAIL); + update_config_filename(FIRMWARE_VERSION); + load_ambient_noise_calibration(); load_config(); } diff --git a/SENSORY_BRIDGE_FIRMWARE/constants.h b/SENSORY_BRIDGE_FIRMWARE/constants.h index 8a21674..6c0778c 100644 --- a/SENSORY_BRIDGE_FIRMWARE/constants.h +++ b/SENSORY_BRIDGE_FIRMWARE/constants.h @@ -1,7 +1,7 @@ // AUDIO ####################################################### #define SERIAL_BAUD 230400 -#define DEFAULT_SAMPLE_RATE 24400 +#define DEFAULT_SAMPLE_RATE 12800 #define SAMPLE_HISTORY_LENGTH 4096 // Don't change this unless you're willing to do a lot of other work on the code :/ @@ -11,6 +11,48 @@ #define I2S_PORT I2S_NUM_0 +#define SPECTRAL_HISTORY_LENGTH 5 + +#define MAX_DOTS 128 + +enum reserved_dots { + GRAPH_NEEDLE, + GRAPH_DOT_1, + GRAPH_DOT_2, + GRAPH_DOT_3, + GRAPH_DOT_4, + GRAPH_DOT_5, + RIPPLE_LEFT, + RIPPLE_RIGHT, + + RESERVED_DOTS +}; + +enum knob_names { + K_NONE, + K_PHOTONS, + K_CHROMA, + K_MOOD +}; + +struct CRGB16 { // Unsigned Q8.8 Fixed-point color channels + SQ15x16 r; + SQ15x16 g; + SQ15x16 b; +}; + +struct DOT { + SQ15x16 position; + SQ15x16 last_position; +}; + +struct KNOB { + SQ15x16 value; + SQ15x16 last_value; + SQ15x16 change_rate; + uint32_t last_change; +}; + const float notes[] = { 55.00000, 58.27047, 61.73541, 65.40639, 69.29566, 73.41619, 77.78175, 82.40689, 87.30706, 92.49861, 97.99886, 103.8262, 110.0000, 116.5409, 123.4708, 130.8128, 138.5913, 146.8324, 155.5635, 164.8138, 174.6141, 184.9972, 195.9977, 207.6523, @@ -25,49 +67,135 @@ const float notes[] = { // GPIO PINS ####################################################### #define PHOTONS_PIN 1 -#define CHROMA_PIN 2 -#define MOOD_PIN 3 +#define CHROMA_PIN 2 +#define MOOD_PIN 3 -#define I2S_BCLK_PIN 33 -#define I2S_LRCLK_PIN 34 -#define I2S_DIN_PIN 35 +#define I2S_BCLK_PIN 33 +#define I2S_LRCLK_PIN 34 +#define I2S_DIN_PIN 35 -#define LED_DATA_PIN 36 -#define LED_CLOCK_PIN 37 +#define LED_DATA_PIN 36 +#define LED_CLOCK_PIN 37 -#define RNG_SEED_PIN 10 +#define RNG_SEED_PIN 10 -#define NOISE_CAL_PIN 11 -#define MODE_PIN 45 +#define NOISE_CAL_PIN 11 +#define MODE_PIN 45 -#define SWEET_SPOT_LEFT_PIN 7 -#define SWEET_SPOT_CENTER_PIN 8 -#define SWEET_SPOT_RIGHT_PIN 9 +#define SWEET_SPOT_LEFT_PIN 7 +#define SWEET_SPOT_CENTER_PIN 8 +#define SWEET_SPOT_RIGHT_PIN 9 // OTHER ####################################################### -const float dither_table[8] = { - 0.125, - 0.250, - 0.375, - 0.500, - 0.625, - 0.750, - 0.875, - 1.000 +const SQ15x16 dither_table[4] = { + 0.25, + 0.50, + 0.75, + 1.00 + /* + 0.166666, + 0.333333, + 0.500000, + 0.666666, + 0.833333, + 1.000000 + */ +}; + +SQ15x16 note_colors[12] = { + 0.0000, + 0.0833, + 0.1666, + 0.2499, + 0.3333, + 0.4166, + 0.4999, + 0.5833, + 0.6666, + 0.7499, + 0.8333, + 0.9166 +}; + +const SQ15x16 hue_lookup[64][3] = { + { 1.0000, 0.0000, 0.0000 }, + { 0.9608, 0.0392, 0.0000 }, + { 0.9176, 0.0824, 0.0000 }, + { 0.8745, 0.1255, 0.0000 }, + { 0.8314, 0.1686, 0.0000 }, + { 0.7922, 0.2078, 0.0000 }, + { 0.7490, 0.2510, 0.0000 }, + { 0.7059, 0.2941, 0.0000 }, + { 0.6706, 0.3333, 0.0000 }, + { 0.6706, 0.3725, 0.0000 }, + { 0.6706, 0.4157, 0.0000 }, + { 0.6706, 0.4588, 0.0000 }, + { 0.6706, 0.5020, 0.0000 }, + { 0.6706, 0.5412, 0.0000 }, + { 0.6706, 0.5843, 0.0000 }, + { 0.6706, 0.6275, 0.0000 }, + { 0.6706, 0.6667, 0.0000 }, + { 0.5882, 0.7059, 0.0000 }, + { 0.5059, 0.7490, 0.0000 }, + { 0.4196, 0.7922, 0.0000 }, + { 0.3373, 0.8353, 0.0000 }, + { 0.2549, 0.8745, 0.0000 }, + { 0.1686, 0.9176, 0.0000 }, + { 0.0863, 0.9608, 0.0000 }, + { 0.0000, 1.0000, 0.0000 }, + { 0.0000, 0.9608, 0.0392 }, + { 0.0000, 0.9176, 0.0824 }, + { 0.0000, 0.8745, 0.1255 }, + { 0.0000, 0.8314, 0.1686 }, + { 0.0000, 0.7922, 0.2078 }, + { 0.0000, 0.7490, 0.2510 }, + { 0.0000, 0.7059, 0.2941 }, + { 0.0000, 0.6706, 0.3333 }, + { 0.0000, 0.5882, 0.4157 }, + { 0.0000, 0.5059, 0.4980 }, + { 0.0000, 0.4196, 0.5843 }, + { 0.0000, 0.3373, 0.6667 }, + { 0.0000, 0.2549, 0.7490 }, + { 0.0000, 0.1686, 0.8353 }, + { 0.0000, 0.0863, 0.9176 }, + { 0.0000, 0.0000, 1.0000 }, + { 0.0392, 0.0000, 0.9608 }, + { 0.0824, 0.0000, 0.9176 }, + { 0.1255, 0.0000, 0.8745 }, + { 0.1686, 0.0000, 0.8314 }, + { 0.2078, 0.0000, 0.7922 }, + { 0.2510, 0.0000, 0.7490 }, + { 0.2941, 0.0000, 0.7059 }, + { 0.3333, 0.0000, 0.6706 }, + { 0.3725, 0.0000, 0.6314 }, + { 0.4157, 0.0000, 0.5882 }, + { 0.4588, 0.0000, 0.5451 }, + { 0.5020, 0.0000, 0.5020 }, + { 0.5412, 0.0000, 0.4627 }, + { 0.5843, 0.0000, 0.4196 }, + { 0.6275, 0.0000, 0.3765 }, + { 0.6667, 0.0000, 0.3333 }, + { 0.7059, 0.0000, 0.2941 }, + { 0.7490, 0.0000, 0.2510 }, + { 0.7922, 0.0000, 0.2078 }, + { 0.8353, 0.0000, 0.1647 }, + { 0.8745, 0.0000, 0.1255 }, + { 0.9176, 0.0000, 0.0824 }, + { 0.9608, 0.0000, 0.0392 }, }; -#define SWEET_SPOT_LEFT_CHANNEL 0 -#define SWEET_SPOT_CENTER_CHANNEL 1 -#define SWEET_SPOT_RIGHT_CHANNEL 2 +#define SWEET_SPOT_LEFT_CHANNEL 0 +#define SWEET_SPOT_CENTER_CHANNEL 1 +#define SWEET_SPOT_RIGHT_CHANNEL 2 -#define TWOPI 6.28318530 +#define TWOPI 6.28318530 #define FOURPI 12.56637061 -#define SIXPI 18.84955593 +#define SIXPI 18.84955593 enum led_types { LED_NEOPIXEL, LED_DOTSTAR }; -CRGB incandescent_lookup = CRGB(255, 114, 40); \ No newline at end of file +CRGB16 incandescent_lookup = { 1.0000, 0.4453, 0.1562 }; \ No newline at end of file diff --git a/SENSORY_BRIDGE_FIRMWARE/globals.h b/SENSORY_BRIDGE_FIRMWARE/globals.h index b8cd694..43c4489 100644 --- a/SENSORY_BRIDGE_FIRMWARE/globals.h +++ b/SENSORY_BRIDGE_FIRMWARE/globals.h @@ -13,12 +13,10 @@ struct conf { uint32_t SAMPLE_RATE; uint8_t NOTE_OFFSET; uint8_t SQUARE_ITER; - uint32_t MAGNITUDE_FLOOR; uint8_t LED_TYPE; uint16_t LED_COUNT; uint16_t LED_COLOR_ORDER; bool LED_INTERPOLATION; - uint16_t MAX_BLOCK_SIZE; uint16_t SAMPLES_PER_CHUNK; float SENSITIVITY; bool BOOT_ANIMATION; @@ -31,14 +29,14 @@ struct conf { bool IS_MAIN_UNIT; uint32_t MAX_CURRENT_MA; bool TEMPORAL_DITHERING; - uint16_t MIN_BLOCK_SIZE; bool AUTO_COLOR_SHIFT; float INCANDESCENT_FILTER; bool INCANDESCENT_MODE; - float BACKDROP_BRIGHTNESS; float BULB_OPACITY; float SATURATION; uint8_t PRISM_COUNT; + bool BASE_COAT; + float VU_LEVEL_FLOOR; }; // ------------------------------------------------------------ @@ -50,38 +48,36 @@ conf CONFIG = { 0.00, // CHROMA 0.05, // MOOD LIGHT_MODE_GDFT, // LIGHTSHOW_MODE - false, // MIRROR_ENABLED (>= 3.2.0 defaults no) + true, // MIRROR_ENABLED // Private values DEFAULT_SAMPLE_RATE, // SAMPLE_RATE 12, // NOTE_OFFSET 1, // SQUARE_ITER - 3000, // MAGNITUDE_FLOOR LED_NEOPIXEL, // LED_TYPE 128, // LED_COUNT GRB, // LED_COLOR_ORDER true, // LED_INTERPOLATION - 3000, // MAX_BLOCK_SIZE - 128, // SAMPLES_PER_CHUNK + 96, // SAMPLES_PER_CHUNK 1.0, // SENSITIVITY true, // BOOT_ANIMATION 750, // SWEET_SPOT_MIN_LEVEL 30000, // SWEET_SPOT_MAX_LEVEL 0, // DC_OFFSET 60, // CHROMAGRAM_RANGE - false, // STANDBY_DIMMING + true, // STANDBY_DIMMING false, // REVERSE_ORDER false, // IS_MAIN_UNIT 1500, // MAX_CURRENT_MA true, // TEMPORAL_DITHERING - 5, // MIN_BLOCK_SIZE false, // AUTO_COLOR_SHIFT - 0.80, // INCANDESCENT_FILTER + 0.50, // INCANDESCENT_FILTER false, // INCANDESCENT_MODE - 0.00, // BACKDROP_BRIGHTNESS 0.00, // BULB_OPACITY 1.00, // SATURATION 0, // PRISM_COUNT + true, // BASE_COAT + 0.00, // VU_LEVEL_FLOOR }; conf CONFIG_DEFAULTS; // Used for resetting to default values at runtime @@ -107,7 +103,7 @@ freq frequencies[NUM_FREQS]; // ------------------------------------------------------------ // Hann window lookup table (generated in system.h) ----------- -float window_lookup[4096] = { 0 }; +int16_t window_lookup[4096] = { 0 }; // ------------------------------------------------------------ // A-weighting lookup table (parsed in system.h) -------------- @@ -131,6 +127,15 @@ float a_weight_table[13][2] = { // ------------------------------------------------------------ // Spectrograms (GDFT.h) -------------------------------------- +SQ15x16 spectrogram[NUM_FREQS] = { 0.0 }; +SQ15x16 spectrogram_smooth[NUM_FREQS] = { 0.0 }; +SQ15x16 chromagram_smooth[12] = { 0.0 }; + +SQ15x16 spectral_history[SPECTRAL_HISTORY_LENGTH][NUM_FREQS]; +SQ15x16 novelty_curve[SPECTRAL_HISTORY_LENGTH] = { 0.0 }; + +uint8_t spectral_history_index = 0; + float note_spectrogram[NUM_FREQS] = {0}; float note_spectrogram_smooth[NUM_FREQS] = {0}; float note_spectrogram_smooth_frame_blending[NUM_FREQS] = {0}; @@ -142,7 +147,7 @@ float chromagram_bass_max_val = 0.0; float smoothing_follower = 0.0; float smoothing_exp_average = 0.0; -float chroma_val = 1.0; +SQ15x16 chroma_val = 1.0; bool chromatic_mode = true; // ------------------------------------------------------------ @@ -151,6 +156,7 @@ bool chromatic_mode = true; int32_t i2s_samples_raw[1024] = { 0 }; short sample_window[SAMPLE_HISTORY_LENGTH] = { 0 }; short waveform[1024] = { 0 }; +SQ15x16 waveform_fixed_point[1024] = { 0 }; short waveform_history[4][1024] = { 0 }; uint8_t waveform_history_index = 0; float max_waveform_val_raw = 0.0; @@ -173,12 +179,13 @@ float sweet_spot_min_temp = 0; // Noise calibration (noise_cal.h) ---------------------------- bool noise_complete = true; -float noise_samples[NUM_FREQS] = { 1 }; +SQ15x16 noise_samples[NUM_FREQS] = { 1 }; uint16_t noise_iterations = 0; // ------------------------------------------------------------ // Display buffers (led_utilities.h) -------------------------- +/* CRGB leds[128]; CRGB leds_frame_blending[128]; CRGB leds_fx[128]; @@ -186,10 +193,22 @@ CRGB leds_temp[128]; CRGB leds_last[128]; CRGB leds_aux [128]; CRGB leds_fade[128]; +*/ +CRGB16 leds_16[128]; +CRGB16 leds_16_prev[128]; +CRGB16 leds_16_fx[128]; +CRGB16 leds_16_fx_2[128]; +CRGB16 leds_16_temp[128]; +CRGB16 leds_16_ui[128]; + +SQ15x16 ui_mask[128]; +SQ15x16 ui_mask_height = 0.0; + +CRGB16 *leds_scaled; CRGB *leds_out; -float hue_shift = 0.0; // Used in auto color cycling +SQ15x16 hue_shift = 0.0; // Used in auto color cycling uint8_t dither_step = 0; bool led_thread_halt = false; @@ -232,7 +251,7 @@ bool noise_transition_queued = false; // ------------------------------------------------------------ // Settings tracking (system.h) ------------------------------- -uint32_t last_setting_change = 0; +uint32_t next_save_time = 0; bool settings_updated = false; // ------------------------------------------------------------ @@ -264,7 +283,11 @@ float max_mags_followers[NUM_ZONES] = { 0.000 }; float mag_targets[NUM_FREQS] = { 0.000 }; float mag_followers[NUM_FREQS] = { 0.000 }; float mag_float_last[NUM_FREQS] = { 0.000 }; -float magnitudes[NUM_FREQS] = { 0.000 }; +int32_t magnitudes[NUM_FREQS] = { 0 }; +float magnitudes_normalized[NUM_FREQS] = { 0.000 }; +float magnitudes_normalized_avg[NUM_FREQS] = { 0.000 }; +float magnitudes_last[NUM_FREQS] = { 0.000 }; +float magnitudes_final[NUM_FREQS] = { 0.000 }; // ------------------------------------------------------------ // Look-ahead smoothing (GDFT.h) ------------------------------ @@ -295,6 +318,35 @@ FirmwareMSC MSC_Update; USBCDC USBSerial; bool msc_update_started = false; +// DOTS +DOT dots[MAX_DOTS]; + +// Auto Color Shift +SQ15x16 hue_position = 0.0; +SQ15x16 hue_shift_speed = 0.0; +SQ15x16 hue_push_direction = -1.0; +SQ15x16 hue_destination = 0.0; +SQ15x16 hue_shifting_mix = -0.35; +SQ15x16 hue_shifting_mix_target = 1.0; + +// VU Calculation +SQ15x16 audio_vu_level = 0.0; +SQ15x16 audio_vu_level_average = 0.0; +SQ15x16 audio_vu_level_last = 0.0; + +// Knobs +KNOB knob_photons; +KNOB knob_chroma; +KNOB knob_mood; +uint8_t current_knob = K_NONE; + +// Base Coat +SQ15x16 base_coat_width = 0.0; +SQ15x16 base_coat_width_target = 1.0; + +// Config File +char config_filename[24]; + // WIP BELOW -------------------------------------------------- float MASTER_BRIGHTNESS = 0.0; diff --git a/SENSORY_BRIDGE_FIRMWARE/i2s_audio.h b/SENSORY_BRIDGE_FIRMWARE/i2s_audio.h index 66f8119..4ee5daa 100644 --- a/SENSORY_BRIDGE_FIRMWARE/i2s_audio.h +++ b/SENSORY_BRIDGE_FIRMWARE/i2s_audio.h @@ -4,28 +4,15 @@ #include -#if defined(CONFIG_IDF_TARGET_ESP32S2) const i2s_config_t i2s_config = { // Many of these settings are defined in (constants.h) .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX), .sample_rate = CONFIG.SAMPLE_RATE, .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT, .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT, .communication_format = (i2s_comm_format_t) (I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB), - .dma_buf_count = 1024 / (CONFIG.SAMPLES_PER_CHUNK * 2), - .dma_buf_len = CONFIG.SAMPLES_PER_CHUNK * 2 -}; -#elif defined(CONFIG_IDF_TARGET_ESP32S3) -const i2s_config_t i2s_config = { // Many of these settings are defined in (constants.h) - .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX), - .sample_rate = CONFIG.SAMPLE_RATE, - .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT, - .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, - .communication_format = (i2s_comm_format_t) (I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB), - .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, - .dma_buf_count = 1024 / CONFIG.SAMPLES_PER_CHUNK, + .dma_buf_count = 2, .dma_buf_len = CONFIG.SAMPLES_PER_CHUNK }; -#endif const i2s_pin_config_t pin_config = { // These too .bck_io_num = I2S_BCLK_PIN, @@ -145,7 +132,7 @@ void acquire_sample_chunk(uint32_t t_now) { // +1 is the right. See run_sweet_spot() in led_utilities.h // for how this value translates to the final LED brightnesses - if (max_waveform_val_raw <= CONFIG.SWEET_SPOT_MIN_LEVEL * 0.95) { + if (max_waveform_val_raw <= CONFIG.SWEET_SPOT_MIN_LEVEL * 1.10) { sweet_spot_state = -1; if (sweet_spot_state_last != -1) { // Just became silent silence_temp = true; @@ -194,6 +181,71 @@ void acquire_sample_chunk(uint32_t t_now) { sample_window[i] = waveform[i - (SAMPLE_HISTORY_LENGTH - CONFIG.SAMPLES_PER_CHUNK)]; } + for(uint16_t i = 0; i < CONFIG.SAMPLES_PER_CHUNK; i++){ + waveform_fixed_point[i] = SQ15x16(waveform[i]) / SQ15x16(32768.0); + } + sweet_spot_state_last = sweet_spot_state; } } + +void calculate_vu() { + /* + Calculates perceived audio loudness or Volume Unit (VU). Uses root mean square (RMS) method + for accurate representation of perceived loudness and incorporates a noise floor calibration. + If calibration is active, updates noise floor level. If not, subtracts the noise floor from + the calculated volume and normalizes the volume level. + + Parameters: + - audio_samples[]: Audio samples to process. + - sample_count: Number of samples in audio_samples array. + + Global variables: + - audio_vu_level: Current VU level. + - audio_vu_level_last: Last calculated VU level. + - CONFIG.VU_LEVEL_FLOOR: Quietest level considered as audio signal. + - audio_vu_level_average: Average of the current and the last VU level. + - noise_cal_active: Indicator of active noise floor calibration. + */ + + SQ15x16 sample_count = CONFIG.SAMPLES_PER_CHUNK; + + // Store last volume level + audio_vu_level_last = audio_vu_level; + + SQ15x16 sum = 0.0; + + // Compute sum of squares + for(uint16_t i = 0; i < sample_count; i++) { + sum += waveform_fixed_point[i] * waveform_fixed_point[i]; + } + + // Compute RMS + SQ15x16 rms = sqrt(float(sum / sample_count)); + + // Update volume level + audio_vu_level = rms; + + // Check noise calibration + if(noise_complete == false){ + // If volume level exceeds noise floor, update noise floor + if(float(audio_vu_level*1.5) > CONFIG.VU_LEVEL_FLOOR){ + CONFIG.VU_LEVEL_FLOOR = float(audio_vu_level*1.5); + } + } + else{ + // Subtract noise floor from volume level + audio_vu_level -= CONFIG.VU_LEVEL_FLOOR; + + // Zero out negative volume + if(audio_vu_level < 0.0){ + audio_vu_level = 0.0; + } + + // Normalize volume level + audio_vu_level /= (1.0-CONFIG.VU_LEVEL_FLOOR); + } + + // Compute average volume level + audio_vu_level_average = (audio_vu_level+audio_vu_level_last) / SQ15x16(2.0); +} diff --git a/SENSORY_BRIDGE_FIRMWARE/knobs.h b/SENSORY_BRIDGE_FIRMWARE/knobs.h index 3c91652..ba28d12 100644 --- a/SENSORY_BRIDGE_FIRMWARE/knobs.h +++ b/SENSORY_BRIDGE_FIRMWARE/knobs.h @@ -2,17 +2,11 @@ Sensory Bridge KNOB FUNCTIONS ----------------------------------------*/ -uint16_t avg_read(uint8_t pin){ +uint16_t avg_read(uint8_t pin) { uint32_t sum = 0; sum += analogRead(pin); sum += analogRead(pin); - sum += analogRead(pin); - sum += analogRead(pin); - sum += analogRead(pin); - sum += analogRead(pin); - sum += analogRead(pin); - sum += analogRead(pin); - return sum >> 3; + return sum >> 1; } void check_knobs(uint32_t t_now) { @@ -24,7 +18,7 @@ void check_knobs(uint32_t t_now) { // not set to the MAIN unit, it will flash all units in the // network (p2p.h) to identify which unit's knobs to modify // instead to affect changes. - + static uint32_t iter = 0; static float PHOTONS_TARGET = 1.0; static float CHROMA_TARGET = 1.0; @@ -40,10 +34,10 @@ void check_knobs(uint32_t t_now) { iter++; - if (iter % 10 == 0) { // If frame count is multiple of 10 + if (iter % 1 == 0) { // If frame count is multiple of 2 PHOTONS_TARGET = (1.0 - (avg_read(PHOTONS_PIN) / 8192.0)); - CHROMA_TARGET = (1.0 - (avg_read(CHROMA_PIN) / 8192.0)); - MOOD_TARGET = (1.0 - (avg_read(MOOD_PIN) / 8192.0)); + CHROMA_TARGET = (1.0 - (avg_read(CHROMA_PIN) / 8192.0)); + MOOD_TARGET = (1.0 - (avg_read(MOOD_PIN) / 8192.0)); } // Happens every frame: @@ -71,12 +65,11 @@ void check_knobs(uint32_t t_now) { MOOD_OUTPUT -= (delta * 0.1); } - if (CONFIG.IS_MAIN_UNIT || main_override) { // If we're MAIN unit, show changed values + if (CONFIG.IS_MAIN_UNIT || main_override) { // If we're MAIN unit, show changed values CONFIG.PHOTONS = PHOTONS_OUTPUT; CONFIG.CHROMA = CHROMA_OUTPUT; CONFIG.MOOD = MOOD_OUTPUT; - } - else { // If NOT MAIN, ignored changed values, flash all units to identify MAIN unit + } else { // If NOT MAIN, ignored changed values, flash all units to identify MAIN unit if (fabs(PHOTONS_TARGET - PHOTONS_TARGET_LAST) >= 0.05) { PHOTONS_TARGET_LAST = PHOTONS_TARGET; if (CONFIG.IS_MAIN_UNIT == false && main_override == false) { @@ -99,11 +92,10 @@ void check_knobs(uint32_t t_now) { // CHROMA Knob handling chroma_val = 1.0; - if(CONFIG.CHROMA < 0.95){ - chroma_val = CONFIG.CHROMA*1.05263157; // Reciprocal of 0.95 above + if (CONFIG.CHROMA < 0.95) { + chroma_val = CONFIG.CHROMA * 1.05263157; // Reciprocal of 0.95 above chromatic_mode = false; - } - else{ + } else { chromatic_mode = true; } @@ -122,11 +114,49 @@ void check_knobs(uint32_t t_now) { } smoothing_bottom_half *= -1.0; smoothing_bottom_half = 1.0 - smoothing_bottom_half; - smoothing_bottom_half = (smoothing_bottom_half * 0.9) + 0.1; // 0.0-1.0 input range becomes 0.1-1.0 + smoothing_bottom_half = (smoothing_bottom_half * 0.9) + 0.1; // 0.0-1.0 input range becomes 0.1-1.0 // Bottom half of knob now has range of 0.1 (fully left) to 1.0 (knob is centered) accesible through this variable (stays 1.0 when in top half) // Making 0.1 the bottom value prevents the LEDs from experiencing 0% change per frame! ;) // These are the final values we'll feed into the two smoothing algorithms soon - smoothing_follower = 0.100 + (smoothing_top_half * 0.300); // 0.0-1.0 input range becomes 0.1 - 0.400 - smoothing_exp_average = 1.0 - smoothing_bottom_half; // invert input -} + smoothing_follower = 0.100 + (smoothing_top_half * 0.300); // 0.0-1.0 input range becomes 0.1 - 0.400 + smoothing_exp_average = 1.0 - smoothing_bottom_half; // invert input + + // Process knob speed and update knob structs + knob_photons.value = CONFIG.PHOTONS; + knob_chroma.value = CONFIG.CHROMA; + knob_mood.value = CONFIG.MOOD; + + SQ15x16 speed_threshold = 0.005; + + knob_photons.change_rate = fabs_fixed(knob_photons.value - knob_photons.last_value); + knob_chroma.change_rate = fabs_fixed(knob_chroma.value - knob_chroma.last_value); + knob_mood.change_rate = fabs_fixed(knob_mood.value - knob_mood.last_value); + + if (knob_photons.change_rate > speed_threshold) { knob_photons.last_change = t_now; } + if (knob_chroma.change_rate > speed_threshold) { knob_chroma.last_change = t_now; } + if (knob_mood.change_rate > speed_threshold) { knob_mood.last_change = t_now; } + + uint16_t knob_timeout_ms = 1500; + + uint32_t most_recent_time = 0; + uint8_t most_recent_knob = K_NONE; + if ((t_now - knob_photons.last_change) <= knob_timeout_ms && knob_photons.last_change > most_recent_time) { + most_recent_time = knob_photons.last_change; + most_recent_knob = K_PHOTONS; + } + if ((t_now - knob_chroma.last_change) <= knob_timeout_ms && knob_chroma.last_change > most_recent_time) { + most_recent_time = knob_chroma.last_change; + most_recent_knob = K_CHROMA; + } + if ((t_now - knob_mood.last_change) <= knob_timeout_ms && knob_mood.last_change > most_recent_time) { + most_recent_time = knob_mood.last_change; + most_recent_knob = K_MOOD; + } + + current_knob = most_recent_knob; + + knob_photons.last_value = knob_photons.value; + knob_chroma.last_value = knob_chroma.value; + knob_mood.last_value = knob_mood.value; +} \ No newline at end of file diff --git a/SENSORY_BRIDGE_FIRMWARE/led_utilities.h b/SENSORY_BRIDGE_FIRMWARE/led_utilities.h index 24e462d..042c2b1 100644 --- a/SENSORY_BRIDGE_FIRMWARE/led_utilities.h +++ b/SENSORY_BRIDGE_FIRMWARE/led_utilities.h @@ -9,6 +9,86 @@ enum blending_modes { NUM_BLENDING_MODES // used to know the length of this list if it changes in the future }; + +CRGB16 interpolate_hue(SQ15x16 hue) { + // Scale hue to [0, 63] + SQ15x16 hue_scaled = hue * 63.0; + + // Calculate index1, avoiding expensive floor() call by using typecast to int + int index1 = hue_scaled.getInteger(); + + // If index1 < 0, make it 0. If index1 > 63, make it 63 + index1 = (index1 < 0 ? 0 : (index1 > 63 ? 63 : index1)); + + // Calculate index2, minimizing the range to [index1, index1+1] + int index2 = index1 + 1; + + // If index2 > 63, make it 63 + index2 = (index2 > 63 ? 63 : index2); + + // Compute interpolation factor + SQ15x16 t = hue_scaled - SQ15x16(index1); + SQ15x16 t_inv = SQ15x16(1.0) - t; + + CRGB16 output; + output.r = t_inv * hue_lookup[index1][0] + t * hue_lookup[index2][0]; + output.g = t_inv * hue_lookup[index1][1] + t * hue_lookup[index2][1]; + output.b = t_inv * hue_lookup[index1][2] + t * hue_lookup[index2][2]; + + return output; +} + + +CRGB16 desaturate(CRGB16 input_color, SQ15x16 amount) { + SQ15x16 luminance = SQ15x16(0.2126) * input_color.r + SQ15x16(0.7152) * input_color.g + SQ15x16(0.0722) * input_color.b; + SQ15x16 amount_inv = SQ15x16(1.0) - amount; + + CRGB16 output; + output.r = input_color.r * amount_inv + luminance * amount; + output.g = input_color.g * amount_inv + luminance * amount; + output.b = input_color.b * amount_inv + luminance * amount; + + return output; +} + +CRGB16 hsv(SQ15x16 h, SQ15x16 s, SQ15x16 v) { + while (h > 1.0) { h -= 1.0; } + while (h < 0.0) { h += 1.0; } + + CRGB base_color = CHSV(uint8_t(h * 255.0), uint8_t(s * 255.0), 255); + + CRGB16 col = { base_color.r / 255.0, base_color.g / 255.0, base_color.b / 255.0 }; + //col = desaturate(col, SQ15x16(1.0) - s); + + col.r *= v; + col.g *= v; + col.b *= v; + + return col; +} + +void clip_led_values() { + for (uint16_t i = 0; i < 128; i++) { + if (leds_16[i].r > (SQ15x16)1.0) { + leds_16[i].r = (SQ15x16)1.0; + } else if (leds_16[i].r < (SQ15x16)0.0) { + leds_16[i].r = (SQ15x16)0.0; + } + + if (leds_16[i].g > (SQ15x16)1.0) { + leds_16[i].g = (SQ15x16)1.0; + } else if (leds_16[i].g < (SQ15x16)0.0) { + leds_16[i].g = (SQ15x16)0.0; + } + + if (leds_16[i].b > (SQ15x16)1.0) { + leds_16[i].b = (SQ15x16)1.0; + } else if (leds_16[i].b < (SQ15x16)0.0) { + leds_16[i].b = (SQ15x16)0.0; + } + } +} + void reverse_leds(CRGB arr[], uint16_t size) { uint16_t start = 0; uint16_t end = size - 1; @@ -97,27 +177,27 @@ CRGB lerp_led_NEW(float index, CRGB* led_array) { return out_col; } -void apply_frame_blending() { - uint8_t blend_val = 48; +// Returns the linear interpolation of a floating point index in a CRGB array +// index is in the range of 0.0 - 128.0 +CRGB16 lerp_led_16(SQ15x16 index, CRGB16* led_array) { + int32_t index_whole = index.getInteger(); + SQ15x16 index_fract = index - (SQ15x16)index_whole; - for (uint8_t i = 0; i < NATIVE_RESOLUTION; i++) { - //leds_frame_blending[i].r = uint16_t(leds_frame_blending[i].r * (255-blend_val)) >> 8; - fadeToBlackBy(leds_frame_blending, NATIVE_RESOLUTION, blend_val); + int32_t index_left = index_whole + 0; + int32_t index_right = index_whole + 1; - if(leds_frame_blending[i].r > leds[i].r) { leds[i].r = leds_frame_blending[i].r; } - if(leds_frame_blending[i].g > leds[i].g) { leds[i].g = leds_frame_blending[i].g; } - if(leds_frame_blending[i].b > leds[i].b) { leds[i].b = leds_frame_blending[i].b; } + SQ15x16 mix_left = SQ15x16(1.0) - index_fract; + SQ15x16 mix_right = SQ15x16(1.0) - mix_left; - leds_frame_blending[i] = leds[i]; - } -} + CRGB16 out_col; + out_col.r = led_array[index_left].r * mix_left + led_array[index_right].r * mix_right; + out_col.g = led_array[index_left].g * mix_left + led_array[index_right].g * mix_right; + out_col.b = led_array[index_left].b * mix_left + led_array[index_right].b * mix_right; -void show_leds() { - dither_step++; - if (dither_step >= 8) { - dither_step = 0; - } + return out_col; +} +void apply_brightness() { // This is only used to fade in when booting! if (millis() >= 1000 && noise_transition_queued == false && mode_transition_queued == false) { if (MASTER_BRIGHTNESS < 1.0) { @@ -128,44 +208,492 @@ void show_leds() { } } - //apply_frame_blending(); + SQ15x16 brightness = MASTER_BRIGHTNESS * (CONFIG.PHOTONS * CONFIG.PHOTONS) * silent_scale; - if (CONFIG.LED_COUNT == NATIVE_RESOLUTION) { - memcpy(leds_out, leds, sizeof(leds)); - } else { // If not native resolution, use interpolation if enabled - if (CONFIG.LED_INTERPOLATION == true) { - float index_push = float(1) / float(CONFIG.LED_COUNT); - float index = 0.0; - - for (uint16_t i = 0; i < CONFIG.LED_COUNT; i++) { // Interpolation - leds_out[i] = lerp_led_NEW(index, leds); - index += index_push; + for (uint16_t i = 0; i < 128; i++) { + leds_16[i].r *= brightness; + leds_16[i].g *= brightness; + leds_16[i].b *= brightness; + } + + clip_led_values(); +} + +void quantize_color(bool temporal_dithering) { + if (temporal_dithering) { + dither_step++; + if (dither_step >= 4) { + dither_step = 0; + } + + static uint8_t noise_origin_r = 0; // 0 + static uint8_t noise_origin_g = 0; // 2 + static uint8_t noise_origin_b = 0; // 4 + + noise_origin_r += 1; + noise_origin_g += 1; + noise_origin_b += 1; + + for (uint16_t i = 0; i < CONFIG.LED_COUNT; i += 1) { + // RED ##################################################### + SQ15x16 decimal_r = leds_scaled[i].r * SQ15x16(254); + SQ15x16 whole_r = decimal_r.getInteger(); + SQ15x16 fract_r = decimal_r - whole_r; + + if (fract_r >= dither_table[(noise_origin_r + i) % 4]) { + whole_r += SQ15x16(1); + } + + leds_out[i].r = whole_r.getInteger(); + + // GREEN ################################################### + SQ15x16 decimal_g = leds_scaled[i].g * SQ15x16(254); + SQ15x16 whole_g = decimal_g.getInteger(); + SQ15x16 fract_g = decimal_g - whole_g; + + if (fract_g >= dither_table[(noise_origin_g + i) % 4]) { + whole_g += SQ15x16(1); + } + + leds_out[i].g = whole_g.getInteger(); + + // BLUE #################################################### + SQ15x16 decimal_b = leds_scaled[i].b * SQ15x16(254); + SQ15x16 whole_b = decimal_b.getInteger(); + SQ15x16 fract_b = decimal_b - whole_b; + + if (fract_b >= dither_table[(noise_origin_b + i) % 4]) { + whole_b += SQ15x16(1); + } + + leds_out[i].b = whole_b.getInteger(); + } + } else { + for (uint16_t i = 0; i < CONFIG.LED_COUNT; i += 1) { + leds_out[i].r = uint8_t(leds_scaled[i].r * 255); + leds_out[i].g = uint8_t(leds_scaled[i].g * 255); + leds_out[i].b = uint8_t(leds_scaled[i].b * 255); + } + } +} + +void apply_incandescent_filter() { + SQ15x16 mix = CONFIG.INCANDESCENT_FILTER; + SQ15x16 inv_mix = 1.0 - mix; + + for (uint8_t i = 0; i < NATIVE_RESOLUTION; i++) { + SQ15x16 filtered_r = leds_16[i].r * incandescent_lookup.r; + SQ15x16 filtered_g = leds_16[i].g * incandescent_lookup.g; + SQ15x16 filtered_b = leds_16[i].b * incandescent_lookup.b; + + leds_16[i].r = (leds_16[i].r * inv_mix) + (filtered_r * mix); + leds_16[i].g = (leds_16[i].g * inv_mix) + (filtered_g * mix); + leds_16[i].b = (leds_16[i].b * inv_mix) + (filtered_b * mix); + + /* + SQ15x16 max_val = 0; + if (leds_16[i].r > max_val) { max_val = leds_16[i].r; } + if (leds_16[i].g > max_val) { max_val = leds_16[i].g; } + if (leds_16[i].b > max_val) { max_val = leds_16[i].b; } + + SQ15x16 leakage = (max_val >> 2) * mix; + + CRGB base_col = CRGB( + (uint16_t(leds[i].r * (255 - leakage)) >> 8), + (uint16_t(leds[i].g * (255 - leakage)) >> 8), + (uint16_t(leds[i].b * (255 - leakage)) >> 8)); + + CRGB leak_col = CRGB( + uint16_t((uint16_t(incandescent_lookup.r * (leakage)) >> 8) * max_val) >> 8, + uint16_t((uint16_t(incandescent_lookup.g * (leakage)) >> 8) * max_val) >> 8, + uint16_t((uint16_t(incandescent_lookup.b * (leakage)) >> 8) * max_val) >> 8); + + leds[i].r = base_col.r + leak_col.r; + leds[i].g = base_col.g + leak_col.g; + leds[i].b = base_col.b + leak_col.b; + */ + } +} + +void set_dot_position(uint16_t dot_index, SQ15x16 new_pos) { + dots[dot_index].last_position = dots[dot_index].position; + dots[dot_index].position = new_pos; +} + +void draw_line(CRGB16* layer, SQ15x16 x1, SQ15x16 x2, CRGB16 color, SQ15x16 alpha) { + bool lighten = true; + if (color.r == 0 && color.g == 0 && color.b == 0) { + lighten = false; + } + + x1 *= (SQ15x16)(128 - 1); + x2 *= (SQ15x16)(128 - 1); + + if (x1 > x2) { // Ensure x1 <= x2 + SQ15x16 temp = x1; + x1 = x2; + x2 = temp; + } + + SQ15x16 ix1 = floorFixed(x1); + SQ15x16 ix2 = ceilFixed(x2); + + // start pixel + if (ix1 >= 0 && ix1 < 128) { + SQ15x16 coverage = 1.0 - (x1 - ix1); + SQ15x16 mix = alpha * coverage; + + if (lighten == true) { + layer[ix1.getInteger()].r += color.r * mix; + layer[ix1.getInteger()].g += color.g * mix; + layer[ix1.getInteger()].b += color.b * mix; + } else { + layer[ix1.getInteger()].r = layer[ix1.getInteger()].r * (1.0 - mix) + color.r * mix; + layer[ix1.getInteger()].g = layer[ix1.getInteger()].g * (1.0 - mix) + color.g * mix; + layer[ix1.getInteger()].b = layer[ix1.getInteger()].b * (1.0 - mix) + color.b * mix; + } + } + + // end pixel + if (ix2 >= 0 && ix2 < 128) { + SQ15x16 coverage = x2 - floorFixed(x2); + SQ15x16 mix = alpha * coverage; + + if (lighten == true) { + layer[ix2.getInteger()].r += color.r * mix; + layer[ix2.getInteger()].g += color.g * mix; + layer[ix2.getInteger()].b += color.b * mix; + } else { + layer[ix2.getInteger()].r = layer[ix2.getInteger()].r * (1.0 - mix) + color.r * mix; + layer[ix2.getInteger()].g = layer[ix2.getInteger()].g * (1.0 - mix) + color.g * mix; + layer[ix2.getInteger()].b = layer[ix2.getInteger()].b * (1.0 - mix) + color.b * mix; + } + } + + // pixels in between + for (SQ15x16 i = ix1 + 1; i < ix2; i++) { + if (i >= 0 && i < 128) { + layer[i.getInteger()].r += color.r * alpha; + layer[i.getInteger()].g += color.g * alpha; + layer[i.getInteger()].b += color.b * alpha; + + if (lighten == true) { + layer[i.getInteger()].r += color.r * alpha; + layer[i.getInteger()].g += color.g * alpha; + layer[i.getInteger()].b += color.b * alpha; + } else { + layer[i.getInteger()].r = layer[i.getInteger()].r * (1.0 - alpha) + color.r * alpha; + layer[i.getInteger()].g = layer[i.getInteger()].g * (1.0 - alpha) + color.g * alpha; + layer[i.getInteger()].b = layer[i.getInteger()].b * (1.0 - alpha) + color.b * alpha; } - } else { // No interpolation - float index_push = float(NATIVE_RESOLUTION) / float(CONFIG.LED_COUNT); - float index = 0.0; + } + } +} + +void draw_dot(CRGB16* layer, uint16_t dot_index, CRGB16 color) { + SQ15x16 position = dots[dot_index].position; + SQ15x16 last_position = dots[dot_index].last_position; + + SQ15x16 positional_distance = fabs_fixed(position - last_position); + if (positional_distance < 1.0) { + positional_distance = 1.0; + } + + SQ15x16 net_brightness_per_pixel = 1.0 / positional_distance; + if (net_brightness_per_pixel > 1.0) { + net_brightness_per_pixel = 1.0; + } + + draw_line( + layer, + position, + last_position, + color, + net_brightness_per_pixel); +} - for (uint16_t i = 0; i < CONFIG.LED_COUNT; i++) { - leds_out[i] = leds[uint8_t(index)]; - index += index_push; +void render_photons_graph() { + // Draw graph ticks + uint8_t ticks = 5; + SQ15x16 tick_distance = (0.425 / (ticks - 1)); + SQ15x16 tick_pos = 0.025; + + CRGB16 background = { 0.0, 0.0, 0.0 }; + CRGB16 needle_color = { incandescent_lookup.r * incandescent_lookup.r * 0.9, incandescent_lookup.g * incandescent_lookup.g * 0.9, incandescent_lookup.b * incandescent_lookup.b * 0.9 }; + + //draw_line(leds_16_ui, 0.0, 0.5, background, 1.0); + + memset(leds_16_ui, 0, sizeof(CRGB16) * 128); + + for (uint8_t i = 0; i < ticks; i++) { + SQ15x16 prog = i / float(ticks); + SQ15x16 tick_brightness = 0.2 + 0.4 * prog; + tick_brightness *= tick_brightness; + tick_brightness *= tick_brightness; + CRGB16 tick_color = { 1.0 * tick_brightness, 0, 0 }; + + set_dot_position(GRAPH_DOT_1 + i, tick_pos); + draw_dot(leds_16_ui, GRAPH_DOT_1 + i, tick_color); + tick_pos += tick_distance; + } + + SQ15x16 needle_pos = 0.025 + (0.425 * CONFIG.PHOTONS); + + // Draw needle + set_dot_position(GRAPH_NEEDLE, needle_pos); + draw_dot(leds_16_ui, GRAPH_NEEDLE, needle_color); +} + +void render_chroma_graph() { + memset(leds_16_ui, 0, sizeof(CRGB16) * 128); + + SQ15x16 half_height = 128 >> 1; + SQ15x16 quarter_height = 128 >> 2; + + if (chromatic_mode == false) { + for (SQ15x16 i = 5; i < half_height - 5; i++) { + SQ15x16 prog = i / half_height; + + SQ15x16 distance_to_center = fabs_fixed(i - quarter_height); + SQ15x16 brightness; // = SQ15x16(1.0) - distance_to_center / quarter_height; + + if (distance_to_center < 3) { + brightness = 1.0; + } else if (distance_to_center < 5) { + brightness = 0.0; + } else { + brightness = 0.20; } + + leds_16_ui[i.getInteger()] = hsv((SQ15x16(chroma_val + hue_position) - 0.48) + prog, CONFIG.SATURATION, brightness * brightness); + } + } else { + SQ15x16 dot_pos = 0.025; + SQ15x16 dot_distance = (0.425 / (12 - 1)); + + static float radians = 0.0; + radians -= 0.025; + + for (uint8_t i = 0; i < 12; i++) { + SQ15x16 wave = sin(radians + (i * 0.5)) * 0.4 + 0.6; + + CRGB16 dot_color = hsv(SQ15x16(i / 12.0), CONFIG.SATURATION, wave * wave); + set_dot_position(MAX_DOTS - 1 - i, dot_pos); + draw_dot(leds_16_ui, MAX_DOTS - 1 - i, dot_color); + + dot_pos += dot_distance; } } +} + +void render_mood_graph() { + // Draw graph ticks + uint8_t ticks = 5; + SQ15x16 tick_distance = (0.425 / (ticks - 1)); + SQ15x16 tick_pos = 0.025; + + CRGB16 background = { 0.0, 0.0, 0.0 }; + CRGB16 needle_color = { incandescent_lookup.r * incandescent_lookup.r * 0.9, incandescent_lookup.g * incandescent_lookup.g * 0.9, incandescent_lookup.b * incandescent_lookup.b * 0.9 }; + + //draw_line(leds_16_ui, 0.0, 0.5, background, 1.0); + + memset(leds_16_ui, 0, sizeof(CRGB16) * 128); + + static float radians = 0.0; + radians -= 0.02; + + for (uint8_t i = 0; i < ticks; i++) { + SQ15x16 tick_brightness = 0.1; // + (0.025 * sin(radians * (1 << i))); // + (0.04 * sin(radians * ((i<<1)+1))); + SQ15x16 mix = i / float(ticks - 1); + + CRGB16 tick_color = { tick_brightness * mix, 0.05 * tick_brightness, tick_brightness * (1.0 - mix) }; + + set_dot_position(GRAPH_DOT_1 + i, tick_pos + (0.008 * sin(radians * (1 << i)))); + draw_dot(leds_16_ui, GRAPH_DOT_1 + i, tick_color); + tick_pos += tick_distance; + } + + SQ15x16 needle_pos = 0.025 + (0.425 * CONFIG.MOOD); + + // Draw needle + set_dot_position(GRAPH_NEEDLE, needle_pos); + draw_dot(leds_16_ui, GRAPH_NEEDLE, needle_color); +} + +void transition_ui_mask_to_height(SQ15x16 target_height) { + SQ15x16 distance = fabs_fixed(ui_mask_height - target_height); + if (ui_mask_height > target_height) { + ui_mask_height -= distance * 0.05; + } else if (ui_mask_height < target_height) { + ui_mask_height += distance * 0.05; + } + + if (ui_mask_height < 0.0) { + ui_mask_height = 0.0; + } else if (ui_mask_height > 1.0) { + ui_mask_height = 1.0; + } + + memset(ui_mask, 0, sizeof(SQ15x16) * 128); + for (uint8_t i = 0; i < 128 * ui_mask_height; i++) { + ui_mask[i] = SQ15x16(1.0); + } +} + +void render_noise_cal() { + // Noise cal UI + float noise_cal_progress = noise_iterations / 256.0; + + uint8_t prog_led_index = (NATIVE_RESOLUTION >> 1) * noise_cal_progress; + float max_val = 0.0; + for (uint16_t i = 0; i < NUM_FREQS; i++) { + if (noise_samples[i] > max_val) { + max_val = float(noise_samples[i]); + } + } + for (uint16_t i = 0; i < NATIVE_RESOLUTION >> 1; i++) { + if (i < prog_led_index) { + float led_level = float(noise_samples[i]) / max_val; + led_level = led_level * 0.9 + 0.1; + leds_16_ui[64 + i] = hsv(0.859, CONFIG.SATURATION, led_level * led_level); + leds_16_ui[63 - i] = leds_16_ui[64 + i]; + } else if (i == prog_led_index) { + leds_16_ui[64 + i] = hsv(0.875, 1.0, 1.0); + leds_16_ui[63 - i] = leds_16_ui[64 + i]; + + ui_mask[64 + i] = 1.0; + ui_mask[63 - i] = ui_mask[64 + i]; + + } else { + leds_16_ui[64 + i] = hsv(0, 0, 0); + leds_16_ui[63 - i] = leds_16_ui[64 + i]; + } + } + + if (noise_iterations > 192) { // fade out towards end of calibration + uint16_t iters_left = 64 - (noise_iterations - 192); + float brightness_level = iters_left / 64.0; + brightness_level *= brightness_level; + } +} + +void render_ui() { + if (noise_complete == true) { + if (current_knob == K_NONE) { + // Close UI if open + if (ui_mask_height > 0.005) { + transition_ui_mask_to_height(0.0); + } + } else { + if (current_knob == K_PHOTONS) { + render_photons_graph(); + } else if (current_knob == K_CHROMA) { + render_chroma_graph(); + } else if (current_knob == K_MOOD) { + render_mood_graph(); + } + + // Open UI if closed + transition_ui_mask_to_height(0.5); + } + } else { + render_noise_cal(); + } + + if (ui_mask_height > 0.005 || noise_complete == false) { + for (uint8_t i = 0; i < 128; i++) { + SQ15x16 mix = ui_mask[i]; + SQ15x16 mix_inv = SQ15x16(1.0) - mix; + + if (mix > 0.0) { + leds_16[i].r = leds_16[i].r * mix_inv + leds_16_ui[i].r * mix; + leds_16[i].g = leds_16[i].g * mix_inv + leds_16_ui[i].g * mix; + leds_16[i].b = leds_16[i].b * mix_inv + leds_16_ui[i].b * mix; + } + } + } +} + +void scale_to_strip() { + if (CONFIG.LED_COUNT == NATIVE_RESOLUTION) { + for (SQ15x16 i = 0; i < CONFIG.LED_COUNT; i++) { + //memcpy(leds_scaled, leds_16, sizeof(CRGB16)*NATIVE_RESOLUTION); + leds_scaled[i.getInteger()] = leds_16[i.getInteger()]; + } + } else { + for (SQ15x16 i = 0; i < CONFIG.LED_COUNT; i++) { + SQ15x16 prog = i / SQ15x16(CONFIG.LED_COUNT); + + // index is in the range of 0.0 - 128.0 + leds_scaled[i.getInteger()] = lerp_led_16(prog * SQ15x16(128.0), leds_16); + } + } +} + +void show_leds() { + apply_brightness(); + + // Tint the color image with an incandescent LUT to reduce harsh blues + if (CONFIG.INCANDESCENT_FILTER > 0.0) { + apply_incandescent_filter(); + } + + if (CONFIG.BASE_COAT == true) { + if (CONFIG.PHOTONS <= 0.05) { + base_coat_width_target = 0.0; + } else { + base_coat_width_target = 1.0; + } + + SQ15x16 transition_speed = 0.05; + if (base_coat_width < base_coat_width_target) { + base_coat_width += (base_coat_width_target - base_coat_width) * transition_speed; + } else if (base_coat_width > base_coat_width_target) { + base_coat_width -= (base_coat_width - base_coat_width_target) * transition_speed; + } + + SQ15x16 backdrop_divisor = 256.0; + + SQ15x16 bottom_value_r = 1 / backdrop_divisor; + SQ15x16 bottom_value_g = 1 / backdrop_divisor; + SQ15x16 bottom_value_b = 1 / backdrop_divisor; + + CRGB16 backdrop_color = { bottom_value_r, bottom_value_g, bottom_value_b }; + + SQ15x16 base_coat_width_scaled = base_coat_width * silent_scale; + + if (base_coat_width_scaled > 0.01) { + draw_line(leds_16, 0.5 - (base_coat_width_scaled * 0.5), 0.5 + (base_coat_width_scaled * 0.5), backdrop_color, 1.0); + } + + /* + for (uint8_t i = 0; i < 128; i++) { + if (leds_16[i].r < bottom_value_r) { leds_16[i].r = bottom_value_r; } + if (leds_16[i].g < bottom_value_g) { leds_16[i].g = bottom_value_g; } + if (leds_16[i].b < bottom_value_b) { leds_16[i].b = bottom_value_b; } + } + */ + } + + render_ui(); + clip_led_values(); + + scale_to_strip(); + + quantize_color(CONFIG.TEMPORAL_DITHERING); if (CONFIG.REVERSE_ORDER == true) { reverse_leds(leds_out, CONFIG.LED_COUNT); } - // PHOTONS knob is squared and applied here: - FastLED.setBrightness((255 * MASTER_BRIGHTNESS) * (CONFIG.PHOTONS * CONFIG.PHOTONS) * silent_scale); - FastLED.setDither(CONFIG.TEMPORAL_DITHERING); + FastLED.setDither(false); FastLED.show(); - //USBSerial.println(MASTER_BRIGHTNESS); } void init_leds() { bool leds_started = false; + leds_scaled = new CRGB16[CONFIG.LED_COUNT]; leds_out = new CRGB[CONFIG.LED_COUNT]; if (CONFIG.LED_TYPE == LED_NEOPIXEL) { @@ -190,8 +718,8 @@ void init_leds() { FastLED.setMaxPowerInVoltsAndMilliamps(5.0, CONFIG.MAX_CURRENT_MA); - for (uint8_t x = 0; x < NATIVE_RESOLUTION; x++) { - leds[x] = CRGB(0, 0, 0); + for (uint8_t x = 0; x < CONFIG.LED_COUNT; x++) { + leds_out[x] = CRGB(0, 0, 0); } show_leds(); @@ -201,125 +729,95 @@ void init_leds() { USBSerial.println(leds_started == true ? PASS : FAIL); } -void save_leds_to_fx() { - memcpy(leds_fx, leds, sizeof(CRGB) * NATIVE_RESOLUTION); -} - -void load_leds_from_fx() { - memcpy(leds, leds_fx, sizeof(CRGB) * NATIVE_RESOLUTION); -} - -void save_leds_to_aux() { - memcpy(leds_aux, leds, sizeof(CRGB) * NATIVE_RESOLUTION); -} - -void load_leds_from_aux() { - memcpy(leds, leds_aux, sizeof(CRGB) * NATIVE_RESOLUTION); -} - -void save_leds_to_last() { - memcpy(leds_last, leds, sizeof(CRGB) * NATIVE_RESOLUTION); -} - -void load_leds_from_last() { - memcpy(leds, leds_last, sizeof(CRGB) * NATIVE_RESOLUTION); -} - -void save_leds_to_temp() { - memcpy(leds_temp, leds, sizeof(CRGB) * NATIVE_RESOLUTION); -} - -void load_leds_from_temp() { - memcpy(leds, leds_temp, sizeof(CRGB) * NATIVE_RESOLUTION); -} - -void blocking_flash(CRGB col) { +void blocking_flash(CRGB16 col) { led_thread_halt = true; for (uint8_t i = 0; i < NATIVE_RESOLUTION; i++) { - leds[i] = CRGB(0, 0, 0); + leds_16[i] = { 0, 0, 0 }; } const uint8_t flash_times = 2; for (uint8_t f = 0; f < flash_times; f++) { for (uint8_t i = 0 + 48; i < NATIVE_RESOLUTION - 48; i++) { - leds[i] = col; + leds_16[i] = col; } show_leds(); FastLED.delay(150); for (uint8_t i = 0; i < NATIVE_RESOLUTION; i++) { - leds[i] = CRGB(0, 0, 0); + leds_16[i] = { 0, 0, 0 }; } show_leds(); FastLED.delay(150); } - led_thread_halt = false; } void clear_all_led_buffers() { for (uint8_t i = 0; i < NATIVE_RESOLUTION; i++) { - leds[i] = CRGB(0, 0, 0); - leds_temp[i] = CRGB(0, 0, 0); - leds_last[i] = CRGB(0, 0, 0); - leds_aux[i] = CRGB(0, 0, 0); - leds_fade[i] = CRGB(0, 0, 0); + leds_16[i] = { 0, 0, 0 }; + leds_16_temp[i] = { 0, 0, 0 }; + leds_16_fx[i] = { 0, 0, 0 }; } for (uint16_t i = 0; i < CONFIG.LED_COUNT; i++) { + leds_scaled[i] = { 0, 0, 0 }; leds_out[i] = CRGB(0, 0, 0); } } -void scale_image_to_half(CRGB* led_array) { +void scale_image_to_half(CRGB16* led_array) { for (uint16_t i = 0; i < (NATIVE_RESOLUTION >> 1); i++) { - leds_temp[i] = led_array[i << 1]; - leds_temp[64 + i] = CRGB(0, 0, 0); + leds_16_temp[i].r = led_array[i << 1].r * SQ15x16(0.5) + led_array[(i << 1) + 1].r * SQ15x16(0.5); + leds_16_temp[i].g = led_array[i << 1].g * SQ15x16(0.5) + led_array[(i << 1) + 1].g * SQ15x16(0.5); + leds_16_temp[i].b = led_array[i << 1].b * SQ15x16(0.5) + led_array[(i << 1) + 1].b * SQ15x16(0.5); + leds_16_temp[64 + i] = { 0, 0, 0 }; } - memcpy(led_array, leds_temp, sizeof(CRGB) * NATIVE_RESOLUTION); + memcpy(led_array, leds_16_temp, sizeof(CRGB16) * NATIVE_RESOLUTION); } -void scale_half_to_full(CRGB* led_array) { - float index_push = float(0.5) / float(NATIVE_RESOLUTION); - float index = 0.0; - +void unmirror() { for (uint16_t i = 0; i < NATIVE_RESOLUTION; i++) { // Interpolation - leds_temp[i] = lerp_led_NEW(index, led_array); - index += index_push; + SQ15x16 index = 64 + (i / 2.0); + + int32_t index_whole = index.getInteger(); + SQ15x16 index_fract = index - (SQ15x16)index_whole; + + int32_t index_left = index_whole + 0; + int32_t index_right = index_whole + 1; + + SQ15x16 mix_left = SQ15x16(1.0) - index_fract; + SQ15x16 mix_right = SQ15x16(1.0) - mix_left; + + CRGB16 out_col; + out_col.r = leds_16[index_left].r * mix_left + leds_16[index_right].r * mix_right; + out_col.g = leds_16[index_left].g * mix_left + leds_16[index_right].g * mix_right; + out_col.b = leds_16[index_left].b * mix_left + leds_16[index_right].b * mix_right; + + leds_16_temp[i] = out_col; } - memcpy(led_array, leds_temp, sizeof(CRGB) * NATIVE_RESOLUTION); + memcpy(leds_16, leds_16_temp, sizeof(CRGB16) * NATIVE_RESOLUTION); } -void shift_leds_up(CRGB* led_array, uint16_t offset) { - memcpy(leds_temp, led_array, sizeof(CRGB) * NATIVE_RESOLUTION); - memcpy(led_array + offset, leds_temp, (NATIVE_RESOLUTION - offset) * sizeof(CRGB)); - memset(led_array, 0, offset * sizeof(CRGB)); +void shift_leds_up(CRGB16* led_array, uint16_t offset) { + memcpy(leds_16_temp, led_array, sizeof(CRGB16) * NATIVE_RESOLUTION); + memcpy(led_array + offset, leds_16_temp, (NATIVE_RESOLUTION - offset) * sizeof(CRGB16)); + memset(led_array, 0, offset * sizeof(CRGB16)); } - void shift_leds_down(CRGB* led_array, uint16_t offset) { memcpy(led_array, led_array + offset, (NATIVE_RESOLUTION - offset) * sizeof(CRGB)); memset(led_array + (NATIVE_RESOLUTION - offset), 0, offset * sizeof(CRGB)); } -void mirror_image_upwards(CRGB* led_array) { - for (uint8_t i = 0; i < (NATIVE_RESOLUTION >> 1); i++) { - leds_temp[i] = led_array[i]; - leds_temp[(NATIVE_RESOLUTION - 1) - i] = led_array[i]; - } - - memcpy(led_array, leds_temp, sizeof(CRGB) * NATIVE_RESOLUTION); -} - -void mirror_image_downwards(CRGB* led_array) { +void mirror_image_downwards(CRGB16* led_array) { for (uint8_t i = 0; i < (NATIVE_RESOLUTION >> 1); i++) { - leds_temp[64 + i] = led_array[64 + i]; - leds_temp[63 - i] = led_array[64 + i]; + leds_16_temp[64 + i] = led_array[64 + i]; + leds_16_temp[63 - i] = led_array[64 + i]; } - memcpy(led_array, leds_temp, sizeof(CRGB) * NATIVE_RESOLUTION); + memcpy(led_array, leds_16_temp, sizeof(CRGB16) * NATIVE_RESOLUTION); } void intro_animation() { @@ -346,11 +844,10 @@ void intro_animation() { delta = 5.0; } float led_level = 1.0 - (delta / 5.0); - CRGB out_col = CHSV(255 * progress, 0, 255 * led_level); - leds[i] = out_col; + CRGB16 out_col = hsv(progress, 0, led_level); + leds_16[i] = out_col; } show_leds(); - FastLED.delay(3); } clear_all_led_buffers(); @@ -359,7 +856,7 @@ void intro_animation() { struct particle { float phase; float speed; - CRGB col; + CRGB16 col; }; particle particles[particle_count]; @@ -367,7 +864,7 @@ void intro_animation() { float prog = i / float(particle_count); particles[i].phase = 0.0; particles[i].speed = 0.002 * (i + 1); - particles[i].col = CHSV(255 * prog, 255 * CONFIG.SATURATION, 255); + particles[i].col = hsv(prog, CONFIG.SATURATION, 1.0); } MASTER_BRIGHTNESS = 1.0; float center_brightness = 0.0; @@ -395,7 +892,9 @@ void intro_animation() { ledcWrite(SWEET_SPOT_LEFT_CHANNEL, dimming * 4096); ledcWrite(SWEET_SPOT_RIGHT_CHANNEL, dimming * 4096); } + clear_all_led_buffers(); + for (uint8_t p = 0; p < particle_count; p++) { particles[p].phase += particles[p].speed; @@ -408,15 +907,17 @@ void intro_animation() { } float led_level = 1.0 - (delta / 10.0); led_level *= led_level; - CRGB out_col = particles[p].col; + CRGB16 out_col = particles[p].col; out_col.r *= led_level; out_col.g *= led_level; out_col.b *= led_level; - leds[i] += out_col; + leds_16[i].r += out_col.r; + leds_16[i].g += out_col.g; + leds_16[i].b += out_col.b; } } show_leds(); - FastLED.delay(6); + FastLED.delay(1); } MASTER_BRIGHTNESS = 0.0; ledcWrite(SWEET_SPOT_LEFT_CHANNEL, 0); @@ -457,6 +958,7 @@ void run_transition_fade() { } } +/* void distort_exponential() { for (uint8_t i = 0; i < NATIVE_RESOLUTION; i++) { float prog = i / float(NATIVE_RESOLUTION - 1); @@ -496,36 +998,7 @@ void fade_top_half(bool shifted = false) { leds[(NATIVE_RESOLUTION - 1 - i) + shift].b *= fade; } } - -// Draws anti-aliased 1-D lines using Xiaolin Wu's algorithm onto a CRGB array -// Start and end positions are floating point for subpixel positioning -void draw_line(CRGB* led_array, CRGB line_color, float start_pos, float end_pos) { - // Get the start and end positions as integers - int16_t x0 = max((int)start_pos, 0); - int16_t x1 = min((int)end_pos, NATIVE_RESOLUTION - 1); - //float gradient = (float)(line_color.r - line_color.b) / (x1 - x0); - - // Determine whether the line is going left or right - if (x0 > x1) { - // If the line is going left, swap the start and end positions - int16_t temp = x0; - x0 = x1; - x1 = temp; - } - - // Iterate over the pixels of the line - for (uint16_t x = x0; x <= x1; x++) { - float t = (x - x0) / (float)(x1 - x0); - //int16_t y = (int16_t)(line_color.b + gradient * (x - x0)); - // determine the coverage of the current pixel - uint16_t coverage = (uint16_t)((1 - t) * 8); - - // Blend the line color with the background color - CRGB blended_color = blend(line_color, led_array[x], coverage); - led_array[x] = blended_color; - } -} - +*/ float apply_contrast_float(float value, float intensity) { float mid_point = 0.5; @@ -537,6 +1010,20 @@ float apply_contrast_float(float value, float intensity) { return contrasted_value; } +SQ15x16 apply_contrast_fixed(SQ15x16 value, SQ15x16 intensity) { + SQ15x16 mid_point = 0.5; + SQ15x16 factor = (intensity * 2.0) + 1.0; + + SQ15x16 contrasted_value = (value - mid_point) * factor + mid_point; + if (contrasted_value > SQ15x16(1.0)) { + contrasted_value = 1.0; + } else if (contrasted_value < SQ15x16(0.0)) { + contrasted_value = 0.0; + } + + return contrasted_value; +} + #include uint8_t apply_contrast(uint8_t value, uint8_t intensity) { @@ -556,42 +1043,7 @@ uint8_t apply_contrast(uint8_t value, uint8_t intensity) { return (uint8_t)contrasted_value; } -void apply_incandescent_filter() { - uint8_t mix = CONFIG.INCANDESCENT_FILTER * 255; - uint8_t inv_mix = 255 - mix; - - for (uint8_t i = 0; i < NATIVE_RESOLUTION; i++) { - uint8_t filtered_r = uint16_t(leds[i].r * incandescent_lookup.r) >> 8; - uint8_t filtered_g = uint16_t(leds[i].g * incandescent_lookup.g) >> 8; - uint8_t filtered_b = uint16_t(leds[i].b * incandescent_lookup.b) >> 8; - - leds[i].r = (uint16_t(leds[i].r * inv_mix) >> 8) + (uint16_t(filtered_r * mix) >> 8); - leds[i].g = (uint16_t(leds[i].g * inv_mix) >> 8) + (uint16_t(filtered_g * mix) >> 8); - leds[i].b = (uint16_t(leds[i].b * inv_mix) >> 8) + (uint16_t(filtered_b * mix) >> 8); - - uint8_t max_val = 0; - if (leds[i].r > max_val) { max_val = leds[i].r; } - if (leds[i].g > max_val) { max_val = leds[i].g; } - if (leds[i].b > max_val) { max_val = leds[i].b; } - - uint8_t leakage = uint16_t((max_val >> 2) * mix) >> 8; - - CRGB base_col = CRGB( - (uint16_t(leds[i].r * (255 - leakage)) >> 8), - (uint16_t(leds[i].g * (255 - leakage)) >> 8), - (uint16_t(leds[i].b * (255 - leakage)) >> 8)); - - CRGB leak_col = CRGB( - uint16_t((uint16_t(incandescent_lookup.r * (leakage)) >> 8) * max_val) >> 8, - uint16_t((uint16_t(incandescent_lookup.g * (leakage)) >> 8) * max_val) >> 8, - uint16_t((uint16_t(incandescent_lookup.b * (leakage)) >> 8) * max_val) >> 8); - - leds[i].r = base_col.r + leak_col.r; - leds[i].g = base_col.g + leak_col.g; - leds[i].b = base_col.b + leak_col.b; - } -} - +/* void force_incandescent_output() { for (uint8_t i = 0; i < NATIVE_RESOLUTION; i++) { uint8_t max_val = 0; @@ -605,17 +1057,24 @@ void force_incandescent_output() { uint16_t(incandescent_lookup.b * max_val) >> 8); } } +*/ void render_bulb_cover() { - const uint8_t cover[4] = { 64, 255, 64, 0 }; + SQ15x16 cover[4] = { 0.25, 1.00, 0.25, 0.00 }; for (uint8_t i = 0; i < NATIVE_RESOLUTION; i++) { - CRGB covered_color = CRGB( - scale8(leds[i].r, cover[i % 4]), - scale8(leds[i].g, cover[i % 4]), - scale8(leds[i].b, cover[i % 4])); - - leds[i] = blend(leds[i], covered_color, 255 * CONFIG.BULB_OPACITY); + CRGB16 covered_color = { + leds_16[i].r * cover[i % 4], + leds_16[i].g * cover[i % 4], + leds_16[i].b * cover[i % 4], + }; + + SQ15x16 bulb_opacity = CONFIG.BULB_OPACITY; + SQ15x16 bulb_opacity_inv = 1.0 - bulb_opacity; + + leds_16[i].r = leds_16[i].r * bulb_opacity_inv + covered_color.r * bulb_opacity; + leds_16[i].g = leds_16[i].g * bulb_opacity_inv + covered_color.g * bulb_opacity; + leds_16[i].b = leds_16[i].b * bulb_opacity_inv + covered_color.b * bulb_opacity; } } @@ -627,34 +1086,325 @@ CRGB force_saturation(CRGB input, uint8_t saturation) { return CRGB(out_col_hsv_sat); } -void blend_buffers(CRGB* output_array, CRGB* input_a, CRGB* input_b, uint8_t blend_mode, uint8_t mix) { +CRGB force_hue(CRGB input, uint8_t hue) { + CHSV out_col_hsv = rgb2hsv_approximate(input); + CHSV out_col_hsv_hue = out_col_hsv; + out_col_hsv_hue.setHSV(hue, out_col_hsv.s, out_col_hsv.v); + + return CRGB(out_col_hsv_hue); +} + +void blend_buffers(CRGB16* output_array, CRGB16* input_a, CRGB16* input_b, uint8_t blend_mode, SQ15x16 mix) { if (blend_mode == BLEND_MIX) { for (uint8_t i = 0; i < NATIVE_RESOLUTION; i++) { - output_array[i] = input_a[i].scale8(255 - mix); - input_b[i].scale8(mix); + output_array[i].r = input_a[i].r * (1.0 - mix) + input_b[i].r * (mix); + output_array[i].g = input_a[i].g * (1.0 - mix) + input_b[i].g * (mix); + output_array[i].b = input_a[i].b * (1.0 - mix) + input_b[i].b * (mix); } } else if (blend_mode == BLEND_ADD) { for (uint8_t i = 0; i < NATIVE_RESOLUTION; i++) { - output_array[i] = input_a[i] + input_b[i].scale8(mix); + output_array[i].r = input_a[i].r + (input_b[i].r * mix); + output_array[i].g = input_a[i].g + (input_b[i].g * mix); + output_array[i].b = input_a[i].b + (input_b[i].b * mix); } } else if (blend_mode == BLEND_MULTIPLY) { for (uint8_t i = 0; i < NATIVE_RESOLUTION; i++) { - output_array[i].r = uint16_t(input_a[i].r * input_b[i].r) >> 8; - output_array[i].g = uint16_t(input_a[i].g * input_b[i].g) >> 8; - output_array[i].b = uint16_t(input_a[i].b * input_b[i].b) >> 8; + output_array[i].r = input_a[i].r * input_b[i].r; + output_array[i].g = input_a[i].g * input_b[i].g; + output_array[i].b = input_a[i].b * input_b[i].b; } } } -void apply_prism_effect(uint8_t iterations, uint8_t opacity) { +void apply_prism_effect(uint8_t iterations, SQ15x16 opacity) { for (uint8_t i = 0; i < iterations; i++) { - save_leds_to_fx(); + memcpy(leds_16_fx, leds_16, sizeof(CRGB16) * NATIVE_RESOLUTION); + + scale_image_to_half(leds_16_fx); + shift_leds_up(leds_16_fx, 64); + mirror_image_downwards(leds_16_fx); + + memcpy(leds_16_fx_2, leds_16, sizeof(CRGB16) * NATIVE_RESOLUTION); + blend_buffers(leds_16, leds_16_fx_2, leds_16_fx, BLEND_ADD, opacity); + } +} + +void clear_leds() { + memset(leds_16, 0, sizeof(CRGB16) * 128); +} + +void process_color_shift() { + int16_t rounded_index = spectral_history_index - 1; + while (rounded_index < 0) { + rounded_index += SPECTRAL_HISTORY_LENGTH; + } + + SQ15x16 novelty_now = novelty_curve[rounded_index]; + + // Remove bottom 10%, stretch values to still occupy full 0.0-1.0 range + novelty_now -= SQ15x16(0.10); + if (novelty_now < 0.0) { + novelty_now = 0.0; + } + novelty_now *= SQ15x16(1.111111); // ---- 1.0 / (1.0 - 0.10) + + novelty_now = novelty_now * novelty_now * novelty_now; // Square the novelty value + //novelty_now = (novelty_now)*0.5 + (novelty_now*novelty_now)*0.5; // "half-square" it again + + if (novelty_now > 0.05) { + novelty_now = 0.05; + } + + if (novelty_now > hue_shift_speed) { + hue_shift_speed = novelty_now * SQ15x16(0.75); + } else { + hue_shift_speed *= SQ15x16(0.99); + } + + // Add and wrap + hue_position += (hue_shift_speed * hue_push_direction); + while (hue_position < 0.0) { + hue_position += 1.0; + } + while (hue_position >= 1.0) { + hue_position -= 1.0; + } + + if (fabs_fixed(hue_position - hue_destination) <= 0.01) { + hue_push_direction *= -1.0; + hue_shifting_mix_target *= -1.0; + hue_destination = random_float(); + //printf("###################################### NEW DEST: %f\n", hue_destination); + } + + SQ15x16 hue_shifting_mix_distance = fabs_fixed(hue_shifting_mix - hue_shifting_mix_target); + if (hue_shifting_mix < hue_shifting_mix_target) { + hue_shifting_mix += hue_shifting_mix_distance * 0.01; + } else if (hue_shifting_mix > hue_shifting_mix_target) { + hue_shifting_mix -= hue_shifting_mix_distance * 0.01; + } + + /* + if(chance(0.2) == true){ //0.2% of loops + hue_push_direction *= -1.0; + printf("###################################### SWITCH DIR: %f\n", hue_push_direction); + } + */ +} + +void make_smooth_chromagram() { + memset(chromagram_smooth, 0, sizeof(SQ15x16) * 12); + + for (uint8_t i = 0; i < CONFIG.CHROMAGRAM_RANGE; i++) { + SQ15x16 note_magnitude = spectrogram_smooth[i]; + + if (note_magnitude > 1.0) { + note_magnitude = 1.0; + } else if (note_magnitude < 0.0) { + note_magnitude = 0.0; + } + + uint8_t chroma_bin = i % 12; + chromagram_smooth[chroma_bin] += note_magnitude / SQ15x16(CONFIG.CHROMAGRAM_RANGE / 12.0); + } + + static SQ15x16 max_peak = 0.001; + + max_peak *= 0.999; + if (max_peak < 0.01) { + max_peak = 0.01; + } + + for (uint16_t i = 0; i < 12; i++) { + if (chromagram_smooth[i] > max_peak) { + SQ15x16 distance = chromagram_smooth[i] - max_peak; + max_peak += distance *= SQ15x16(0.05); + } + } + + SQ15x16 multiplier = 1.0 / max_peak; + + for (uint8_t i = 0; i < 12; i++) { + chromagram_smooth[i] *= multiplier; + } +} + - scale_image_to_half(leds_fx); - shift_leds_up(leds_fx, 64); - mirror_image_downwards(leds_fx); +void draw_sprite(CRGB16 dest[], CRGB16 sprite[], uint32_t dest_length, uint32_t sprite_length, float position, SQ15x16 alpha) { + int32_t position_whole = position; // Downcast to integer accuracy + float position_fract = position - position_whole; + SQ15x16 mix_right = position_fract; + SQ15x16 mix_left = 1.0 - mix_right; - save_leds_to_temp(); - blend_buffers(leds, leds_temp, leds_fx, BLEND_ADD, opacity); + for (uint16_t i = 0; i < sprite_length; i++) { + int32_t pos_left = i + position_whole; + int32_t pos_right = i + position_whole + 1; + + bool skip_left = false; + bool skip_right = false; + + if (pos_left < 0) { + pos_left = 0; + skip_left = true; + } + if (pos_left > dest_length - 1) { + pos_left = dest_length - 1; + skip_left = true; + } + + if (pos_right < 0) { + pos_right = 0; + skip_right = true; + } + if (pos_right > dest_length - 1) { + pos_right = dest_length - 1; + skip_right = true; + } + + if (skip_left == false) { + dest[pos_left].r += sprite[i].r * mix_left * alpha; + dest[pos_left].g += sprite[i].g * mix_left * alpha; + dest[pos_left].b += sprite[i].b * mix_left * alpha; + } + + if (skip_right == false) { + dest[pos_right].r += sprite[i].r * mix_right * alpha; + dest[pos_right].g += sprite[i].g * mix_right * alpha; + dest[pos_right].b += sprite[i].b * mix_right * alpha; + } } +} + +CRGB16 force_saturation_16(CRGB16 rgb, SQ15x16 saturation) { + // Convert RGB to HSV + SQ15x16 max_val = fmax_fixed(rgb.r, fmax_fixed(rgb.g, rgb.b)); + SQ15x16 min_val = fmax_fixed(rgb.r, fmax_fixed(rgb.g, rgb.b)); + SQ15x16 delta = max_val - min_val; + + SQ15x16 hue, saturation_value, value; + value = max_val; + + if (delta == 0) { + // The color is achromatic (gray) + hue = 0; + saturation_value = 0; + } else { + // Calculate hue and saturation + if (max_val == rgb.r) { + hue = (rgb.g - rgb.b) / delta; + } else if (max_val == rgb.g) { + hue = 2 + (rgb.b - rgb.r) / delta; + } else { + hue = 4 + (rgb.r - rgb.g) / delta; + } + + hue *= 60; + if (hue < 0) { + hue += 360; + } + + saturation_value = delta / max_val; + } + + // Set the saturation to the input value + saturation_value = saturation; + + // Convert back to RGB + SQ15x16 c = saturation_value * value; + SQ15x16 x = c * (1 - fabs_fixed(fmod_fixed(hue / 60, 2) - 1)); + SQ15x16 m = value - c; + + CRGB16 modified_rgb; + if (hue >= 0 && hue < 60) { + modified_rgb.r = c; + modified_rgb.g = x; + modified_rgb.b = 0; + } else if (hue >= 60 && hue < 120) { + modified_rgb.r = x; + modified_rgb.g = c; + modified_rgb.b = 0; + } else if (hue >= 120 && hue < 180) { + modified_rgb.r = 0; + modified_rgb.g = c; + modified_rgb.b = x; + } else if (hue >= 180 && hue < 240) { + modified_rgb.r = 0; + modified_rgb.g = x; + modified_rgb.b = c; + } else if (hue >= 240 && hue < 300) { + modified_rgb.r = x; + modified_rgb.g = 0; + modified_rgb.b = c; + } else { + modified_rgb.r = c; + modified_rgb.g = 0; + modified_rgb.b = x; + } + + modified_rgb.r += m; + modified_rgb.g += m; + modified_rgb.b += m; + + return modified_rgb; +} + +CRGB16 adjust_hue_and_saturation(CRGB16 color, SQ15x16 hue, SQ15x16 saturation) { + // Store the RGB values + SQ15x16 r = color.r, g = color.g, b = color.b; + + // Calculate maximum and minimum values of r, g, b + SQ15x16 max_val = fmax_fixed(r, fmax_fixed(g, b)); + + // Calculate the value of the HSV color + SQ15x16 v = max_val; + + // Use the input saturation + SQ15x16 s = saturation; + + // Prepare to convert HSV back to RGB + SQ15x16 c = v * s; // chroma + SQ15x16 h_prime = fmod_fixed(hue * SQ15x16(6.0), SQ15x16(6.0)); + SQ15x16 x = c * (SQ15x16(1.0) - fabs_fixed(fmod_fixed(h_prime, SQ15x16(2.0)) - SQ15x16(1.0))); + + // Recalculate r, g, b based on the new hue and saturation + if (h_prime >= 0 && h_prime < 1) { + r = c; + g = x; + b = 0; + } else if (h_prime >= 1 && h_prime < 2) { + r = x; + g = c; + b = 0; + } else if (h_prime >= 2 && h_prime < 3) { + r = 0; + g = c; + b = x; + } else if (h_prime >= 3 && h_prime < 4) { + r = 0; + g = x; + b = c; + } else if (h_prime >= 4 && h_prime < 5) { + r = x; + g = 0; + b = c; + } else if (h_prime >= 5 && h_prime < 6) { + r = c; + g = 0; + b = x; + } + + // Add the calculated difference to get the final RGB values + SQ15x16 m = v - c; + r += m; + g += m; + b += m; + + // Clamp the values between 0.0 and 1.0 to account for rounding errors + r = fmax_fixed(SQ15x16(0.0), fmin_fixed(SQ15x16(1.0), r)); + g = fmax_fixed(SQ15x16(0.0), fmin_fixed(SQ15x16(1.0), g)); + b = fmax_fixed(SQ15x16(0.0), fmin_fixed(SQ15x16(1.0), b)); + + // Return the resulting color + CRGB16 result = { r, g, b }; + return result; } \ No newline at end of file diff --git a/SENSORY_BRIDGE_FIRMWARE/lightshow_modes.h b/SENSORY_BRIDGE_FIRMWARE/lightshow_modes.h index 65dad28..3e76160 100644 --- a/SENSORY_BRIDGE_FIRMWARE/lightshow_modes.h +++ b/SENSORY_BRIDGE_FIRMWARE/lightshow_modes.h @@ -1,3 +1,20 @@ +void get_smooth_spectrogram() { + static SQ15x16 spectrogram_smooth_last[64]; + + for (uint8_t bin = 0; bin < 64; bin++) { + SQ15x16 note_brightness = spectrogram[bin]; + + if (spectrogram_smooth[bin] < note_brightness) { + SQ15x16 distance = note_brightness - spectrogram_smooth[bin]; + spectrogram_smooth[bin] += distance * SQ15x16(0.75); + + } else if (spectrogram_smooth[bin] > note_brightness) { + SQ15x16 distance = spectrogram_smooth[bin] - note_brightness; + spectrogram_smooth[bin] -= distance * SQ15x16(0.75); + } + } +} + CRGB calc_chromagram_color() { CRGB sum_color = CRGB(0, 0, 0); for (uint8_t i = 0; i < 12; i++) { @@ -20,7 +37,7 @@ CRGB calc_chromagram_color() { //out_col.b = scale8(out_col.b, out_col.b); sum_color += out_col; } else { - sum_color += CHSV(255 * chroma_val + hue_shift, 255 * CONFIG.SATURATION, 255 * bright); + //sum_color += hsv(255 * float(chroma_val) + float(hue_shift), 255 * CONFIG.SATURATION, 255 * bright); } } @@ -35,54 +52,50 @@ void avg_bins(uint8_t low_bin, uint8_t high_bin) { // TBD } +void test_mode() { + static float radians = 0.00; + radians += CONFIG.MOOD; + float position = sin(radians) * 0.5 + 0.5; + set_dot_position(RESERVED_DOTS + 0, position); + clear_leds(); + draw_dot(leds_16, RESERVED_DOTS + 0, hsv(chroma_val, CONFIG.SATURATION, CONFIG.PHOTONS * CONFIG.PHOTONS)); +} + // Default mode! void light_mode_gdft() { - for (uint8_t i = 0; i < NUM_FREQS; i += 1) { // 64 freqs - float bin = note_spectrogram_smooth[i] * 1.25; + for (SQ15x16 i = 0; i < NUM_FREQS; i += 1) { // 64 freqs + SQ15x16 prog = i / (SQ15x16)NUM_FREQS; + SQ15x16 bin = spectrogram_smooth[i.getInteger()]; if (bin > 1.0) { bin = 1.0; } - for (uint8_t s = 0; s < CONFIG.SQUARE_ITER; s++) { - bin = (bin * bin); - } - - bin = apply_contrast_float(bin, 0.10); - - bin *= 1.0 - CONFIG.BACKDROP_BRIGHTNESS; - bin += CONFIG.BACKDROP_BRIGHTNESS; - - float led_brightness_raw = 254 * bin; // -1 for temporal dithering below - int16_t led_brightness = led_brightness_raw; - float fract = led_brightness_raw - led_brightness; - - if (CONFIG.TEMPORAL_DITHERING == true) { - if (fract >= dither_table[dither_step]) { - led_brightness += 1; - } + uint8_t extra_iters = 0; + if (chromatic_mode == true) { + extra_iters = 1; } - - led_brightness -= 1; - if (led_brightness < 0) { - led_brightness = 0; + for (uint8_t s = 0; s < CONFIG.SQUARE_ITER + extra_iters; s++) { + bin = (bin * bin) * SQ15x16(0.65) + (bin * SQ15x16(0.35)); } - brightness_levels[i] = led_brightness; // Can use this value later if needed + //bin = apply_contrast_fixed(bin, 0.04); - //hue_shift += 0.01; + //bin *= SQ15x16(1.0 - CONFIG.BACKDROP_BRIGHTNESS); + //bin += SQ15x16(CONFIG.BACKDROP_BRIGHTNESS); - float led_hue; + SQ15x16 led_hue; if (chromatic_mode == true) { - led_hue = 21.33333333 * i; // Makes hue completely cycle once per octave + led_hue = note_colors[i.getInteger() % 12]; // Makes hue completely cycle once per octave } else { - led_hue = 255 * chroma_val + (i >> 1) + hue_shift; // User color selection + led_hue = chroma_val + hue_position + ((sqrt(float(bin)) * SQ15x16(0.05)) + (prog * SQ15x16(0.10)) * hue_shifting_mix); } - leds[i] = CHSV(led_hue - (bin * 32), 255 * CONFIG.SATURATION, brightness_levels[i]); + leds_16[i.getInteger()] = hsv(led_hue + bin * SQ15x16(0.050), CONFIG.SATURATION, bin); } - // Interpolate LEDs from resolution of 64 to 128 - scale_half_to_full(leds); + shift_leds_up(leds_16, 64); // (led_utilities.h) Move image up one half + mirror_image_downwards(leds_16); // (led_utilities.h) Mirror downwards } +/* void light_mode_gdft_chromagram() { for (uint16_t i = 0; i < NATIVE_RESOLUTION; i++) { float prog = i / float(NATIVE_RESOLUTION); @@ -109,15 +122,17 @@ void light_mode_gdft_chromagram() { float led_hue; if (chromatic_mode == true) { - led_hue = 255 * prog; + //led_hue = 255 * prog; } else { - led_hue = 255 * chroma_val + (i >> 1) + hue_shift; + //led_hue = 255 * chroma_val + (i >> 1) + hue_shift; } - leds[i] = CHSV(led_hue + hue_shift, 255 * CONFIG.SATURATION, led_brightness); + //leds[i] = CHSV(led_hue + hue_shift, 255 * CONFIG.SATURATION, led_brightness); } } +*/ +/* void light_mode_bloom(bool fast_scroll) { static uint32_t iter = 0; const float led_share = 1.0 / 12.0; @@ -160,181 +175,325 @@ void light_mode_bloom(bool fast_scroll) { load_leds_from_aux(); } } +*/ -void light_mode_vu() { - const float led_share = 255 / float(12); - //static float sum_color_last[3] = { 0, 0, 0 }; +void light_mode_vu_dot() { + static SQ15x16 dot_pos_last = 0.0; + static SQ15x16 audio_vu_level_smooth = 0.0; + static SQ15x16 max_level = 0.01; - float smoothing = (0.025 + CONFIG.MOOD * 0.975) * 0.25; - float led_pos = waveform_peak_scaled * (NATIVE_RESOLUTION - 1); - static float led_pos_smooth = 0.0; + SQ15x16 mix_amount = mood_scale(0.10, 0.05); - led_pos_smooth = led_pos * (smoothing) + led_pos_smooth * (1.0 - smoothing); + audio_vu_level_smooth = (audio_vu_level_average * mix_amount) + (audio_vu_level_smooth * (1.0 - mix_amount)); - if (led_pos_smooth > 126) { - led_pos_smooth = 126; - } else if (led_pos_smooth < 0) { - led_pos_smooth = 0; + if (audio_vu_level_smooth * 1.1 > max_level) { + SQ15x16 distance = (audio_vu_level_smooth * 1.1) - max_level; + max_level += distance *= 0.1; + } else { + max_level *= 0.9999; + if (max_level < 0.0025) { + max_level = 0.0025; + } } + SQ15x16 multiplier = 1.0 / max_level; - uint16_t led_pos_smooth_whole = led_pos_smooth; - float fract = led_pos_smooth - led_pos_smooth_whole; - - CRGB sum_color = calc_chromagram_color(); - sum_color.maximizeBrightness(); - - for (uint8_t i = 0; i < NATIVE_RESOLUTION; i++) { - leds[i] = CRGB(0, 0, 0); - if (i < led_pos_smooth) { - leds[i] = sum_color; - } else if (i == led_pos_smooth) { - leds[i] = CRGB( - sum_color.r * fract, - sum_color.g * fract, - sum_color.b * fract); - } + SQ15x16 dot_pos = (audio_vu_level_smooth * multiplier); + + if (dot_pos > 1.0) { + dot_pos = 1.0; } + + SQ15x16 mix = mood_scale(0.25, 0.24); + SQ15x16 dot_pos_smooth = (dot_pos * mix) + (dot_pos_last * (1.0-mix)); + dot_pos_last = dot_pos_smooth; + + SQ15x16 brightness = sqrt(float(dot_pos_smooth)); + + set_dot_position(RESERVED_DOTS + 0, dot_pos_smooth * 0.5 + 0.5); + set_dot_position(RESERVED_DOTS + 1, 0.5 - dot_pos_smooth * 0.5); + + clear_leds(); + //fade_grayscale(0.15); + + SQ15x16 hue = chroma_val + hue_position; + CRGB16 color = hsv(hue, CONFIG.SATURATION, brightness); + draw_dot(leds_16, RESERVED_DOTS + 0, color); + draw_dot(leds_16, RESERVED_DOTS + 1, color); } -void light_mode_vu_dot() { - const float led_share = 255 / float(12); - static float sum_color_last[3] = { 0, 0, 0 }; - static float led_pos_last = 0; +void light_mode_kaleidoscope() { + static float pos_r = 0.0; + static float pos_g = 0.0; + static float pos_b = 0.0; - float smoothing = (0.025 + CONFIG.MOOD * 0.975) * 0.25; - float led_pos = waveform_peak_scaled * (NATIVE_RESOLUTION - 1); - static float led_pos_smooth = 0.0; + static SQ15x16 brightness_low = 0.0; + static SQ15x16 brightness_mid = 0.0; + static SQ15x16 brightness_high = 0.0; - led_pos_smooth = led_pos * (smoothing) + led_pos_smooth * (1.0 - smoothing); - if (led_pos_smooth > NATIVE_RESOLUTION - 2) { - led_pos_smooth = NATIVE_RESOLUTION - 2; - } else if (led_pos_smooth < 0) { - led_pos_smooth = 0; - } - CRGB sum_color = calc_chromagram_color(); - //if (sum_color.r < 5) { sum_color.r = 5; } - //if (sum_color.g < 5) { sum_color.g = 5; } - //if (sum_color.b < 5) { sum_color.b = 5; } - fadeToBlackBy(leds, NATIVE_RESOLUTION, 255); + SQ15x16 sum_low = 0.0; + SQ15x16 sum_mid = 0.0; + SQ15x16 sum_high = 0.0; - if (led_pos_last < led_pos_smooth) { - for (uint8_t i = led_pos_last; i <= led_pos_smooth; i++) { - leds[i] = sum_color; - leds[i + 1] = sum_color; + for (uint8_t i = 0; i < 20; i++) { + SQ15x16 bin = spectrogram_smooth[0 + i]; + bin = bin * 0.5 + (bin * bin) * 0.5; + sum_low += bin; + if (bin > brightness_low) { + SQ15x16 dist = fabs_fixed(bin - brightness_low); + brightness_low += dist * 0.1; } - } else if (led_pos_last > led_pos_smooth) { - for (uint8_t i = led_pos_smooth; i <= led_pos_last; i++) { - leds[i] = sum_color; - leds[i + 1] = sum_color; + } + for (uint8_t i = 0; i < 20; i++) { + SQ15x16 bin = spectrogram_smooth[20 + i]; + bin = bin * 0.5 + (bin * bin) * 0.5; + sum_mid += bin; + if (bin > brightness_mid) { + SQ15x16 dist = fabs_fixed(bin - brightness_mid); + brightness_mid += dist * 0.1; + } + } + for (uint8_t i = 0; i < 20; i++) { + SQ15x16 bin = spectrogram_smooth[40 + i]; + bin = bin * 0.5 + (bin * bin) * 0.5; + sum_high += bin; + if (bin > brightness_high) { + SQ15x16 dist = fabs_fixed(bin - brightness_high); + brightness_high += dist * 0.1; } - } else { - leds[uint8_t(led_pos_smooth)] = sum_color; - leds[uint8_t(led_pos_smooth) + 1] = sum_color; } - led_pos_last = led_pos_smooth; + brightness_low *= 0.99; + brightness_mid *= 0.99; + brightness_high *= 0.99; - save_leds_to_fx(); - scale_image_to_half(leds_fx); + SQ15x16 shift_speed = (SQ15x16)100 + ((SQ15x16)500 * (SQ15x16)CONFIG.MOOD); - for (uint8_t i = 0; i < NATIVE_RESOLUTION; i++) { - CRGB col = leds_fx[i]; + SQ15x16 shift_r = (shift_speed * sum_low); + SQ15x16 shift_g = (shift_speed * sum_mid); + SQ15x16 shift_b = (shift_speed * sum_high); - leds_fx[i] = CRGB( - col.g, - col.b, - col.r); - } + SQ15x16 speed_limit = (SQ15x16)2000 + (SQ15x16)2000 * (SQ15x16)CONFIG.MOOD; - save_leds_to_temp(); - blend_buffers(leds, leds_temp, leds_fx, BLEND_ADD, 32); + pos_r += (float)shift_r; + pos_g += (float)shift_g; + pos_b += (float)shift_b; - save_leds_to_fx(); - scale_half_to_full(leds_fx); - for (uint8_t i = 0; i < NATIVE_RESOLUTION; i++) { - CRGB col = leds_fx[i]; - leds_fx[i] = CRGB( - col.g, - col.b, - col.r); - } - save_leds_to_temp(); - blend_buffers(leds, leds_temp, leds_fx, BLEND_ADD, 32); -} -void light_mode_kaleidoscope() { - float kaleidoscope_mood = (CONFIG.MOOD * 0.75 + 0.25) * 0.09; + for (uint8_t i = 0; i < 64; i++) { + uint32_t y_pos_r = pos_r; + uint32_t y_pos_g = pos_g; + uint32_t y_pos_b = pos_b; - static float pos_r = 0.0; - static float pos_g = 1000.0; - static float pos_b = 10000.0; + uint32_t i_shifted = i + 18; + uint32_t i_scaled = (i_shifted * i_shifted * i_shifted); - static float punch_r_decay = 0.0; - static float punch_g_decay = 0.0; - static float punch_b_decay = 0.0; + SQ15x16 r_val = inoise16(i_scaled * 0.5 + y_pos_r) / 65536.0; + SQ15x16 g_val = inoise16(i_scaled * 1.0 + y_pos_g) / 65536.0; + SQ15x16 b_val = inoise16(i_scaled * 1.5 + y_pos_b) / 65536.0; - uint8_t sat = 255 * CONFIG.SATURATION; // Do this float math once ahead of time + if (r_val > 1.0) { r_val = 1.0; }; + if (g_val > 1.0) { g_val = 1.0; }; + if (b_val > 1.0) { b_val = 1.0; }; - // BEGIN FIXED-POINT-ONLY ZONE ---------------------------------------- - for (uint8_t i = 0; i < NATIVE_RESOLUTION; i++) { - uint32_t x_pos = i << 12; - uint32_t y_pos_r = uint32_t(pos_r); - uint32_t y_pos_g = uint32_t(pos_g); - uint32_t y_pos_b = uint32_t(pos_b); + for (uint8_t i = 0; i < CONFIG.SQUARE_ITER + 1; i++) { + r_val *= r_val; + g_val *= g_val; + b_val *= b_val; + } - uint8_t r_val = inoise16(x_pos, y_pos_r) >> 8; - uint8_t g_val = inoise16(x_pos, y_pos_g) >> 8; - uint8_t b_val = inoise16(x_pos, y_pos_b) >> 8; + r_val = apply_contrast_fixed(r_val, 0.1); + g_val = apply_contrast_fixed(g_val, 0.1); + b_val = apply_contrast_fixed(b_val, 0.1); - for (uint8_t i = 0; i < CONFIG.SQUARE_ITER + 1; i++) { - r_val = uint16_t(r_val * r_val) >> 8; - g_val = uint16_t(g_val * g_val) >> 8; - b_val = uint16_t(b_val * b_val) >> 8; + SQ15x16 prog = 1.0; + if (i < 32) { + prog = (i / 31.0); } + prog *= prog; + + r_val *= prog * brightness_low; + g_val *= prog * brightness_mid; + b_val *= prog * brightness_high; + + CRGB16 col = { r_val, g_val, b_val }; + col = desaturate(col, 0.1 + (0.9 - 0.9*CONFIG.SATURATION)); - //r_val = apply_contrast(r_val, 16); - //g_val = apply_contrast(g_val, 16); - //b_val = apply_contrast(b_val, 16); + if (chromatic_mode == false) { + SQ15x16 brightness = 0.0; + if(r_val > brightness){ brightness = r_val; } + if(g_val > brightness){ brightness = g_val; } + if(b_val > brightness){ brightness = b_val; } + SQ15x16 led_hue = chroma_val + hue_position + ((sqrt(float(brightness)) * SQ15x16(0.05)) + (prog * SQ15x16(0.10)) * hue_shifting_mix); + col = hsv(led_hue, CONFIG.SATURATION, brightness); + } + + leds_16[i] = { col.r, col.g, col.b }; + leds_16[NATIVE_RESOLUTION - 1 - i] = leds_16[i]; + } +} + +void light_mode_chromagram_gradient() { + for (uint8_t i = 0; i < 64; i++) { + SQ15x16 prog = i / 64.0; + SQ15x16 note_magnitude = interpolate(prog, chromagram_smooth, 12) * 0.9 + 0.1; + + for (uint8_t s = 0; s < CONFIG.SQUARE_ITER; s++) { + note_magnitude = (note_magnitude * note_magnitude) * SQ15x16(0.65) + (note_magnitude * SQ15x16(0.35)); + } + + SQ15x16 led_hue; if (chromatic_mode == true) { - leds[i] = force_saturation(CRGB(r_val, g_val, b_val), sat); + led_hue = interpolate(prog, note_colors, 12); } else { - leds[i] = force_saturation(CRGB(r_val, g_val, b_val), 0); + led_hue = chroma_val + hue_position + ((sqrt(float(note_magnitude)) * SQ15x16(0.05)) + (prog * SQ15x16(0.10)) * hue_shifting_mix); + } + + CRGB16 col = hsv(led_hue, CONFIG.SATURATION, note_magnitude * note_magnitude); + + leds_16[64 + i] = col; + leds_16[63 - i] = col; + } +} - uint8_t chroma = 255 * chroma_val; +void light_mode_chromagram_dots() { + static SQ15x16 chromagram_last[12]; - CRGB base_hue = CHSV(chroma + (i >> 1) + hue_shift, sat, 255); + memset(leds_16, 0, sizeof(CRGB16) * 128); + //dim_display(0.9); - leds[i].r = uint16_t(leds[i].r * base_hue.r) >> 8; - leds[i].g = uint16_t(leds[i].g * base_hue.g) >> 8; - leds[i].b = uint16_t(leds[i].b * base_hue.b) >> 8; + low_pass_array_fixed(chromagram_smooth, chromagram_last, 12, LED_FPS, float(mood_scale(3.5, 1.5))); + memcpy(chromagram_last, chromagram_smooth, sizeof(float) * 12); + + for (uint8_t i = 0; i < 12; i++) { + SQ15x16 led_hue; + if (chromatic_mode == true) { + led_hue = note_colors[i]; + } else { + led_hue = chroma_val + hue_position + (sqrt(float(1.0)) * SQ15x16(0.05)); } + + SQ15x16 magnitude = chromagram_smooth[i] * 1.0; + if (magnitude > 1.0) { magnitude = 1.0; } + + magnitude = magnitude * magnitude; + + CRGB16 col = hsv(led_hue, CONFIG.SATURATION, magnitude); + + set_dot_position(RESERVED_DOTS + i * 2 + 0, magnitude * 0.45 + 0.5); + set_dot_position(RESERVED_DOTS + i * 2 + 1, 0.5 - magnitude * 0.45); + + draw_dot(leds_16, RESERVED_DOTS + i * 2 + 0, col); + draw_dot(leds_16, RESERVED_DOTS + i * 2 + 1, col); } +} - // END FIXED-POINT-ONLY ZONE ------------------------------------------ +void light_mode_bloom() { + // Clear output + memset(leds_16, 0, sizeof(CRGB16) * NATIVE_RESOLUTION); - punch_r_decay *= 0.8; - punch_g_decay *= 0.8; - punch_b_decay *= 0.8; + draw_sprite(leds_16, leds_16_prev, 128, 128, 0.250 + 1.750 * CONFIG.MOOD, 0.99); - //float punch_r = avg_bins(0, 20) * kaleidoscope_mood; + //------------------------------------------------------- - float punch_r = calc_punch(0, 20) * kaleidoscope_mood; - float punch_g = calc_punch(20, 40) * kaleidoscope_mood; - float punch_b = calc_punch(40, 60) * kaleidoscope_mood; + /* + SQ15x16 brightness_low = 0.0; + SQ15x16 brightness_mid = 0.0; + SQ15x16 brightness_high = 0.0; - if (punch_r >= punch_r_decay) { punch_r_decay = punch_r; } - if (punch_g >= punch_g_decay) { punch_g_decay = punch_g; } - if (punch_b >= punch_b_decay) { punch_b_decay = punch_b; } + for (uint8_t i = 0; i < 20; i++) { + SQ15x16 bin = spectrogram_smooth[0 + i]; + bin = bin * 0.5 + (bin * bin) * 0.5; + brightness_low += bin; + } + for (uint8_t i = 0; i < 20; i++) { + SQ15x16 bin = spectrogram_smooth[20 + i]; + bin = bin * 0.5 + (bin * bin) * 0.5; + brightness_mid += bin; + } + for (uint8_t i = 0; i < 20; i++) { + SQ15x16 bin = spectrogram_smooth[40 + i]; + bin = bin * 0.5 + (bin * bin) * 0.5; + brightness_high += bin; + } - pos_r += (((0.25 * kaleidoscope_mood) + punch_r_decay)) * 16384; - pos_g += (((0.26 * kaleidoscope_mood) + punch_g_decay)) * 16384; - pos_b += (((0.27 * kaleidoscope_mood) + punch_b_decay)) * 16384; + brightness_low /= (SQ15x16)10.0; + brightness_mid /= (SQ15x16)10.0; + brightness_high /= (SQ15x16)10.0; + + if(brightness_low > 1.0){ + brightness_low = 1.0; + } + if(brightness_mid > 1.0){ + brightness_mid = 1.0; + } + if(brightness_high > 1.0){ + brightness_high = 1.0; + } + + for(uint8_t i = 0; i < CONFIG.SQUARE_ITER; i++){ + brightness_low *= brightness_low; + brightness_mid *= brightness_mid; + brightness_high *= brightness_high; + } + + CRGB16 col = { brightness_low, brightness_mid, brightness_high }; + leds_16[63] = col; + */ + + CRGB16 sum_color; + SQ15x16 share = 1 / 6.0; + for (uint8_t i = 0; i < 12; i++) { + float prog = i / 12.0; + SQ15x16 bin = chromagram_smooth[i]; + CRGB16 add_color = hsv(prog, CONFIG.SATURATION, bin*bin * share); + + sum_color.r += add_color.r; + sum_color.g += add_color.g; + sum_color.b += add_color.b; + } + + if (sum_color.r > 1.0) { sum_color.r = 1.0; }; + if (sum_color.g > 1.0) { sum_color.g = 1.0; }; + if (sum_color.b > 1.0) { sum_color.b = 1.0; }; + + for (uint8_t i = 0; i < CONFIG.SQUARE_ITER; i++) { + sum_color.r *= sum_color.r; + sum_color.g *= sum_color.g; + sum_color.b *= sum_color.b; + } + + CRGB temp_col = { uint8_t(sum_color.r * 255), uint8_t(sum_color.g * 255), uint8_t(sum_color.b * 255) }; + temp_col = force_saturation(temp_col, 255*CONFIG.SATURATION); + + if (chromatic_mode == false) { + SQ15x16 led_hue = chroma_val + hue_position + (sqrt(float(1.0)) * SQ15x16(0.05)); + temp_col = force_hue(temp_col, 255*float(led_hue)); + } + + leds_16[63] = { temp_col.r / 255.0, temp_col.g / 255.0, temp_col.b / 255.0 }; + leds_16[64] = leds_16[63]; + + //------------------------------------------------------- + + // Copy last frame to temp + memcpy(leds_16_prev, leds_16, sizeof(CRGB16) * NATIVE_RESOLUTION); + + for(uint8_t i = 0; i < 32; i++){ + float prog = i / 31.0; + leds_16[128-1-i].r *= (prog*prog); + leds_16[128-1-i].g *= (prog*prog); + leds_16[128-1-i].b *= (prog*prog); + } + + for (uint8_t i = 0; i < 64; i++) { + leds_16[i] = leds_16[128 - 1 - i]; + } } \ No newline at end of file diff --git a/SENSORY_BRIDGE_FIRMWARE/noise_cal.h b/SENSORY_BRIDGE_FIRMWARE/noise_cal.h index fe9b7e5..f4724c1 100644 --- a/SENSORY_BRIDGE_FIRMWARE/noise_cal.h +++ b/SENSORY_BRIDGE_FIRMWARE/noise_cal.h @@ -7,10 +7,14 @@ void start_noise_cal() { noise_iterations = 0; dc_offset_sum = 0; CONFIG.DC_OFFSET = 0; + CONFIG.VU_LEVEL_FLOOR = 0.0; CONFIG.SWEET_SPOT_MIN_LEVEL = 0; for (uint8_t i = 0; i < NUM_FREQS; i++) { noise_samples[i] = 0; } + for (uint8_t i = 0; i < NATIVE_RESOLUTION; i++) { + ui_mask[i] = 0; + } USBSerial.println("STARTING NOISE CAL"); } @@ -22,35 +26,4 @@ void clear_noise_cal() { save_config(); save_ambient_noise_calibration(); USBSerial.println("NOISE CAL CLEARED"); -} - -void noise_cal_led_readout(){ - float noise_cal_progress = noise_iterations / 1024.0; - uint8_t prog_led_index = NATIVE_RESOLUTION*noise_cal_progress; - float max_val = 0.0; - for (uint16_t i = 0; i < NUM_FREQS; i++) { - if(noise_samples[i] > max_val){ - max_val = noise_samples[i]; - } - } - for (uint16_t i = 0; i < NATIVE_RESOLUTION; i++) { - if(i < prog_led_index){ - float led_level = noise_samples[i>>1] / max_val; - leds[i] = CHSV(220, 255 * CONFIG.SATURATION, 255*led_level); - } - else if(i == prog_led_index){ - leds[i] = CRGB(0,255,255); - } - else{ - leds[i] = CRGB(0,0,0); - } - } - - if(noise_iterations > 768){ // fade out towards end of calibration - uint16_t iters_left = 256-(noise_iterations-768); - float brightness_level = iters_left / 256.0; - brightness_level *= brightness_level; - - MASTER_BRIGHTNESS = brightness_level; - } -} +} \ No newline at end of file diff --git a/SENSORY_BRIDGE_FIRMWARE/p2p.h b/SENSORY_BRIDGE_FIRMWARE/p2p.h index 3abacca..b07ab24 100644 --- a/SENSORY_BRIDGE_FIRMWARE/p2p.h +++ b/SENSORY_BRIDGE_FIRMWARE/p2p.h @@ -81,7 +81,8 @@ void identify_main_unit() { esp_now_send(peer_addr, (uint8_t *)&identify, sizeof(SB_COMMAND_IDENTIFY_MAIN)); } - blocking_flash(CRGB(255, 0, 0)); // We aren't main unit, flash red + CRGB16 col = {1.0, 0.0, 0.0}; + blocking_flash(col); // We aren't main unit, flash red } void propagate_noise_cal() { @@ -196,6 +197,7 @@ void run_p2p() { if (flashing_flag) { flashing_flag = false; - blocking_flash(CRGB(0, 255, 0)); + CRGB16 col = {0.0, 1.0, 0.0}; + blocking_flash(col); } } diff --git a/SENSORY_BRIDGE_FIRMWARE/presets.h b/SENSORY_BRIDGE_FIRMWARE/presets.h index 4021a4e..6ec8f86 100644 --- a/SENSORY_BRIDGE_FIRMWARE/presets.h +++ b/SENSORY_BRIDGE_FIRMWARE/presets.h @@ -5,7 +5,7 @@ void set_preset(char* preset_name) { CONFIG.SQUARE_ITER = 1; CONFIG.INCANDESCENT_FILTER = 0.80; CONFIG.INCANDESCENT_MODE = false; - CONFIG.BACKDROP_BRIGHTNESS = 0.00; + CONFIG.BASE_COAT = true; CONFIG.BULB_OPACITY = 0.0; CONFIG.SATURATION = 1.0; } @@ -14,7 +14,7 @@ void set_preset(char* preset_name) { CONFIG.SQUARE_ITER = 1; CONFIG.INCANDESCENT_FILTER = 0.80; CONFIG.INCANDESCENT_MODE = false; - CONFIG.BACKDROP_BRIGHTNESS = 0.15; + CONFIG.BASE_COAT = false; CONFIG.BULB_OPACITY = 1.0; CONFIG.SATURATION = 1.0; } @@ -23,7 +23,7 @@ void set_preset(char* preset_name) { CONFIG.SQUARE_ITER = 1; CONFIG.INCANDESCENT_FILTER = 1.0; CONFIG.INCANDESCENT_MODE = true; - CONFIG.BACKDROP_BRIGHTNESS = 0.0; + CONFIG.BASE_COAT = true; CONFIG.BULB_OPACITY = 0.0; CONFIG.SATURATION = 1.0; } @@ -32,7 +32,7 @@ void set_preset(char* preset_name) { CONFIG.SQUARE_ITER = 1; CONFIG.INCANDESCENT_FILTER = 0; CONFIG.INCANDESCENT_MODE = false; - CONFIG.BACKDROP_BRIGHTNESS = 0.15; + CONFIG.BASE_COAT = true; CONFIG.BULB_OPACITY = 0.0; CONFIG.SATURATION = 0.0; } @@ -41,7 +41,7 @@ void set_preset(char* preset_name) { CONFIG.SQUARE_ITER = 1; CONFIG.INCANDESCENT_FILTER = 0.0; CONFIG.INCANDESCENT_MODE = false; - CONFIG.BACKDROP_BRIGHTNESS = 0.00; + CONFIG.BASE_COAT = false; CONFIG.BULB_OPACITY = 0.0; CONFIG.SATURATION = 1.0; } diff --git a/SENSORY_BRIDGE_FIRMWARE/serial_menu.h b/SENSORY_BRIDGE_FIRMWARE/serial_menu.h index 98700ff..5b9911e 100644 --- a/SENSORY_BRIDGE_FIRMWARE/serial_menu.h +++ b/SENSORY_BRIDGE_FIRMWARE/serial_menu.h @@ -137,9 +137,6 @@ void dump_info() { USBSerial.print("CONFIG.SQUARE_ITER: "); USBSerial.println(CONFIG.SQUARE_ITER); - USBSerial.print("CONFIG.MAGNITUDE_FLOOR: "); - USBSerial.println(CONFIG.MAGNITUDE_FLOOR); - USBSerial.print("CONFIG.LED_TYPE: "); USBSerial.println(CONFIG.LED_TYPE); @@ -149,9 +146,6 @@ void dump_info() { USBSerial.print("CONFIG.LED_COLOR_ORDER: "); USBSerial.println(CONFIG.LED_COLOR_ORDER); - USBSerial.print("CONFIG.MAX_BLOCK_SIZE: "); - USBSerial.println(CONFIG.MAX_BLOCK_SIZE); - USBSerial.print("CONFIG.SAMPLES_PER_CHUNK: "); USBSerial.println(CONFIG.SAMPLES_PER_CHUNK); @@ -185,9 +179,6 @@ void dump_info() { USBSerial.print("CONFIG.TEMPORAL_DITHERING: "); USBSerial.println(CONFIG.TEMPORAL_DITHERING); - USBSerial.print("CONFIG.MIN_BLOCK_SIZE: "); - USBSerial.println(CONFIG.MIN_BLOCK_SIZE); - USBSerial.print("CONFIG.AUTO_COLOR_SHIFT: "); USBSerial.println(CONFIG.AUTO_COLOR_SHIFT); @@ -197,9 +188,6 @@ void dump_info() { USBSerial.print("CONFIG.INCANDESCENT_MODE: "); USBSerial.println(CONFIG.INCANDESCENT_MODE); - USBSerial.print("CONFIG.BACKDROP_BRIGHTNESS: "); - USBSerial.println(CONFIG.BACKDROP_BRIGHTNESS); - USBSerial.print("CONFIG.BULB_OPACITY: "); USBSerial.println(CONFIG.BULB_OPACITY); @@ -209,6 +197,9 @@ void dump_info() { USBSerial.print("CONFIG.PRISM_COUNT: "); USBSerial.println(CONFIG.PRISM_COUNT); + USBSerial.print("CONFIG.BASE_COAT: "); + USBSerial.println(CONFIG.BASE_COAT); + USBSerial.print("MASTER_BRIGHTNESS: "); USBSerial.println(MASTER_BRIGHTNESS); @@ -248,8 +239,8 @@ void dump_info() { USBSerial.print("last_rx_time: "); USBSerial.println(last_rx_time); - USBSerial.print("last_setting_change: "); - USBSerial.println(last_setting_change); + USBSerial.print("next_save_time: "); + USBSerial.println(next_save_time); USBSerial.print("settings_updated: "); USBSerial.println(settings_updated); @@ -314,7 +305,7 @@ void parse_command(char* command_buf) { USBSerial.println(" reverse_order=[true/false/default] | Toggle whether image is flipped upside down before final rendering"); USBSerial.println(" get_mode_name=[int] | Get a mode's name by ID (index)"); USBSerial.println(" stream=[type] | Stream live data to a Serial Plotter."); - USBSerial.println(" Options are: audio, fps, max_mags, max_mags_followers, magnitudes, spectrogram, chromagram"); + USBSerial.println(" Options are: audio, fps, magnitudes, spectrogram, chromagram"); USBSerial.println(" led_type=['neopixel'/'dotstar'] | Sets which LED protocol to use, 3 wire or 4 wire"); USBSerial.println(" led_count=[int or 'default'] | Sets how many LEDs your display will use (native resolution is 128)"); USBSerial.println(" led_color_order=[GRB/RGB/BGR/default] | Sets LED color ordering, default GRB"); @@ -323,9 +314,6 @@ void parse_command(char* command_buf) { USBSerial.println(" sample_rate=[hz or 'default'] | Sets the microphone sample rate"); USBSerial.println(" note_offset=[0-32 or 'default'] | Sets the lowest note, as a positive offset from A1 (55.0Hz)"); USBSerial.println(" square_iter=[int or 'default'] | Sets the number of times the LED output is squared (contrast)"); - USBSerial.println(" magnitude_floor=[int or 'default'] | Sets minimum magnitude a frequency bin must have to contribute the show"); - USBSerial.println(" min_block_size=[int or 'default'] | Sets the minimum number of samples used to compute frequency data"); - USBSerial.println(" max_block_size=[int or 'default'] | Sets the maximum number of samples used to compute frequency data"); USBSerial.println(" samples_per_chunk=[int or 'default'] | Sets the number of samples collected every frame"); USBSerial.println(" sensitivity=[float or 'default'] | Sets the scaling of audio data (>1.0 is more sensitive, <1.0 is less sensitive)"); USBSerial.println(" boot_animation=[true/false/default] | Enable or disable the boot animation"); @@ -341,7 +329,7 @@ void parse_command(char* command_buf) { USBSerial.println(" auto_color_shift=[true/false/default] | Toggle automated color shifting based on positive spectral changes"); USBSerial.println(" incandescent_filter=[float or 'default'] | Set the intensity of the incandescent LUT (reduces harsh blues)"); USBSerial.println(" incandescent_mode=[true/false/default] | Force all output into monochrome and tint with 2700K incandescent color"); - USBSerial.println(" backdrop_brightness=[float or 'default'] | Set the intensity of the backdrop color (approves appearance in certain modes)"); + USBSerial.println(" base_coat=[true/false/default] | Enable a dim gray backdrop to the LEDs (approves appearance in most modes)"); USBSerial.println(" bulb_opacity=[float or 'default'] | Set opacity of a filter that portrays the output as 32 \"bulbs\" with separation and hot spots"); USBSerial.println(" saturation=[float or 'default'] | Sets the saturation of internal hues"); USBSerial.println(" prism_count=[int or 'default'] | Sets the number of times the \"prism\" effect is applied"); @@ -393,7 +381,8 @@ void parse_command(char* command_buf) { else if (strcmp(command_buf, "identify") == 0) { ack(); - blocking_flash(CRGB(255, 64, 0)); + CRGB16 col = {1.00, 0.25, 0.00}; + blocking_flash(col); } @@ -661,7 +650,7 @@ void parse_command(char* command_buf) { CONFIG.SAMPLE_RATE = CONFIG_DEFAULTS.SAMPLE_RATE; } else { good = true; - CONFIG.SAMPLE_RATE = constrain(atol(command_data), 8000, 44100); + CONFIG.SAMPLE_RATE = constrain(atol(command_data), 6400, 44100); } if (good) { @@ -740,21 +729,6 @@ void parse_command(char* command_buf) { tx_end(); } - // Set Magnitude Floor ----------------------------------- - else if (strcmp(command_type, "magnitude_floor") == 0) { - if (strcmp(command_data, "default") == 0) { - CONFIG.MAGNITUDE_FLOOR = CONFIG_DEFAULTS.MAGNITUDE_FLOOR; - } else { - CONFIG.MAGNITUDE_FLOOR = constrain(atol(command_data), 0, uint32_t(-1)); - } - save_config_delayed(); - - tx_begin(); - USBSerial.print("CONFIG.MAGNITUDE_FLOOR: "); - USBSerial.println(CONFIG.MAGNITUDE_FLOOR); - tx_end(); - } - // Set LED Type --------------------------------------- else if (strcmp(command_type, "led_type") == 0) { bool good = false; @@ -821,6 +795,31 @@ void parse_command(char* command_buf) { } } + // Set Base Coat ---------------------------- + else if (strcmp(command_type, "base_coat") == 0) { + bool good = false; + if (strcmp(command_data, "default") == 0) { + CONFIG.BASE_COAT = CONFIG_DEFAULTS.BASE_COAT; + good = true; + } else if (strcmp(command_data, "true") == 0) { + CONFIG.BASE_COAT = true; + good = true; + } else if (strcmp(command_data, "false") == 0) { + CONFIG.BASE_COAT = false; + good = true; + } else { + bad_command(command_type, command_data); + } + + if (good) { + save_config_delayed(); + tx_begin(); + USBSerial.print("CONFIG.BASE_COAT: "); + USBSerial.println(CONFIG.BASE_COAT); + tx_end(); + } + } + // Set LED Temporal Dithering ---------------------------- else if (strcmp(command_type, "temporal_dithering") == 0) { bool good = false; @@ -875,38 +874,6 @@ void parse_command(char* command_buf) { } } - // Set Max Block Size ------------------------------ - else if (strcmp(command_type, "max_block_size") == 0) { - if (strcmp(command_data, "default") == 0) { - CONFIG.MAX_BLOCK_SIZE = CONFIG_DEFAULTS.MAX_BLOCK_SIZE; - } else { - CONFIG.MAX_BLOCK_SIZE = constrain(atol(command_data), 1, SAMPLE_HISTORY_LENGTH); - } - - save_config(); - tx_begin(); - USBSerial.print("CONFIG.MAX_BLOCK_SIZE: "); - USBSerial.println(CONFIG.MAX_BLOCK_SIZE); - tx_end(); - reboot(); - } - - // Set Min Block Size ------------------------------ - else if (strcmp(command_type, "min_block_size") == 0) { - if (strcmp(command_data, "default") == 0) { - CONFIG.MIN_BLOCK_SIZE = CONFIG_DEFAULTS.MIN_BLOCK_SIZE; - } else { - CONFIG.MIN_BLOCK_SIZE = constrain(atol(command_data), 1, SAMPLE_HISTORY_LENGTH); - } - - save_config(); - tx_begin(); - USBSerial.print("CONFIG.MIN_BLOCK_SIZE: "); - USBSerial.println(CONFIG.MIN_BLOCK_SIZE); - tx_end(); - reboot(); - } - // Set Samples Per Chunk --------------------------- else if (strcmp(command_type, "samples_per_chunk") == 0) { if (strcmp(command_data, "default") == 0) { @@ -1224,26 +1191,6 @@ void parse_command(char* command_buf) { } } - // Set Backdrop Brightness ---------------------------- - else if (strcmp(command_type, "backdrop_brightness") == 0) { - if (strcmp(command_data, "default") == 0) { - CONFIG.BACKDROP_BRIGHTNESS = CONFIG_DEFAULTS.BACKDROP_BRIGHTNESS; - } else { - CONFIG.BACKDROP_BRIGHTNESS = atof(command_data); - if (CONFIG.BACKDROP_BRIGHTNESS < 0.0) { - CONFIG.BACKDROP_BRIGHTNESS = 0.0; - } else if (CONFIG.BACKDROP_BRIGHTNESS > 1.0) { - CONFIG.BACKDROP_BRIGHTNESS = 1.0; - } - } - - save_config_delayed(); - tx_begin(); - USBSerial.print("CONFIG.BACKDROP_BRIGHTNESS: "); - USBSerial.println(CONFIG.BACKDROP_BRIGHTNESS); - tx_end(); - } - // Set Bulb Cover Opacity ---------------------------- else if (strcmp(command_type, "bulb_opacity") == 0) { if (strcmp(command_data, "default") == 0) { diff --git a/SENSORY_BRIDGE_FIRMWARE/system.h b/SENSORY_BRIDGE_FIRMWARE/system.h index 0c8aa0a..30e22b4 100644 --- a/SENSORY_BRIDGE_FIRMWARE/system.h +++ b/SENSORY_BRIDGE_FIRMWARE/system.h @@ -103,17 +103,17 @@ void enable_usb_update_mode() { while (true) { for (uint8_t i = 0; i < NATIVE_RESOLUTION; i++) { - leds[i] = CRGB(0, 0, 0); + leds_16[i] = {0, 0, 0}; } if (msc_update_started == false) { - leds[led_index] = CRGB(0, 0, 32); + leds_16[led_index] = {0, 0, 0.25}; ledcWrite(SWEET_SPOT_LEFT_CHANNEL, sweet_order[sweet_index][0] * 512); ledcWrite(SWEET_SPOT_CENTER_CHANNEL, sweet_order[sweet_index][1] * 512); ledcWrite(SWEET_SPOT_RIGHT_CHANNEL, sweet_order[sweet_index][2] * 512); } else { - leds[NATIVE_RESOLUTION-1-led_index] = CRGB(0, 32, 0); + leds_16[NATIVE_RESOLUTION-1-led_index] = {0, 0.25, 0}; ledcWrite(SWEET_SPOT_LEFT_CHANNEL, sweet_order[sweet_index][2] * 4095); ledcWrite(SWEET_SPOT_CENTER_CHANNEL, sweet_order[sweet_index][1] * 4095); ledcWrite(SWEET_SPOT_RIGHT_CHANNEL, sweet_order[sweet_index][0] * 4095); @@ -206,33 +206,53 @@ void generate_window_lookup() { end_timing(); } -void generate_frequency_data() { - start_timing("GENERATING GOERTZEL FREQUENCY ANALYSIS VARIABLES"); - for (uint8_t i = 0; i < NUM_FREQS; i++) { - float prog = i / float(NUM_FREQS); - - uint16_t note_index = i + CONFIG.NOTE_OFFSET; - uint16_t max_index = sizeof(notes) / sizeof(float); +void precompute_goertzel_constants() { + for (uint16_t i = 0; i < NUM_FREQS; i++) { + int16_t n = i; + frequencies[i].target_freq = notes[n + CONFIG.NOTE_OFFSET]; + + float neighbor_left; + float neighbor_right; + + if (i == 0) { + neighbor_left = notes[n + CONFIG.NOTE_OFFSET]; + neighbor_right = notes[n + CONFIG.NOTE_OFFSET + 1]; + } else if (i == NUM_FREQS - 1) { + neighbor_left = notes[n + CONFIG.NOTE_OFFSET - 1]; + neighbor_right = notes[n + CONFIG.NOTE_OFFSET]; + } else { + neighbor_left = notes[n + CONFIG.NOTE_OFFSET - 1]; + neighbor_right = notes[n + CONFIG.NOTE_OFFSET + 1]; + } - if (note_index > max_index - 1) { - note_index = max_index - 1; + float neighbor_left_distance_hz = fabs(neighbor_left - frequencies[i].target_freq); + float neighbor_right_distance_hz = fabs(neighbor_right - frequencies[i].target_freq); + float max_distance_hz = 0; + if (neighbor_left_distance_hz > max_distance_hz) { + max_distance_hz = neighbor_left_distance_hz; + } + if (neighbor_right_distance_hz > max_distance_hz) { + max_distance_hz = neighbor_right_distance_hz; } - float block_size_mix = 1.0-sqrt(sqrt(prog)); + frequencies[i].block_size = CONFIG.SAMPLE_RATE / (max_distance_hz * 2.0); - frequencies[i].target_freq = notes[note_index]; - frequencies[i].block_size = CONFIG.MAX_BLOCK_SIZE * block_size_mix + CONFIG.MIN_BLOCK_SIZE * (1.0-block_size_mix); - frequencies[i].block_size_recip = 1.0 / float(frequencies[i].block_size); + if(frequencies[i].block_size > 2000){ + frequencies[i].block_size = 2000; + } - frequencies[i].zone = (i / float(NUM_FREQS)) * NUM_ZONES; + frequencies[i].block_size_recip = 1.0 / float(frequencies[i].block_size); - float w = 2.0 * PI * ((float)frequencies[i].target_freq / CONFIG.SAMPLE_RATE); - float coeff = 2.0 * cos(w); + float k = (int)(0.5 + ((frequencies[i].block_size * frequencies[i].target_freq) / CONFIG.SAMPLE_RATE)); + float w = (2.0 * PI * k) / frequencies[i].block_size; + float cosine = cos(w); + float sine = sin(w); + float coeff = 2.0 * cosine; frequencies[i].coeff_q14 = (1 << 14) * coeff; frequencies[i].window_mult = 4096.0 / frequencies[i].block_size; + frequencies[i].zone = (i / float(NUM_FREQS)) * NUM_ZONES; } - end_timing(); } void debug_function_timing(uint32_t t_now) { @@ -296,7 +316,7 @@ void init_system() { init_p2p(); generate_a_weights(); generate_window_lookup(); - generate_frequency_data(); + precompute_goertzel_constants(); USBSerial.println("SYSTEM INIT COMPLETE!"); @@ -344,7 +364,10 @@ void log_fps(uint32_t t_now_us) { // you're rapidly cycling through modes for example. void check_settings(uint32_t t_now) { if (settings_updated) { - if (t_now - last_setting_change >= 10000) { + if (t_now >= next_save_time) { + if(debug_mode == true){ + USBSerial.println("QUEUED CONFIG SAVE TRIGGERED"); + } save_config(); settings_updated = false; } diff --git a/SENSORY_BRIDGE_FIRMWARE/utilities.h b/SENSORY_BRIDGE_FIRMWARE/utilities.h index 9cdff7f..12d69be 100644 --- a/SENSORY_BRIDGE_FIRMWARE/utilities.h +++ b/SENSORY_BRIDGE_FIRMWARE/utilities.h @@ -1,11 +1,11 @@ // Can return a value between two array indices with linear interpolation -float IRAM_ATTR interpolate(float index, float* array, uint16_t array_size) { - float index_f = index * (array_size - 1); +SQ15x16 IRAM_ATTR interpolate(SQ15x16 index, SQ15x16* array, uint16_t array_size) { + SQ15x16 index_f = index * (array_size - 1); uint16_t index_i = (uint16_t)index_f; - float index_f_frac = index_f - index_i; + SQ15x16 index_f_frac = index_f - index_i; - float left_val = array[index_i]; - float right_val = array[index_i + 1]; + SQ15x16 left_val = array[index_i]; + SQ15x16 right_val = array[index_i + 1]; if (index_i + 1 >= array_size) { right_val = left_val; @@ -48,3 +48,58 @@ void blur_array(float* input, int length, int kernel_size) { } } +float low_pass_filter(float new_data, float last_data, uint32_t sample_rate, float cutoff_freq) { + float alpha = 1.0 - expf(-2.0 * PI * cutoff_freq / sample_rate); + float output = (1.0 - alpha) * (last_data) + alpha * new_data; + return output; +} + +void low_pass_array(float* new_frame, float* last_frame, uint16_t length, uint32_t sample_rate, float cutoff_freq){ + for(uint16_t i = 0; i < length; i++){ + new_frame[i] = low_pass_filter(new_frame[i], last_frame[i], sample_rate, cutoff_freq); + } +} + +SQ15x16 low_pass_filter_fixed(SQ15x16 new_data, SQ15x16 last_data, uint32_t sample_rate, float cutoff_freq) { + SQ15x16 alpha = 1.0 - expf(-2.0 * PI * cutoff_freq / sample_rate); + SQ15x16 output = SQ15x16(1.0 - alpha) * (last_data) + alpha * new_data; + return output; +} + +void low_pass_array_fixed(SQ15x16* new_frame, SQ15x16* last_frame, uint16_t length, uint32_t sample_rate, float cutoff_freq){ + for(uint16_t i = 0; i < length; i++){ + new_frame[i] = low_pass_filter_fixed(new_frame[i], last_frame[i], sample_rate, cutoff_freq); + } +} + +float random_float(){ + return esp_random() / (float)UINT32_MAX; +} + +SQ15x16 mood_scale(SQ15x16 center, SQ15x16 range){ + SQ15x16 knob_value_bidirectional = (CONFIG.MOOD - 0.5) * SQ15x16(2.0); // 0.0 - 1.0 range transformed to -1.0 to +1.0 + SQ15x16 result = center + range*knob_value_bidirectional; + + return result; +} + +SQ15x16 fabs_fixed(SQ15x16 input){ + if(input < SQ15x16(0.0)){ + input *= SQ15x16(-1.0); + } + + return input; +} + +SQ15x16 fmin_fixed(SQ15x16 a, SQ15x16 b) { + return (a < b) ? a : b; +} + +SQ15x16 fmax_fixed(SQ15x16 a, SQ15x16 b) { + return (a > b) ? a : b; +} + +SQ15x16 fmod_fixed(SQ15x16 dividend, SQ15x16 divisor) { + SQ15x16 quotient = dividend / divisor; + return dividend - (divisor * floorFixed(quotient)); +} \ No newline at end of file