#pragma once #include "HSApplication.h" #include "OC_apps.h" #include "OC_DAC.h" #include "OC_ADC.h" #include "OC_io.h" #include "OC_strings.h" #include "util/util_stream_buffer.h" #ifndef ORN8_DEFER_INVERT #define ORN8_DEFER_INVERT // Inverts the cursor row AFTER its text is drawn (end of scope), // so the selected value shows as dark text on a light bar. struct OrnDeferInvert { bool on; int x, y, w, h; OrnDeferInvert(bool o, int x_, int y_, int w_, int h_) : on(o), x(x_), y(y_), w(w_), h(h_) {} ~OrnDeferInvert() { if (on) graphics.invertRect(x, y, w, h); } }; #endif #ifdef ARDUINO_TEENSY41 // ============================================================================ // CrazyLFO - 4-Channel Polyrhythmic, Algorithmic & Marbles-Style CV LFO // Specially engineered for Römholt Modular T4.1 / O.R.N.8 Hardware (8 Outputs) // ============================================================================ namespace crazylfo { static constexpr uint32_t ISR_RATE = 16666; static constexpr int NUM_LFOS = 4; static constexpr int NUM_FACTORY_PRESETS = 16; static constexpr int NUM_USER_PRESETS = 4; static constexpr int TOTAL_PRESETS = NUM_FACTORY_PRESETS + NUM_USER_PRESETS; static constexpr int MAX_DEJAVU_LEN = 16; enum Waveform : uint8_t { WAVE_SINE = 0, WAVE_TRIANGLE = 1, WAVE_SAW_UP = 2, WAVE_SAW_DOWN = 3, WAVE_SQUARE = 4, WAVE_STEP_RND = 5, WAVE_SMOOTH_RND = 6, WAVE_CHAOS = 7, WAVE_BOUNCE = 8, WAVE_STAIRS = 9, NUM_WAVEFORMS = 10 }; static const char *const WAVE_NAMES[NUM_WAVEFORMS] = { "Sine", "Triangle", "Saw Up", "Saw Down", "Square", "Step Rnd", "Smooth Rnd", "Chaos", "Bounce", "Staircase" }; static const char *const WAVE_SHORT[NUM_WAVEFORMS] = { "SIN", "TRI", "RAMP", "SAW", "SQU", "S&H", "RND", "CHS", "BNC", "STP" }; enum SyncMode : uint8_t { SYNC_TEMPO = 0, SYNC_FREE = 1 }; enum Polarity : uint8_t { POLARITY_BIPOLAR = 0, // -V to +V POLARITY_UNIPOLAR_POS = 1, // 0 to +V POLARITY_UNIPOLAR_NEG = 2 // 0 to -V }; enum AuxMode : uint8_t { AUX_INVERTED = 0, AUX_QUAD_90 = 1, AUX_PULSE_EOC = 2, AUX_RECTIFIED = 3 }; static const char *const AUX_NAMES[4] = { "Invert", "Quad 90", "Pulse EOC", "Rectify" }; // Clock ratios: Divisions and Multipliers static constexpr int NUM_RATIOS = 17; static const char *const RATIO_NAMES[NUM_RATIOS] = { "/32", "/24", "/16", "/12", "/8", "/6", "/4", "/3", "/2", "1:1", "x2", "x3", "x4", "x6", "x8", "x12", "x16" }; static constexpr int RATIO_NUMERATORS[NUM_RATIOS] = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 4, 6, 8, 12, 16 }; static constexpr int RATIO_DENOMINATORS[NUM_RATIOS] = { 32, 24, 16, 12, 8, 6, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1 }; // Polyrhythm Tuplets (Meter against Master Beat) static constexpr int NUM_TUPLETS = 10; static const char *const TUPLET_NAMES[NUM_TUPLETS] = { "1:1 Straight", "3:2 Triplet", "4:3 Quart", "5:4 Quint", "7:4 Sept", "5:3 Poly", "7:8 Poly", "9:8 Poly", "11:8 Poly", "13:16 Poly" }; static const char *const TUPLET_SHORT[NUM_TUPLETS] = { "1:1", "3:2", "4:3", "5:4", "7:4", "5:3", "7:8", "9:8", "11:8", "13:16" }; static constexpr int TUPLET_NUMERATORS[NUM_TUPLETS] = { 1, 3, 4, 5, 7, 5, 7, 9, 11, 13 }; static constexpr int TUPLET_DENOMINATORS[NUM_TUPLETS] = { 1, 2, 3, 4, 4, 3, 8, 8, 8, 16 }; enum ResyncPeriod : uint8_t { RESYNC_OFF = 0, RESYNC_4_BEATS = 1, RESYNC_8_BEATS = 2, RESYNC_12_BEATS = 3, RESYNC_16_BEATS = 4, RESYNC_32_BEATS = 5 }; static const char *const RESYNC_NAMES[6] = { "Off (Free)", "4 Beats (1 Bar)", "8 Beats (2 Bar)", "12 Beats", "16 Beats (4 Bar)", "32 Beats" }; struct LfoConfig { uint8_t waveform = WAVE_SINE; uint8_t sync_mode = SYNC_TEMPO; uint16_t rate_free_hz = 100; // in 0.01 Hz (100 = 1.00 Hz) int8_t clock_ratio_idx = 9; // 9 = 1:1 uint8_t tuplet_idx = 0; // 0 = 1:1 Straight uint16_t phase_offset_deg = 0; // 0..315 in 45 deg steps int8_t level_pct = 100; // 0..100% int8_t offset_pct = 0; // -100..+100% uint8_t polarity = POLARITY_BIPOLAR; uint8_t morph_pct = 50; // 0..100% (symmetry / pulse width / quantize) uint8_t dejavu_pct = 0; // 0% = Free/Off, 1..99% = Mutating Loop, 100% = Locked Loop uint8_t loop_len = 8; // 1..16 steps loop length bool reset_enabled = true; uint8_t drift_pct = 0; // 0..100% analog drift uint8_t aux_mode = AUX_INVERTED; }; struct Preset { char name[12]; uint16_t bpm = 120; uint8_t clock_source = 0; // 0: Auto, 1: Internal, 2: TR1 Ext uint8_t resync_period = RESYNC_OFF; uint8_t tr3_mode = 0; // 0: Freeze, 1: DejaVu Lock uint8_t tr4_mode = 0; // 0: Crazy Random, 1: Deja Mutate uint8_t master_dejavu = 0; // 0: Per-LFO, 1..100: Global override LfoConfig lfo[NUM_LFOS]; }; } // namespace crazylfo // ============================================================================ // App Declaration // ============================================================================ OC_APP_CLASS(AppCrazyLFO, TWOCCS("CL"), "CrazyLFO", "4-Ch Polyrhythm LFO"), public HSApplication { public: static constexpr size_t STORAGE_SIZE = 480; OC_APP_INTERFACE_DECLARE(AppCrazyLFO, STORAGE_SIZE); void Start() final; void Controller() final; void View() const final; void Resume() final { isEditing = false; } private: enum Page : uint8_t { PAGE_OVERVIEW = 0, PAGE_LFO1 = 1, PAGE_LFO2 = 2, PAGE_LFO3 = 3, PAGE_LFO4 = 4, PAGE_SYNC = 5, PAGE_PRESETS = 6, PAGE_COUNT = 7 }; // State crazylfo::Preset p; crazylfo::Preset factory_presets[crazylfo::NUM_FACTORY_PRESETS]; crazylfo::Preset user_presets[crazylfo::NUM_USER_PRESETS]; uint8_t active_preset_idx = 0; // LFO Engine runtime state uint32_t phase[crazylfo::NUM_LFOS]; uint32_t last_phase[crazylfo::NUM_LFOS]; int16_t rand_target[crazylfo::NUM_LFOS]; int16_t rand_prev[crazylfo::NUM_LFOS]; int32_t chaos_state[crazylfo::NUM_LFOS]; // Marbles Deja-Vu Ring Buffers (16 steps per LFO) int16_t dejavu_buf[crazylfo::NUM_LFOS][crazylfo::MAX_DEJAVU_LEN]; uint8_t dejavu_step[crazylfo::NUM_LFOS]; // Outputs (DAC units) int out_vals[8]; int out_meters[8]; // Clock & Sync uint32_t last_tr1_us = 0; uint32_t clock_period_us = 500000; // 120 BPM default uint32_t beat_counter = 0; bool ext_clock_detected = false; uint32_t ext_clock_timeout = 0; bool flash_tr1 = false; bool flash_tr2 = false; // UI Navigation Page current_page = PAGE_OVERVIEW; int8_t cursor = 0; bool isEditing = false; char status_msg[16] = {0}; uint32_t status_timer = 0; // Sine Lookup Table (256 entries) static int16_t sin_lut[256]; static bool lut_initialized; // Helpers void InitLut(); void InitFactoryPresets(); void InitDejaVuBuffers(); void LoadPreset(uint8_t idx); void SaveUserPreset(uint8_t user_idx); void CrazyRandomize(); void MutateDejaVuAll(); int32_t CalcWaveform(int ch, uint32_t ph_acc, uint8_t wave, uint8_t morph); int32_t CalcSin(uint16_t ph16); void ProcessClockAndSync(); // Rendering void DrawHeader() const; void DrawOverviewPage() const; void DrawLfoPage(int ch) const; void DrawSyncPage() const; void DrawPresetsPage() const; void DrawMiniWaveIcon(int x, int y, uint8_t wave) const; }; // Static LUT storage int16_t AppCrazyLFO::sin_lut[256]; bool AppCrazyLFO::lut_initialized = false; // ============================================================================ // Implementation // ============================================================================ FLASHMEM void AppCrazyLFO::InitLut() { if (lut_initialized) return; for (int i = 0; i < 256; ++i) { float rad = (i * 2.0f * 3.141592653589793f) / 256.0f; sin_lut[i] = (int16_t)(sinf(rad) * 32767.0f); } lut_initialized = true; } FLASHMEM void AppCrazyLFO::InitDejaVuBuffers() { for (int ch = 0; ch < crazylfo::NUM_LFOS; ++ch) { dejavu_step[ch] = 0; for (int s = 0; s < crazylfo::MAX_DEJAVU_LEN; ++s) { dejavu_buf[ch][s] = random(-32767, 32768); } } } FLASHMEM void AppCrazyLFO::MutateDejaVuAll() { for (int ch = 0; ch < crazylfo::NUM_LFOS; ++ch) { int s = random(0, p.lfo[ch].loop_len); dejavu_buf[ch][s] = random(-32767, 32768); } snprintf(status_msg, 16, "DejaVu Mutated!"); status_timer = millis() + 1800; } FLASHMEM void AppCrazyLFO::InitFactoryPresets() { InitLut(); // Helper lambda for clean defaults auto setDefaultLfo = [](crazylfo::LfoConfig &cfg, uint8_t wave, int ratio, uint8_t tuplet, uint8_t dejavu, uint8_t loop_len, uint8_t morph) { cfg.waveform = wave; cfg.sync_mode = crazylfo::SYNC_TEMPO; cfg.clock_ratio_idx = ratio; cfg.tuplet_idx = tuplet; cfg.phase_offset_deg = 0; cfg.level_pct = 100; cfg.offset_pct = 0; cfg.polarity = crazylfo::POLARITY_BIPOLAR; cfg.morph_pct = morph; cfg.dejavu_pct = dejavu; cfg.loop_len = loop_len; cfg.reset_enabled = true; cfg.drift_pct = 0; cfg.aux_mode = crazylfo::AUX_INVERTED; }; for (int f = 0; f < crazylfo::NUM_FACTORY_PRESETS; ++f) { factory_presets[f].bpm = 120; factory_presets[f].clock_source = 0; factory_presets[f].resync_period = crazylfo::RESYNC_OFF; factory_presets[f].tr3_mode = 0; factory_presets[f].tr4_mode = 0; factory_presets[f].master_dejavu = 0; } // F1: Straight 4 strncpy(factory_presets[0].name, "Straight 4", 12); setDefaultLfo(factory_presets[0].lfo[0], crazylfo::WAVE_SINE, 9, 0, 0, 8, 50); setDefaultLfo(factory_presets[0].lfo[1], crazylfo::WAVE_TRIANGLE, 8, 0, 0, 8, 50); setDefaultLfo(factory_presets[0].lfo[2], crazylfo::WAVE_SAW_DOWN, 7, 0, 0, 8, 50); setDefaultLfo(factory_presets[0].lfo[3], crazylfo::WAVE_SQUARE, 6, 0, 0, 8, 50); // F2: Poly 3-4-5-7 snprintf(factory_presets[1].name, 12, "Poly 3-4-5"); factory_presets[1].resync_period = crazylfo::RESYNC_16_BEATS; setDefaultLfo(factory_presets[1].lfo[0], crazylfo::WAVE_SINE, 9, 0, 0, 8, 50); setDefaultLfo(factory_presets[1].lfo[1], crazylfo::WAVE_TRIANGLE, 9, 1, 0, 8, 50); // 3:2 setDefaultLfo(factory_presets[1].lfo[2], crazylfo::WAVE_SAW_UP, 9, 3, 0, 8, 50); // 5:4 setDefaultLfo(factory_presets[1].lfo[3], crazylfo::WAVE_SMOOTH_RND, 9, 4, 80, 7, 50); // 7:4 DejaVu 80% // F3: Quad Phase strncpy(factory_presets[2].name, "Quad Phase", 12); for (int i = 0; i < crazylfo::NUM_LFOS; ++i) { setDefaultLfo(factory_presets[2].lfo[i], crazylfo::WAVE_SINE, 8, 0, 0, 8, 50); factory_presets[2].lfo[i].phase_offset_deg = i * 90; } // F4: Buchla Chaos snprintf(factory_presets[3].name, 12, "Buchla Chaos"); factory_presets[3].bpm = 110; factory_presets[3].tr3_mode = 1; // TR3 DejaVu Lock setDefaultLfo(factory_presets[3].lfo[0], crazylfo::WAVE_SMOOTH_RND, 8, 1, 85, 8, 50); setDefaultLfo(factory_presets[3].lfo[1], crazylfo::WAVE_CHAOS, 8, 3, 100, 8, 60); setDefaultLfo(factory_presets[3].lfo[2], crazylfo::WAVE_BOUNCE, 9, 0, 0, 8, 50); setDefaultLfo(factory_presets[3].lfo[3], crazylfo::WAVE_STEP_RND, 9, 4, 100, 8, 45); // Min Pent // F5: Techno Mod strncpy(factory_presets[4].name, "Techno Mod", 12); factory_presets[4].bpm = 132; setDefaultLfo(factory_presets[4].lfo[0], crazylfo::WAVE_SAW_DOWN, 9, 0, 0, 8, 50); factory_presets[4].lfo[0].polarity = crazylfo::POLARITY_UNIPOLAR_POS; setDefaultLfo(factory_presets[4].lfo[1], crazylfo::WAVE_SQUARE, 8, 0, 0, 8, 25); setDefaultLfo(factory_presets[4].lfo[2], crazylfo::WAVE_TRIANGLE, 10, 0, 0, 8, 50); setDefaultLfo(factory_presets[4].lfo[3], crazylfo::WAVE_STAIRS, 9, 1, 0, 8, 50); // F6: Slow Ambient snprintf(factory_presets[5].name, 12, "Slow Ambient"); factory_presets[5].bpm = 80; setDefaultLfo(factory_presets[5].lfo[0], crazylfo::WAVE_SINE, 2, 0, 0, 8, 50); factory_presets[5].lfo[0].drift_pct = 25; setDefaultLfo(factory_presets[5].lfo[1], crazylfo::WAVE_TRIANGLE, 3, 0, 0, 8, 50); factory_presets[5].lfo[1].drift_pct = 30; setDefaultLfo(factory_presets[5].lfo[2], crazylfo::WAVE_SMOOTH_RND, 4, 0, 85, 12, 50); setDefaultLfo(factory_presets[5].lfo[3], crazylfo::WAVE_CHAOS, 1, 0, 100, 16, 50); // F7: Marbles 8 (Classic Marbles 8-Step Locked & Repeating) strncpy(factory_presets[6].name, "Marbles 8", 12); factory_presets[6].bpm = 125; factory_presets[6].tr3_mode = 1; // TR3 DejaVu Lock factory_presets[6].tr4_mode = 1; // TR4 Deja Mutate setDefaultLfo(factory_presets[6].lfo[0], crazylfo::WAVE_STEP_RND, 9, 0, 100, 8, 25); // Chromatic Locked setDefaultLfo(factory_presets[6].lfo[1], crazylfo::WAVE_SMOOTH_RND, 9, 0, 100, 8, 50); // Fluid Marbles setDefaultLfo(factory_presets[6].lfo[2], crazylfo::WAVE_SAW_DOWN, 9, 0, 0, 8, 50); setDefaultLfo(factory_presets[6].lfo[3], crazylfo::WAVE_STEP_RND, 8, 0, 80, 8, 50); // Maj Pent Mutating // F8: Marbles Poly (Polyrhythmic Deja-Vu Repeating Chaos) snprintf(factory_presets[7].name, 12, "Marbles Poly"); factory_presets[7].bpm = 120; factory_presets[7].tr3_mode = 1; setDefaultLfo(factory_presets[7].lfo[0], crazylfo::WAVE_STEP_RND, 9, 0, 100, 8, 45); // Min Pent 8-step setDefaultLfo(factory_presets[7].lfo[1], crazylfo::WAVE_STEP_RND, 9, 3, 80, 5, 90); // 5:4 Quint 5-step Oct setDefaultLfo(factory_presets[7].lfo[2], crazylfo::WAVE_SMOOTH_RND, 9, 4, 90, 7, 50);// 7:4 Sept 7-step setDefaultLfo(factory_presets[7].lfo[3], crazylfo::WAVE_STAIRS, 9, 1, 100, 6, 50); // 3:2 Triplet // F9: Turing Shift (16-Step Turing Machine Register) snprintf(factory_presets[8].name, 12, "Turing Shift"); factory_presets[8].bpm = 120; factory_presets[8].tr3_mode = 1; factory_presets[8].tr4_mode = 1; setDefaultLfo(factory_presets[8].lfo[0], crazylfo::WAVE_STEP_RND, 9, 0, 85, 16, 25); // 16-step Chromatic setDefaultLfo(factory_presets[8].lfo[1], crazylfo::WAVE_STEP_RND, 9, 0, 85, 16, 50); factory_presets[8].lfo[1].phase_offset_deg = 90; setDefaultLfo(factory_presets[8].lfo[2], crazylfo::WAVE_SMOOTH_RND, 8, 0, 90, 16, 50); setDefaultLfo(factory_presets[8].lfo[3], crazylfo::WAVE_SQUARE, 7, 0, 0, 8, 20); // F10: Evolving Mood (Breathing Organic Deja-Vu Mutation) snprintf(factory_presets[9].name, 12, "Evolving"); factory_presets[9].bpm = 95; setDefaultLfo(factory_presets[9].lfo[0], crazylfo::WAVE_SMOOTH_RND, 8, 0, 90, 8, 50); setDefaultLfo(factory_presets[9].lfo[1], crazylfo::WAVE_SMOOTH_RND, 7, 1, 85, 12, 50); setDefaultLfo(factory_presets[9].lfo[2], crazylfo::WAVE_SINE, 8, 0, 0, 8, 50); setDefaultLfo(factory_presets[9].lfo[3], crazylfo::WAVE_SMOOTH_RND, 6, 3, 75, 6, 50); // F11: Pentatonic CV (Quantized Pitch Melodies Locked) strncpy(factory_presets[10].name, "Pentatonic", 12); factory_presets[10].bpm = 128; factory_presets[10].tr3_mode = 1; setDefaultLfo(factory_presets[10].lfo[0], crazylfo::WAVE_STEP_RND, 9, 0, 100, 8, 45); // Min Pent setDefaultLfo(factory_presets[10].lfo[1], crazylfo::WAVE_STEP_RND, 8, 0, 100, 8, 70); // Maj Pent setDefaultLfo(factory_presets[10].lfo[2], crazylfo::WAVE_TRIANGLE, 8, 0, 0, 8, 50); setDefaultLfo(factory_presets[10].lfo[3], crazylfo::WAVE_BOUNCE, 9, 0, 0, 8, 50); // F12: Acid Bouncer snprintf(factory_presets[11].name, 12, "Acid Bouncer"); factory_presets[11].bpm = 135; setDefaultLfo(factory_presets[11].lfo[0], crazylfo::WAVE_BOUNCE, 9, 0, 0, 8, 50); setDefaultLfo(factory_presets[11].lfo[1], crazylfo::WAVE_SAW_DOWN, 9, 0, 0, 8, 50); factory_presets[11].lfo[1].polarity = crazylfo::POLARITY_UNIPOLAR_POS; setDefaultLfo(factory_presets[11].lfo[2], crazylfo::WAVE_STEP_RND, 8, 0, 100, 4, 90); // 4-step Oct setDefaultLfo(factory_presets[11].lfo[3], crazylfo::WAVE_SQUARE, 9, 0, 0, 8, 15); factory_presets[11].lfo[3].aux_mode = crazylfo::AUX_PULSE_EOC; // F13: Glitch Machine (Audio Rate Multiplied Chaos) strncpy(factory_presets[12].name, "Glitch Mach", 12); factory_presets[12].bpm = 140; setDefaultLfo(factory_presets[12].lfo[0], crazylfo::WAVE_CHAOS, 11, 0, 100, 8, 80); setDefaultLfo(factory_presets[12].lfo[1], crazylfo::WAVE_STAIRS, 12, 0, 0, 8, 50); setDefaultLfo(factory_presets[12].lfo[2], crazylfo::WAVE_STEP_RND, 10, 1, 70, 4, 0); setDefaultLfo(factory_presets[12].lfo[3], crazylfo::WAVE_SQUARE, 13, 0, 0, 8, 50); // F14: Drone Drift (Free Hz Micro-Modulation) strncpy(factory_presets[13].name, "Drone Drift", 12); factory_presets[13].bpm = 60; for (int i = 0; i < crazylfo::NUM_LFOS; ++i) { factory_presets[13].lfo[i].sync_mode = crazylfo::SYNC_FREE; factory_presets[13].lfo[i].drift_pct = 35; } factory_presets[13].lfo[0].waveform = crazylfo::WAVE_SINE; factory_presets[13].lfo[0].rate_free_hz = 12; // 0.12 Hz factory_presets[13].lfo[1].waveform = crazylfo::WAVE_TRIANGLE; factory_presets[13].lfo[1].rate_free_hz = 8; // 0.08 Hz factory_presets[13].lfo[2].waveform = crazylfo::WAVE_SMOOTH_RND; factory_presets[13].lfo[2].rate_free_hz = 15; factory_presets[13].lfo[2].dejavu_pct = 85; factory_presets[13].lfo[2].loop_len = 16; factory_presets[13].lfo[3].waveform = crazylfo::WAVE_CHAOS; factory_presets[13].lfo[3].rate_free_hz = 5; // F15: Chaos Orbit (Coupled Chaotic Attractors) strncpy(factory_presets[14].name, "Chaos Orbit", 12); factory_presets[14].bpm = 115; factory_presets[14].tr3_mode = 1; setDefaultLfo(factory_presets[14].lfo[0], crazylfo::WAVE_CHAOS, 8, 0, 100, 8, 70); setDefaultLfo(factory_presets[14].lfo[1], crazylfo::WAVE_CHAOS, 8, 1, 80, 12, 60); setDefaultLfo(factory_presets[14].lfo[2], crazylfo::WAVE_SMOOTH_RND, 7, 0, 90, 8, 50); setDefaultLfo(factory_presets[14].lfo[3], crazylfo::WAVE_SINE, 6, 0, 0, 8, 50); // F16: Euclid 12 (12-Step Locked Polyrhythm) strncpy(factory_presets[15].name, "Euclid 12", 12); factory_presets[15].bpm = 120; factory_presets[15].resync_period = crazylfo::RESYNC_12_BEATS; setDefaultLfo(factory_presets[15].lfo[0], crazylfo::WAVE_STEP_RND, 9, 0, 100, 12, 45); // Min Pent 12 len setDefaultLfo(factory_presets[15].lfo[1], crazylfo::WAVE_TRIANGLE, 9, 2, 0, 8, 50); // 4:3 setDefaultLfo(factory_presets[15].lfo[2], crazylfo::WAVE_STAIRS, 9, 3, 0, 8, 50); // 5:4 setDefaultLfo(factory_presets[15].lfo[3], crazylfo::WAVE_SQUARE, 9, 6, 0, 8, 50); // 7:8 // User Presets Default Initialization for (int u = 0; u < crazylfo::NUM_USER_PRESETS; ++u) { user_presets[u] = factory_presets[u % crazylfo::NUM_FACTORY_PRESETS]; snprintf(user_presets[u].name, 12, "User %d", u + 1); } } FLASHMEM void AppCrazyLFO::LoadPreset(uint8_t idx) { if (idx < crazylfo::NUM_FACTORY_PRESETS) { p = factory_presets[idx]; } else if (idx < crazylfo::TOTAL_PRESETS) { p = user_presets[idx - crazylfo::NUM_FACTORY_PRESETS]; } active_preset_idx = idx; InitDejaVuBuffers(); snprintf(status_msg, 16, "Loaded: %.8s", p.name); status_timer = millis() + 2000; } FLASHMEM void AppCrazyLFO::SaveUserPreset(uint8_t user_idx) { if (user_idx < crazylfo::NUM_USER_PRESETS) { user_presets[user_idx] = p; snprintf(user_presets[user_idx].name, 12, "User %d", user_idx + 1); active_preset_idx = crazylfo::NUM_FACTORY_PRESETS + user_idx; OC::SaveAppData(); snprintf(status_msg, 16, "Saved User %d", user_idx + 1); status_timer = millis() + 2000; } } FLASHMEM void AppCrazyLFO::CrazyRandomize() { for (int i = 0; i < crazylfo::NUM_LFOS; ++i) { p.lfo[i].waveform = random(0, crazylfo::NUM_WAVEFORMS); p.lfo[i].sync_mode = crazylfo::SYNC_TEMPO; p.lfo[i].clock_ratio_idx = random(6, 13); // /4 to x4 p.lfo[i].tuplet_idx = random(0, crazylfo::NUM_TUPLETS); p.lfo[i].phase_offset_deg = random(0, 8) * 45; p.lfo[i].level_pct = random(50, 101); p.lfo[i].offset_pct = random(-40, 41); p.lfo[i].morph_pct = random(10, 91); p.lfo[i].dejavu_pct = random(0, 11) * 10; // 0..100% p.lfo[i].loop_len = random(2, 17); // 2..16 steps p.lfo[i].drift_pct = random(0, 35); } InitDejaVuBuffers(); snprintf(status_msg, 16, "Crazy Randomized!"); status_timer = millis() + 2000; } FLASHMEM void AppCrazyLFO::Init() { InitFactoryPresets(); InitDejaVuBuffers(); LoadPreset(0); BaseStart(); } FLASHMEM void AppCrazyLFO::Start() { for (int i = 0; i < crazylfo::NUM_LFOS; ++i) { phase[i] = 0; last_phase[i] = 0; rand_target[i] = random(-32767, 32768); rand_prev[i] = 0; chaos_state[i] = random(1000, 9000); } for (int i = 0; i < 8; ++i) { out_vals[i] = 0; out_meters[i] = 0; } clock_period_us = (60ULL * 1000000ULL) / p.bpm; } void AppCrazyLFO::Process(OC::IOFrame *ioframe) { BaseController(ioframe); } void AppCrazyLFO::Loop() {} FLASHMEM void AppCrazyLFO::HandleAppEvent(OC::AppEvent event) { if (event == OC::APP_EVENT_RESUME) Resume(); } FLASHMEM void AppCrazyLFO::GetIOConfig(OC::IOConfig &ioconfig) const { ioconfig.digital_inputs[0].set("TR1: Clock In"); ioconfig.digital_inputs[1].set("TR2: Reset In"); ioconfig.digital_inputs[2].set("TR3: Freeze/DejaVu Lock"); ioconfig.digital_inputs[3].set("TR4: Rand/Mutate"); ioconfig.cv[0].set("CV1: Master Rate"); ioconfig.cv[1].set("CV2: Level Mod"); ioconfig.cv[2].set("CV3: LFO2 Rate"); ioconfig.cv[3].set("CV4: Offset/DV Mod"); ioconfig.outputs[0].set("OUT 1: LFO 1 CV", OC::OUTPUT_MODE_PITCH); ioconfig.outputs[1].set("OUT 2: LFO 2 CV", OC::OUTPUT_MODE_PITCH); ioconfig.outputs[2].set("OUT 3: LFO 3 CV", OC::OUTPUT_MODE_PITCH); ioconfig.outputs[3].set("OUT 4: LFO 4 CV", OC::OUTPUT_MODE_PITCH); ioconfig.outputs[4].set("OUT 5: LFO 1 Aux", OC::OUTPUT_MODE_PITCH); ioconfig.outputs[5].set("OUT 6: LFO 2 Aux", OC::OUTPUT_MODE_PITCH); ioconfig.outputs[6].set("OUT 7: LFO 3 Aux", OC::OUTPUT_MODE_PITCH); ioconfig.outputs[7].set("OUT 8: LFO 4 Aux", OC::OUTPUT_MODE_PITCH); } FLASHMEM size_t AppCrazyLFO::SaveAppData(util::StreamBufferWriter &sb) const { sb.Write(0x43); // 'C' sb.Write(0x4C); // 'L' sb.Write(active_preset_idx); sb.Write(p.master_dejavu); sb.Write(p.tr3_mode); sb.Write(p.tr4_mode); for (uint8_t u = 0; u < crazylfo::NUM_USER_PRESETS; ++u) { const crazylfo::Preset &up = user_presets[u]; for (int i = 0; i < 12; ++i) sb.Write(up.name[i]); sb.Write(up.bpm); sb.Write(up.clock_source); sb.Write(up.resync_period); for (int l = 0; l < crazylfo::NUM_LFOS; ++l) { sb.Write(up.lfo[l].waveform); sb.Write(up.lfo[l].sync_mode); sb.Write(up.lfo[l].rate_free_hz); sb.Write(up.lfo[l].clock_ratio_idx); sb.Write(up.lfo[l].tuplet_idx); sb.Write(up.lfo[l].phase_offset_deg); sb.Write(up.lfo[l].level_pct); sb.Write(up.lfo[l].offset_pct); sb.Write(up.lfo[l].polarity); sb.Write(up.lfo[l].morph_pct); sb.Write(up.lfo[l].dejavu_pct); sb.Write(up.lfo[l].loop_len); sb.Write(up.lfo[l].reset_enabled ? 1 : 0); sb.Write(up.lfo[l].drift_pct); sb.Write(up.lfo[l].aux_mode); } } return sb.written(); } FLASHMEM size_t AppCrazyLFO::RestoreAppData(util::StreamBufferReader &sb) { InitFactoryPresets(); uint8_t m1 = sb.Read(); uint8_t m2 = sb.Read(); if (m1 != 0x43 || m2 != 0x4C) { LoadPreset(0); return sb.read(); } active_preset_idx = sb.Read(); p.master_dejavu = sb.Read(); p.tr3_mode = sb.Read(); p.tr4_mode = sb.Read(); for (uint8_t u = 0; u < crazylfo::NUM_USER_PRESETS; ++u) { crazylfo::Preset &up = user_presets[u]; for (int i = 0; i < 12; ++i) up.name[i] = sb.Read(); up.bpm = sb.Read(); up.clock_source = sb.Read(); up.resync_period = sb.Read(); for (int l = 0; l < crazylfo::NUM_LFOS; ++l) { up.lfo[l].waveform = sb.Read(); up.lfo[l].sync_mode = sb.Read(); up.lfo[l].rate_free_hz = sb.Read(); up.lfo[l].clock_ratio_idx = sb.Read(); up.lfo[l].tuplet_idx = sb.Read(); up.lfo[l].phase_offset_deg = sb.Read(); up.lfo[l].level_pct = sb.Read(); up.lfo[l].offset_pct = sb.Read(); up.lfo[l].polarity = sb.Read(); up.lfo[l].morph_pct = sb.Read(); up.lfo[l].dejavu_pct = sb.Read(); up.lfo[l].loop_len = sb.Read(); up.lfo[l].reset_enabled = (sb.Read() != 0); up.lfo[l].drift_pct = sb.Read(); up.lfo[l].aux_mode = sb.Read(); } } LoadPreset(active_preset_idx); return sb.read(); } // ============================================================================ // Real-Time Engine Execution // ============================================================================ int32_t AppCrazyLFO::CalcSin(uint16_t ph16) { uint8_t idx = ph16 >> 8; uint8_t frac = ph16 & 0xFF; int16_t y0 = sin_lut[idx]; int16_t y1 = sin_lut[(idx + 1) & 0xFF]; return y0 + (((int32_t)(y1 - y0) * frac) >> 8); } int32_t AppCrazyLFO::CalcWaveform(int ch, uint32_t ph_acc, uint8_t wave, uint8_t morph) { uint16_t p16 = ph_acc >> 16; int32_t val = 0; switch (wave) { case crazylfo::WAVE_SINE: val = CalcSin(p16); break; case crazylfo::WAVE_TRIANGLE: { uint32_t pivot = (65535ULL * constrain((int)morph, 1, 99)) / 100ULL; if (p16 < pivot) { val = -32767 + (int32_t)((65534ULL * p16) / pivot); } else { val = 32767 - (int32_t)((65534ULL * (p16 - pivot)) / (65535 - pivot)); } break; } case crazylfo::WAVE_SAW_UP: val = -32767 + (int32_t)((65534ULL * p16) / 65535); break; case crazylfo::WAVE_SAW_DOWN: val = 32767 - (int32_t)((65534ULL * p16) / 65535); break; case crazylfo::WAVE_SQUARE: { uint32_t thresh = (65535ULL * constrain((int)morph, 1, 99)) / 100ULL; val = (p16 < thresh) ? 32767 : -32767; break; } case crazylfo::WAVE_STEP_RND: { int32_t v = rand_target[ch]; if (morph >= 15) { // Quantize voltage (Marbles style scales) if (morph < 35) { // Chromatic semitones (-12 to +12) int semi = map(v, -32767, 32767, -12, 12); val = (semi * 32767) / 12; } else if (morph < 60) { // Minor Pentatonic: 0, 3, 5, 7, 10 static const int pent_min[16] = {-12, -9, -7, -5, -2, 0, 3, 5, 7, 10, 12, 15, 17, 19, 22, 24}; int idx = map(v, -32767, 32767, 0, 15); val = (pent_min[idx] * 32767) / 12; } else if (morph < 85) { // Major Pentatonic: 0, 2, 4, 7, 9 static const int pent_maj[16] = {-12, -10, -8, -5, -3, 0, 2, 4, 7, 9, 12, 14, 16, 19, 21, 24}; int idx = map(v, -32767, 32767, 0, 15); val = (pent_maj[idx] * 32767) / 12; } else { // Octaves & Fifths static const int oct_5th[9] = {-12, -5, 0, 7, 12, 19, 24, 31, 36}; int idx = map(v, -32767, 32767, 0, 8); val = (oct_5th[idx] * 32767) / 12; } } else { val = v; // Continuous smooth random voltage } break; } case crazylfo::WAVE_SMOOTH_RND: { // Fluid Marbles interpolation uint16_t half_angle = p16 >> 1; // 0..32767 int32_t c = CalcSin(half_angle + 16384); // cos from 1 to -1 int32_t w = (32767 - c) >> 8; // 0..256 if (morph > 50) { int32_t lin = p16 >> 8; // 0..255 int blend = (morph - 50) * 2; // 0..100 w = (w * (100 - blend) + lin * blend) / 100; } val = rand_prev[ch] + (((rand_target[ch] - rand_prev[ch]) * w) >> 8); break; } case crazylfo::WAVE_CHAOS: { // Logistic attractor int32_t target = ((chaos_state[ch] - 5000) * 32767) / 5000; val = target; break; } case crazylfo::WAVE_BOUNCE: { // Parabolic bounce: 1 - 4*(t-0.5)^2 int32_t t = (int32_t)p16 - 32768; // -32768..+32767 int32_t t2 = (t * t) >> 15; // 0..32768 val = -32767 + (32767 - t2) * 2; val = constrain(val, -32767, 32767); break; } case crazylfo::WAVE_STAIRS: { int step = p16 / 8192; // 8 steps (0..7) val = -32767 + (step * (65534 / 7)); break; } default: val = 0; break; } return constrain(val, -32767, 32767); } void AppCrazyLFO::ProcessClockAndSync() { uint32_t now = micros(); // 1. TR1 Clock In if (Clock(0)) { flash_tr1 = true; if (last_tr1_us > 0) { uint32_t interval = now - last_tr1_us; if (interval >= 30000 && interval <= 3000000) { // 20 to 2000 BPM clock_period_us = (clock_period_us * 3 + interval) / 4; ext_clock_detected = true; ext_clock_timeout = now + 2500000; } } last_tr1_us = now; beat_counter++; // Polyrhythm Master Resync check if (p.resync_period > crazylfo::RESYNC_OFF) { static constexpr int RESYNC_BEATS[6] = {0, 4, 8, 12, 16, 32}; int target_beats = RESYNC_BEATS[p.resync_period]; if ((beat_counter % target_beats) == 0) { for (int i = 0; i < crazylfo::NUM_LFOS; ++i) { if (p.lfo[i].sync_mode == crazylfo::SYNC_TEMPO && p.lfo[i].reset_enabled) { phase[i] = ((uint64_t)p.lfo[i].phase_offset_deg * 4294967296ULL) / 360ULL; dejavu_step[i] = 0; // Resync Deja-Vu step to start of measure } } } } } // Timeout external clock if (ext_clock_detected && (now > ext_clock_timeout)) { ext_clock_detected = false; } // Internal clock fallback when not using Ext if (!ext_clock_detected || p.clock_source == 1) { uint32_t int_period = (60ULL * 1000000ULL) / constrain((int)p.bpm, 30, 300); clock_period_us = int_period; } // 2. TR2 Reset In if (Clock(1)) { flash_tr2 = true; beat_counter = 0; for (int i = 0; i < crazylfo::NUM_LFOS; ++i) { if (p.lfo[i].reset_enabled) { phase[i] = ((uint64_t)p.lfo[i].phase_offset_deg * 4294967296ULL) / 360ULL; dejavu_step[i] = 0; // Synchronize Deja-Vu loop restart } } } // 3. TR4 Trigger: Randomize or Mutate if (Clock(3)) { if (p.tr4_mode == 1) { MutateDejaVuAll(); } else { CrazyRandomize(); } } } void AppCrazyLFO::Controller() { ProcessClockAndSync(); // Freeze check (TR3) bool freeze = (p.tr3_mode == 0) && Gate(2); bool tr3_dejavu_lock = (p.tr3_mode == 1) && Gate(2); // Modulation inputs int cv_rate_mod = In(0); // CV1 modulates Master Rate int cv_lvl_mod = In(1); // CV2 modulates Level int cv_lfo2_mod = In(2); // CV3 modulates LFO 2 Rate int cv_offset_mod = In(3); // CV4 modulates Offset or DejaVu for (int i = 0; i < crazylfo::NUM_LFOS; ++i) { const crazylfo::LfoConfig &cfg = p.lfo[i]; // Calculate effective period in microseconds uint32_t period_us = 500000; if (cfg.sync_mode == crazylfo::SYNC_TEMPO) { int r_num = crazylfo::RATIO_NUMERATORS[cfg.clock_ratio_idx]; int r_den = crazylfo::RATIO_DENOMINATORS[cfg.clock_ratio_idx]; int t_num = crazylfo::TUPLET_NUMERATORS[cfg.tuplet_idx]; int t_den = crazylfo::TUPLET_DENOMINATORS[cfg.tuplet_idx]; uint64_t base_p = ((uint64_t)clock_period_us * r_den * t_den) / (r_num * t_num); period_us = constrain((uint32_t)base_p, 2000UL, 60000000UL); } else { int hz = constrain((int)cfg.rate_free_hz, 1, 5000); period_us = (100000000ULL) / hz; } // Apply CV Rate mod to LFO 1 or LFO 2 if (i == 0 && cv_rate_mod != 0) { int32_t mod_factor = 1000 + (cv_rate_mod / 8); if (mod_factor > 100) period_us = ((uint64_t)period_us * 1000) / mod_factor; } if (i == 1 && cv_lfo2_mod != 0) { int32_t mod_factor = 1000 + (cv_lfo2_mod / 8); if (mod_factor > 100) period_us = ((uint64_t)period_us * 1000) / mod_factor; } // Phase increment: (2^32 * 1,000,000) / (period_us * 16666) uint32_t phase_inc = (uint32_t)(257708346129ULL / period_us); // Add Drift/Jitter if enabled if (cfg.drift_pct > 0) { int32_t jitter = ((random(0, 1024) - 512) * (int32_t)cfg.drift_pct) / 100; phase_inc = phase_inc + (int32_t)(((int64_t)phase_inc * jitter) / 512000LL); } // Advance phase if (!freeze) { last_phase[i] = phase[i]; phase[i] += phase_inc; // Cycle wrap detection -> Step clock for this LFO! if (phase[i] < last_phase[i]) { rand_prev[i] = rand_target[i]; // Marbles Deja-Vu Loop & Mutation logic uint8_t eff_dejavu = cfg.dejavu_pct; if (p.master_dejavu > 0) { eff_dejavu = p.master_dejavu; } if (tr3_dejavu_lock) { eff_dejavu = 100; // Locked via TR3 Gate! } uint8_t len = constrain((int)cfg.loop_len, 1, crazylfo::MAX_DEJAVU_LEN); dejavu_step[i] = (dejavu_step[i] + 1) % len; bool should_mutate = false; if (eff_dejavu == 0) { should_mutate = true; // 100% fresh random } else if (eff_dejavu < 100) { uint8_t mutate_chance = 100 - eff_dejavu; if ((uint8_t)random(0, 100) < mutate_chance) { should_mutate = true; } } if (should_mutate) { dejavu_buf[i][dejavu_step[i]] = random(-32767, 32768); } rand_target[i] = dejavu_buf[i][dejavu_step[i]]; // Advance chaotic logistic attractor if (eff_dejavu == 100) { // Exact repeating attractor points chaos_state[i] = map(rand_target[i], -32767, 32768, 1000, 9000); } else { int32_t x = chaos_state[i]; int32_t next_x = (39ULL * x * (10000 - x)) / 100000ULL; chaos_state[i] = constrain(next_x, 100, 9900); } } } // Compute raw waveform (-32767..+32767) int32_t raw = CalcWaveform(i, phase[i], cfg.waveform, cfg.morph_pct); // Level (0..100%) int effective_level = cfg.level_pct; if (i == 0 && cv_lvl_mod != 0) { effective_level = constrain(effective_level + (cv_lvl_mod / 150), 0, 100); } // Scale to DAC units (1536 units = 1V, 7680 units = 5V) int32_t dac_units = 0; if (cfg.polarity == crazylfo::POLARITY_BIPOLAR) { dac_units = (raw * 3840LL * effective_level) / (32767LL * 100LL); } else if (cfg.polarity == crazylfo::POLARITY_UNIPOLAR_POS) { dac_units = ((raw + 32767) * 7680LL * effective_level) / (65534LL * 100LL); } else { dac_units = -((raw + 32767) * 7680LL * effective_level) / (65534LL * 100LL); } // Offset (-100%..+100% -> -5V..+5V) int effective_offset = cfg.offset_pct; if (i == 1 && cv_offset_mod != 0) { effective_offset = constrain(effective_offset + (cv_offset_mod / 150), -100, 100); } int32_t offset_units = (effective_offset * 7680LL) / 100LL; int32_t final_val = constrain(dac_units + offset_units, -7680, 7680); out_vals[i] = final_val; out_meters[i] = final_val; Out(i, final_val); // Aux Output for this LFO (Ch 4..7 / OUT 5..8) int32_t aux_val = 0; switch (cfg.aux_mode) { case crazylfo::AUX_INVERTED: aux_val = -final_val; break; case crazylfo::AUX_QUAD_90: { int32_t raw_quad = CalcWaveform(i, phase[i] + 0x40000000UL, cfg.waveform, cfg.morph_pct); int32_t quad_dac = (cfg.polarity == crazylfo::POLARITY_BIPOLAR) ? (raw_quad * 3840LL * effective_level) / (32767LL * 100LL) : ((raw_quad + 32767) * 7680LL * effective_level) / (65534LL * 100LL); aux_val = constrain(quad_dac + offset_units, -7680, 7680); break; } case crazylfo::AUX_PULSE_EOC: { uint16_t p16 = phase[i] >> 16; aux_val = (p16 < 2000) ? 7680 : 0; break; } case crazylfo::AUX_RECTIFIED: aux_val = abs(final_val); break; } out_vals[4 + i] = aux_val; out_meters[4 + i] = aux_val; Out(4 + i, aux_val); } } // ============================================================================ // UI & Display Methods (128x64 OLED) // ============================================================================ FLASHMEM void AppCrazyLFO::DrawDebugInfo() const {} FLASHMEM void AppCrazyLFO::DrawScreensaver() const { DrawOverviewPage(); } FLASHMEM void AppCrazyLFO::DrawMenu() const { View(); } FLASHMEM void AppCrazyLFO::View() const { DrawHeader(); switch (current_page) { case PAGE_OVERVIEW: DrawOverviewPage(); break; case PAGE_LFO1: DrawLfoPage(0); break; case PAGE_LFO2: DrawLfoPage(1); break; case PAGE_LFO3: DrawLfoPage(2); break; case PAGE_LFO4: DrawLfoPage(3); break; case PAGE_SYNC: DrawSyncPage(); break; case PAGE_PRESETS: DrawPresetsPage(); break; default: break; } } FLASHMEM void AppCrazyLFO::DrawHeader() const { static const char *const page_titles[PAGE_COUNT] = { "OVERVIEW", "LFO 1", "LFO 2", "LFO 3", "LFO 4", "SYNC/DV", "PRESETS" }; graphics.setPrintPos(2, 2); graphics.printf("CL:%s", page_titles[current_page]); // Status message or BPM indicator if (status_timer > millis()) { graphics.setPrintPos(55, 2); graphics.print(status_msg); } else { graphics.setPrintPos(65, 2); if (ext_clock_detected) { int bpm = (60000000ULL / clock_period_us); graphics.printf("EXT:%dB", bpm); } else { graphics.printf("INT:%dB", p.bpm); } } // Flash indicator for TR1 & TR2 if (flash_tr1) { graphics.drawRect(114, 2, 5, 5); const_cast(this)->flash_tr1 = false; } if (flash_tr2) { graphics.invertRect(121, 2, 5, 5); const_cast(this)->flash_tr2 = false; } graphics.drawHLine(0, 10, 128); } FLASHMEM void AppCrazyLFO::DrawMiniWaveIcon(int x, int y, uint8_t wave) const { switch (wave) { case crazylfo::WAVE_SINE: graphics.setPixel(x, y + 3); graphics.setPixel(x + 2, y + 1); graphics.setPixel(x + 4, y + 3); graphics.setPixel(x + 6, y + 5); graphics.setPixel(x + 8, y + 3); break; case crazylfo::WAVE_TRIANGLE: graphics.drawLine(x, y + 5, x + 4, y); graphics.drawLine(x + 4, y, x + 8, y + 5); break; case crazylfo::WAVE_SAW_UP: graphics.drawLine(x, y + 5, x + 8, y); graphics.drawLine(x + 8, y, x + 8, y + 5); break; case crazylfo::WAVE_SAW_DOWN: graphics.drawLine(x, y, x + 8, y + 5); graphics.drawLine(x, y, x, y + 5); break; case crazylfo::WAVE_SQUARE: graphics.drawRect(x, y, 4, 3); graphics.drawRect(x + 4, y + 3, 4, 3); break; default: graphics.drawRect(x, y + 1, 8, 4); break; } } FLASHMEM void AppCrazyLFO::DrawOverviewPage() const { for (int i = 0; i < crazylfo::NUM_LFOS; ++i) { int y = 13 + i * 12; const crazylfo::LfoConfig &cfg = p.lfo[i]; // LFO Index graphics.setPrintPos(2, y); graphics.printf("%d:", i + 1); // Mini icon & wave name DrawMiniWaveIcon(14, y, cfg.waveform); graphics.setPrintPos(26, y); graphics.print(crazylfo::WAVE_SHORT[cfg.waveform]); // Ratio / Tuplet or DejaVu Status graphics.setPrintPos(48, y); if (cfg.dejavu_pct > 0) { if (cfg.dejavu_pct == 100) { graphics.printf("L%d:Lck", cfg.loop_len); } else { graphics.printf("L%d:%d%%", cfg.loop_len, cfg.dejavu_pct); } } else if (cfg.sync_mode == crazylfo::SYNC_TEMPO) { if (cfg.tuplet_idx > 0) { graphics.print(crazylfo::TUPLET_SHORT[cfg.tuplet_idx]); } else { graphics.print(crazylfo::RATIO_NAMES[cfg.clock_ratio_idx]); } } else { graphics.printf("%d.%1dHz", cfg.rate_free_hz / 100, (cfg.rate_free_hz % 100) / 10); } // Animated scope / Level bar on right (x=82..126) int meter_w = map(constrain(out_meters[i], -7680, 7680), -7680, 7680, 0, 40); graphics.drawFrame(82, y + 1, 42, 7); graphics.drawLine(103, y, 103, y + 8); // center line (0V) if (meter_w >= 21) { graphics.drawRect(103, y + 2, meter_w - 21, 5); } else { graphics.drawRect(82 + meter_w, y + 2, 21 - meter_w, 5); } } graphics.drawHLine(0, 62, 128); } FLASHMEM void AppCrazyLFO::DrawLfoPage(int ch) const { const crazylfo::LfoConfig &cfg = p.lfo[ch]; static const char *const param_labels[14] = { "Waveform", "Sync Mode", "Rate/Ratio", "Poly Tuplet", "Phase Shift", "Level", "Offset", "Polarity", "Morph/Scale", "DejaVu %", "Loop Len", "Reset TR2", "Drift/Chaos", "Aux Out" }; static constexpr int NUM_PARAMS = 14; int start_idx = (cursor / 4) * 4; for (int i = 0; i < 4; ++i) { int idx = start_idx + i; if (idx >= NUM_PARAMS) break; int y = 14 + i * 11; OrnDeferInvert _inv(cursor == idx, 1, y - 1, 126, 10); graphics.setPrintPos(4, y); graphics.print(param_labels[idx]); graphics.setPrintPos(72, y); switch (idx) { case 0: graphics.print(crazylfo::WAVE_NAMES[cfg.waveform]); break; case 1: graphics.print(cfg.sync_mode == crazylfo::SYNC_TEMPO ? "Tempo Sync" : "Free Hz"); break; case 2: if (cfg.sync_mode == crazylfo::SYNC_TEMPO) { graphics.print(crazylfo::RATIO_NAMES[cfg.clock_ratio_idx]); } else { graphics.printf("%d.%02d Hz", cfg.rate_free_hz / 100, cfg.rate_free_hz % 100); } break; case 3: graphics.print(crazylfo::TUPLET_NAMES[cfg.tuplet_idx]); break; case 4: graphics.printf("%d deg", cfg.phase_offset_deg); break; case 5: graphics.printf("%d%%", cfg.level_pct); break; case 6: graphics.printf("%+d%%", cfg.offset_pct); break; case 7: graphics.print(cfg.polarity == crazylfo::POLARITY_BIPOLAR ? "Bipolar" : cfg.polarity == crazylfo::POLARITY_UNIPOLAR_POS ? "Unipolar+" : "Unipolar-"); break; case 8: if (cfg.waveform == crazylfo::WAVE_STEP_RND) { if (cfg.morph_pct < 15) graphics.print("Free S&H"); else if (cfg.morph_pct < 35) graphics.print("Chromatic"); else if (cfg.morph_pct < 60) graphics.print("Min Pent"); else if (cfg.morph_pct < 85) graphics.print("Maj Pent"); else graphics.print("Oct & 5th"); } else { graphics.printf("%d%%", cfg.morph_pct); } break; case 9: if (cfg.dejavu_pct == 0) graphics.print("Off (0%)"); else if (cfg.dejavu_pct == 100) graphics.print("100% Lock"); else graphics.printf("%d%% Mut", cfg.dejavu_pct); break; case 10: graphics.printf("%d Steps", cfg.loop_len); break; case 11: graphics.print(cfg.reset_enabled ? "Yes" : "No"); break; case 12: graphics.printf("%d%%", cfg.drift_pct); break; case 13: graphics.print(crazylfo::AUX_NAMES[cfg.aux_mode]); break; } } // Real-time voltage readout on bottom int v_millivolts = (out_meters[ch] * 1000) / 1536; graphics.setPrintPos(2, 56); graphics.printf("OUT %d: %+d.%02dV Aux:%+d.%02dV", ch + 1, v_millivolts / 1000, abs((v_millivolts % 1000) / 10), (out_meters[4 + ch] * 1000) / 1536000, abs(((out_meters[4 + ch] * 1000) / 1536) % 1000) / 10); } FLASHMEM void AppCrazyLFO::DrawSyncPage() const { static const char *const sync_labels[7] = { "Clock", "BPM", "Resync", "TR3", "TR4", "DejaVu", "Random!" }; // 4 rows of 10 px (font is 8 px high) - scrolls with the cursor int start_idx = (cursor / 4) * 4; for (int r = 0; r < 4; ++r) { int i = start_idx + r; if (i >= 7) break; int y = 14 + r * 11; OrnDeferInvert _inv(cursor == i, 1, y - 1, 126, 10); graphics.setPrintPos(4, y); graphics.print(sync_labels[i]); graphics.setPrintPos(50, y); switch (i) { case 0: graphics.print(p.clock_source == 0 ? "Auto" : p.clock_source == 1 ? "Internal" : "TR1 Ext"); break; case 1: graphics.printf("%d BPM", p.bpm); break; case 2: graphics.printf("%.12s", crazylfo::RESYNC_NAMES[p.resync_period]); break; case 3: graphics.print(p.tr3_mode == 0 ? "Freeze/Hold" : "DejaVu Lock"); break; case 4: graphics.print(p.tr4_mode == 0 ? "Randomize" : "Deja Mutate"); break; case 5: if (p.master_dejavu == 0) graphics.print("Per-LFO"); else if (p.master_dejavu == 100) graphics.print("Lock All"); else graphics.printf("%d%%", p.master_dejavu); break; case 6: graphics.print("[Push Click]"); break; } } } FLASHMEM void AppCrazyLFO::DrawPresetsPage() const { int start_idx = (cursor / 4) * 4; for (int i = 0; i < 4; ++i) { int idx = start_idx + i; if (idx >= crazylfo::TOTAL_PRESETS) break; int y = 13 + i * 10; OrnDeferInvert _inv(cursor == idx, 1, y - 1, 126, 9); graphics.setPrintPos(4, y); if (idx < crazylfo::NUM_FACTORY_PRESETS) { graphics.printf("F%d: %.10s", idx + 1, factory_presets[idx].name); } else { int u = idx - crazylfo::NUM_FACTORY_PRESETS; graphics.printf("U%d: %.10s", u + 1, user_presets[u].name); } if (idx == active_preset_idx) { graphics.setPrintPos(104, y); graphics.print("*ACT"); } } graphics.setPrintPos(2, 54); graphics.print("Click R: Load(F/U) Save(U)"); } // ============================================================================ // UI Event Handlers // ============================================================================ FLASHMEM void AppCrazyLFO::HandleButtonEvent(const UI::Event &event) { if (event.type == UI::EVENT_BUTTON_PRESS) { if (event.control == OC::CONTROL_BUTTON_UP) { current_page = (Page)((current_page + 1) % PAGE_COUNT); cursor = 0; isEditing = false; } else if (event.control == OC::CONTROL_BUTTON_DOWN) { current_page = (Page)((current_page + PAGE_COUNT - 1) % PAGE_COUNT); cursor = 0; isEditing = false; } else if (event.control == OC::CONTROL_BUTTON_L) { // Toggle between Overview and Detail if (current_page == PAGE_OVERVIEW) current_page = PAGE_LFO1; else current_page = PAGE_OVERVIEW; cursor = 0; isEditing = false; } else if (event.control == OC::CONTROL_BUTTON_R) { // Click action if (current_page == PAGE_SYNC && cursor == 6) { CrazyRandomize(); } else if (current_page == PAGE_PRESETS) { if (cursor < crazylfo::NUM_FACTORY_PRESETS) { LoadPreset(cursor); } else { // Save or load user preset SaveUserPreset(cursor - crazylfo::NUM_FACTORY_PRESETS); } } else { isEditing = !isEditing; } } } } FLASHMEM void AppCrazyLFO::HandleEncoderEvent(const UI::Event &event) { if (event.control == OC::CONTROL_ENCODER_L) { // Navigate Cursor int max_cursor = 0; switch (current_page) { case PAGE_OVERVIEW: max_cursor = 0; break; case PAGE_LFO1: case PAGE_LFO2: case PAGE_LFO3: case PAGE_LFO4: max_cursor = 13; break; case PAGE_SYNC: max_cursor = 6; break; case PAGE_PRESETS: max_cursor = crazylfo::TOTAL_PRESETS - 1; break; default: break; } cursor = constrain(cursor + event.value, 0, max_cursor); } else if (event.control == OC::CONTROL_ENCODER_R) { // Edit Parameters int delta = event.value; if (current_page >= PAGE_LFO1 && current_page <= PAGE_LFO4) { int ch = current_page - PAGE_LFO1; crazylfo::LfoConfig &cfg = p.lfo[ch]; switch (cursor) { case 0: cfg.waveform = (cfg.waveform + delta + crazylfo::NUM_WAVEFORMS) % crazylfo::NUM_WAVEFORMS; break; case 1: cfg.sync_mode = (cfg.sync_mode + 1) % 2; break; case 2: if (cfg.sync_mode == crazylfo::SYNC_TEMPO) { cfg.clock_ratio_idx = constrain(cfg.clock_ratio_idx + delta, 0, crazylfo::NUM_RATIOS - 1); } else { cfg.rate_free_hz = constrain((int)cfg.rate_free_hz + delta * 5, 2, 5000); } break; case 3: cfg.tuplet_idx = constrain(cfg.tuplet_idx + delta, 0, crazylfo::NUM_TUPLETS - 1); break; case 4: cfg.phase_offset_deg = (cfg.phase_offset_deg + delta * 45 + 360) % 360; break; case 5: cfg.level_pct = constrain((int)cfg.level_pct + delta * 2, 0, 100); break; case 6: cfg.offset_pct = constrain((int)cfg.offset_pct + delta * 2, -100, 100); break; case 7: cfg.polarity = (cfg.polarity + delta + 3) % 3; break; case 8: cfg.morph_pct = constrain((int)cfg.morph_pct + delta * 2, 1, 99); break; case 9: cfg.dejavu_pct = constrain((int)cfg.dejavu_pct + delta * 5, 0, 100); break; case 10: cfg.loop_len = constrain((int)cfg.loop_len + delta, 1, crazylfo::MAX_DEJAVU_LEN); break; case 11: cfg.reset_enabled = !cfg.reset_enabled; break; case 12: cfg.drift_pct = constrain((int)cfg.drift_pct + delta * 2, 0, 100); break; case 13: cfg.aux_mode = (cfg.aux_mode + delta + 4) % 4; break; } } else if (current_page == PAGE_SYNC) { switch (cursor) { case 0: p.clock_source = (p.clock_source + delta + 3) % 3; break; case 1: p.bpm = constrain((int)p.bpm + delta, 30, 300); break; case 2: p.resync_period = (p.resync_period + delta + 6) % 6; break; case 3: p.tr3_mode = (p.tr3_mode + 1) % 2; break; case 4: p.tr4_mode = (p.tr4_mode + 1) % 2; break; case 5: p.master_dejavu = constrain((int)p.master_dejavu + delta * 5, 0, 100); break; default: break; } } else if (current_page == PAGE_PRESETS) { cursor = constrain(cursor + delta, 0, crazylfo::TOTAL_PRESETS - 1); } } } #endif // ARDUINO_TEENSY41