{"id":35365574,"url":"https://github.com/danilogcrf2-oss/esp32synth","last_synced_at":"2026-04-18T04:00:49.063Z","repository":{"id":330629195,"uuid":"1123417709","full_name":"danilogcrf2-oss/ESP32Synth","owner":"danilogcrf2-oss","description":"Polyphonic synthesizer with 80 voices/channels for the ESP32 family, offering high-fidelity audio (48kHz).","archived":false,"fork":false,"pushed_at":"2026-02-21T04:44:15.000Z","size":959,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-02-21T11:44:35.015Z","etag":null,"topics":["audio","audio-player","audio-processing","esp32","esp32s3","synthesis","synthesizer"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/danilogcrf2-oss.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-26T20:39:36.000Z","updated_at":"2026-02-21T04:44:19.000Z","dependencies_parsed_at":null,"dependency_job_id":"a07ba322-e308-4098-9fa5-e113d0be1201","html_url":"https://github.com/danilogcrf2-oss/ESP32Synth","commit_stats":null,"previous_names":["danilogcrf2-oss/esp32synth"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/danilogcrf2-oss/ESP32Synth","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danilogcrf2-oss%2FESP32Synth","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danilogcrf2-oss%2FESP32Synth/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danilogcrf2-oss%2FESP32Synth/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danilogcrf2-oss%2FESP32Synth/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/danilogcrf2-oss","download_url":"https://codeload.github.com/danilogcrf2-oss/ESP32Synth/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danilogcrf2-oss%2FESP32Synth/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31955919,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-18T00:39:45.007Z","status":"online","status_checked_at":"2026-04-18T02:00:07.018Z","response_time":103,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["audio","audio-player","audio-processing","esp32","esp32s3","synthesis","synthesizer"],"created_at":"2026-01-02T01:51:47.869Z","updated_at":"2026-04-18T04:00:49.055Z","avatar_url":"https://github.com/danilogcrf2-oss.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ESP32Synth v2.4.0 — Professional Audio Synthesis Engine\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/danilogcrf2-oss/ESP32Synth/main/banner.jpg\" alt=\"ESP32Synth banner\" width=\"100%\"\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/version-2.4.0-green.svg\" alt=\"Version\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/platform-ESP32-orange.svg\" alt=\"Platform\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/license-MIT-blue.svg\" alt=\"License\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/performance-extreme-red.svg\" alt=\"Performance\"\u003e\n\u003c/p\u003e\n\n**[English]** A high-performance, polyphonic audio synthesis library for the ESP32. Engineered for professional applications requiring extreme optimization, zero-latency audio, massive polyphony (up to 350+ voices), custom DSP hooks, and direct SD card audio streaming.\n\n**[Português]** Uma biblioteca de síntese de áudio polifônica de extrema performance para o ESP32. Projetada para aplicações profissionais que exigem otimização brutal, zero latência, polifonia massiva (até 350+ vozes), injeção de efeitos DSP customizados e streaming direto do cartão SD.\n\n\u003e *\"Se Deus não existisse, esse projeto também não existiria. Tudo só foi possível por causa d'Ele.\"*\n\n---\n\n### 📝 Developer's Note / Nota do Desenvolvedor\n\n\u003e Muito obrigado por usar o ESP32Synth! \n\u003e\n\u003e O ESP32Synth foi concebido e exaustivamente testado em um ESP32 DevKit V1 (ESP32-D0WD-V3) a 240MHz e em um ESP32-S3 Zero da Waveshare. O objetivo sempre foi um só: transformar um microcontrolador barato em um sintetizador absurdamente rápido, polifônico e de alta fidelidade.\n\u003e \n\u003e Com ele, você pode criar músicas, efeitos sonoros complexos e trilhas gerativas. Pode emular o som clássico de Chiptune (usando nossa engine de bitcrush e redução de bits nativa) ou reproduzir arquivos WAV pesados de um cartão SD em segundo plano sem engasgar o processador. Deixe a criatividade fluir!\n\u003e\n\u003e **Um aviso sobre a Arquitetura Aberta (DSP e Custom Waves):** Nesta versão, abri o núcleo da biblioteca. Agora você pode escrever suas próprias funções de ondas matemáticas ou injetar algoritmos de Reverb e Delay direto no buffer de áudio master. No entanto, lembre-se: o código roda a 48.000 vezes por segundo. Use matemática puramente otimizada (como os exemplos que deixei neste README). Evite *floats* e divisões no loop de DSP para não sacrificar a polifonia ou roubar tempo das tasks do FreeRTOS.\n\u003e\n\u003e Qualquer bug, problema de compilação ou ideia de melhoria, abra uma issue no GitHub. A comunidade agradece!\n\n---\n\n## 📖 Table of Contents / Índice\n\n1. [🇺🇸 English Documentation](#-english-documentation)\n   - [1. Overview \u0026 Key Features](#1-overview--key-features)\n   - [2. Under the Hood (Architecture \u0026 Limits)](#2-how-it-works-under-the-hood-internal-architecture)\n   - [3. Requirements \u0026 Installation](#3-requirements--installation)\n   - [4. Definitive API Guide](#4-definitive-api-guide)\n   - [5. Advanced DSP \u0026 Custom Waves](#5-creating-effects-reverb--custom-waves)\n2. [🇧🇷 Documentação em Português](#-documentação-em-português) *(Abaixo da seção em Inglês)*\n3. [🛠 Tools / Ferramentas](#-tools--ferramentas)\n4. [⚠️ Troubleshooting](#-common-troubleshooting)\n\n---\n\n# 🇺🇸 English Documentation\n\n## 1. Overview \u0026 Key Features\n\n**ESP32Synth** is not just a simple beep generator; it's a full-fledged, studio-grade mixing and synthesis engine written bare-metal over the ESP-IDF.\n\n* **Extreme Polyphony:** Comfortably supports **80 simultaneous voices** out of the box, with an engine capable of pushing up to **350 voices** if needed.\n* **Low-Level Access (NEW):** A powerful *Hooks* system (`setCustomDSP`, `setCustomWave`, `setCustomControl`) allowing you to inject your own effect algorithms (Reverb, Delays) and waveforms directly into the I2S render loop.\n* **Lo-Fi Engine (NEW):** Native *Bitcrush* control and bit-depth volume reduction for dirty, retro, Chiptune-style sounds.\n* **Flexible Oscillators:** Sine, Triangle, Sawtooth, Pulse (with adjustable PWM), Noise (fast LCG), *Wavetables*, RAM Samplers, and *Custom Waves*. Seamlessly switch between any wave type on the fly in `O(1)` time.\n* **Decoupled SD Streaming:** Play up to 4 heavy WAV files simultaneously. The SD card is managed by a background Ring Buffer task, guaranteeing the main audio thread never stutters.\n* **Full Modulation:** Independent ADSR Envelopes, LFOs (Vibrato, Tremolo), Portamento (Absolute pitch and volume slides), and a built-in Arpeggiator.\n\n---\n\n## 2. How It Works: Under the Hood (Internal Architecture)\n\nTo achieve massive polyphony on an embedded MCU, slow operations like `float` math, divisions (`/`), and branching (complex `if/else` chains) have been eradicated from the audio path. \n\n### A. The Limits of Silicon (Polyphony Scale)\nThe library allows you to configure `MAX_VOICES` in the header file. Here is how the ESP32 behaves across the spectrum:\n* **80 Voices (Default):** The sweet spot. Audio is crystal clear, RAM usage is low, and the CPU has plenty of idle time to handle Wi-Fi, displays (LVGL), and sensor reading on Core 0.\n* **140 Voices (RAM Safe Max):** For heavy multi-track midi playback. It consumes a larger chunk of the Heap for voice data structures but maintains absolute stability.\n* **350 Voices (Engine Limit):** Pushing the ESP32 to its absolute limits. The audio renders flawlessly, but Core 1 is heavily occupied.\n* **364+ Voices (The Abyss/Starvation):** At this threshold, the render loop takes longer to compute a block of audio than it takes to play it. FreeRTOS has no CPU ticks left to manage basic system tasks. The result? Audio jitter, RTOS starvation, and eventual Watchdog Timer (WDT) panics. *It proves the sheer brute force of the library—but respect the limits of physics!*\n\n### B. 16.16 Fixed-Point Math \u0026 32-bit Accumulators\nWe *never* use `float` or `double` during audio rendering. The synth maps frequencies using 32-bit phase accumulators. To control pitch and read speeds, we use **16.16 Fixed-Point** arithmetic: 16 bits for the integer part, 16 bits for the fractional. Volume math utilizes heavy `int64_t` bit-shifts (`\u003e\u003e 16`) for brutal, instantaneous precision.\n\n### C. Dedicated Core 1 \u0026 IRAM_ATTR\nThe `renderLoop()` is a FreeRTOS Task pinned to **Core 1** with maximum priority. Critical rendering functions use the `IRAM_ATTR` flag. This forces the ESP32 to load the code into the ultra-fast internal RAM, bypassing the massive latency of fetching instructions from Flash memory (Cache Misses).\n\n### D. Separated Rates (Audio vs. Control)\nIt is inefficient to calculate the ADSR envelope or Vibrato LFO 48,000 times per second. \n1.  **Audio Rate (48kHz):** Processes only oscillators, phase increments, and buffer sums.\n2.  **Control Rate (Default 100Hz):** Wakes up only every ~480 audio samples to recalculate pitch slides, advance LFO phases, and update ADSR state machines. \n\n### E. Safe Mixing (Headroom)\nSumming dozens of 16-bit voices would instantly clip (distort) the signal. To prevent this, the internal `mixBuffer` uses **32-bit integers**. Voices are summed freely with massive headroom, and only at the very final output stage is the signal scaled by the Master Volume, bitcrushed (if enabled), and cleanly down-sampled to fit the 16-bit or 32-bit I2S hardware.\n\n---\n\n## 3. Requirements \u0026 Installation\n\n* **Hardware:** ESP32 Classic or ESP32-S3 (Dual Core @ 240MHz). *Single-Core variants (S2, C3) are not recommended due to RTOS task collision.*\n* **External I2S DAC (Highly Recommended):** Modules like the **PCM5102A** or **UDA1334A** guarantee studio-quality audio. The ESP32's internal DAC has a high noise floor and is only 8-bit native.\n* **Installation:** \n  1. Download this repository as a `.ZIP` or search for \"ESP32Synth\" in the Arduino IDE Library Manager.\n  2. *Requires ESP32 Board Core version 3.0.0 or newer.*\n## 4. Definitive API Guide\n\nHere we explain how to wield the absolute power of the ESP32Synth engine.\n\n### Initialization \u0026 Global Configuration\n```cpp\nESP32Synth synth;\n\nvoid setup() {\n    // Initialize I2S in 32-bit format. \n    // Recommended DAC Pins (e.g., PCM5102A): BCK=4, WS=15, DATA=2\n    synth.begin(2, SMODE_I2S, 4, 15, I2S_32BIT);\n    \n    // Set the calculation rate for envelopes and LFOs (default 100Hz)\n    // Higher = smoother slides, but uses slightly more CPU.\n    synth.setControlRateHz(200);\n\n    // [NEW] Reduces master audio quality to 8-bits, creating a gritty Lo-Fi effect\n    synth.setMasterBitcrush(8);\n\n    // [NEW] Reduces the base resolution for dynamic internal volume calculations\n    synth.setVolDepthBase(8);\n\n    // Volume is a uint16_t. If VolDepthBase is 8 (default), the maximum is 255.\n    synth.setMasterVolume(255); \n}\n```\n\n### Triggering Notes \u0026 Seamless Wave Switching\nThe library handles notes using \"CentiHz\" (Hz * 100). Include `ESP32SynthNotes.h` to use standard constants like `c4` (Middle C = 261.63 Hz). Thanks to the `O(1)` Jump Table architecture, you can switch waveforms *instantly*, even while a note is currently sounding, without audio glitches or memory leaks.\n```cpp\n// Voice 0, C4 (c4), Volume 255\nsynth.noteOn(0, c4, 255);\n\n// Change the wave of Voice 0 to a Square/Pulse wave LIVE!\nsynth.setWave(0, WAVE_PULSE);\n\n// Change the Pulse Width (PWM) of the square wave (0 to 255)\nsynth.setPulseWidth(0, 128); // 128 = 50% = Perfect square wave\n\n// Release the key (starts the Release phase of the ADSR envelope)\nsynth.noteOff(0);\n```\n\n### Pitch \u0026 Volume Sliding (Portamento)\nSlides use Bresenham's line algorithm for absolute integer precision, moving gracefully over time without expensive float mathematics.\n```cpp\nsynth.noteOn(0, c4, 255);\n// Slide from C4 to C5 taking exactly 1000 milliseconds\nsynth.slideFreqTo(0, c5, 1000);\n\n// Gradually fade the volume to zero over 2 seconds\nsynth.slideVolTo(0, 0, 2000);\n```\n\n### Configuring the ADSR (Envelope)\nAttack, Decay, and Release are defined in milliseconds. Sustain is an amplitude level.\n```cpp\n// Attack = 10ms (Fast punch)\n// Decay = 300ms\n// Sustain = 127 (Half volume)\n// Release = 1500ms (Long, fading tail)\nsynth.setEnv(0, 10, 300, 127, 1500);\n```\n\n### Direct WAV File Streaming (SD Card)\nStreaming heavy WAV files won't stutter your synth, but it requires your SD module to be initialized at high speeds (16MHz to 20MHz) on standard hardware SPI pins.\n```cpp\n#include \u003cSD.h\u003e\n#include \u003cSPI.h\u003e\n\nvoid setup() {\n    SPI.begin(18, 19, 23, 5);\n    SD.begin(5, SPI, 16000000); // 16MHz is VITAL for audio stability!\n\n    // Playback: Voice 1, FileSystem, Path, Volume (255), Pitch (c4), Loop (true)\n    synth.playStream(1, SD, \"/drum_loop.wav\", 255, c4, true);\n\n    // Jump to the 5-second mark (5000 ms) of the audio track\n    synth.seekStreamMs(1, 5000);\n}\n```\n\n---\n\n## 5. Advanced DSP \u0026 Custom Waves\n\nWith version 2.4.0, **ESP32Synth** has opened its core engine for you to inject high-performance code.\n\n### A. Master Global Effects (Reverb, Delay)\nBy using `setCustomDSP`, you intercept the final 32-bit mix buffer BEFORE it goes to the DAC. \n**Warning:** This code runs 48,000 times per second. It must be brutally optimized. Do not use modulo (`%`) for buffer wrapping, and avoid floats. \n\nHere is an example of a **Studio-Grade Tape Reverb with Analog Saturation and a DC Blocker**:\n\n```cpp\n#define TAPE_LEN 20000 \nint32_t* reverbTape;\nint writeHead = 0;\n\nint32_t dcBlockerPrevWet = 0;\nint32_t dcBlockerState = 0;\nint32_t lpState = 0; \n\n// IRAM_ATTR forces this function into ultra-fast RAM!\nvoid IRAM_ATTR reverbDSP(int32_t* mixBuffer, int numSamples) {\n    if (!reverbTape) return; \n\n    for (int i = 0; i \u003c numSamples; i++) {\n        int32_t dry = mixBuffer[i];\n\n        // 1. Safe Circular Buffer Reads (No slow '%' operator)\n        // We choose prime numbers smaller than TAPE_LEN for the delay taps to avoid resonance buildup.\n        int tap1 = writeHead - 4327;  if (tap1 \u003c 0) tap1 += TAPE_LEN;\n        int tap2 = writeHead - 11003; if (tap2 \u003c 0) tap2 += TAPE_LEN;\n        int tap3 = writeHead - 19013; if (tap3 \u003c 0) tap3 += TAPE_LEN;\n\n        // Sum the 3 heads and divide by 4 (\u003e\u003e 2) to prevent clipping\n        int32_t wet = (reverbTape[tap1] \u003e\u003e 2) + (reverbTape[tap2] \u003e\u003e 2) + (reverbTape[tap3] \u003e\u003e 2);\n\n        // 2. DC Blocker (Crucial High-Pass Filter!)\n        // Kills any standing low-frequency energy that would cause an infinite noise loop.\n        int32_t dcBlocked = wet - dcBlockerPrevWet + ((dcBlockerState * 253) \u003e\u003e 8);\n        dcBlockerPrevWet = wet;\n        dcBlockerState = dcBlocked;\n\n        // 3. Low-Pass Filter (Dampens the echoes over time, creating warmth)\n        lpState = ((dcBlocked * 50) + (lpState * 206)) \u003e\u003e 8; \n\n        // 4. Calculate Feedback to record back to the tape (~78% feedback)\n        int32_t feedback = (dry \u003e\u003e 1) + ((lpState * 200) \u003e\u003e 8);\n\n        // 5. ANALOG SATURATION (Safety Soft-Clipping)\n        // If the math tries to explode, we crush it cleanly at the 16-bit limit.\n        if (feedback \u003e 32767) feedback = 32767;\n        else if (feedback \u003c -32768) feedback = -32768;\n\n        // Write to tape and advance the head\n        reverbTape[writeHead] = feedback;\n        writeHead++;\n        if (writeHead \u003e= TAPE_LEN) writeHead = 0;\n\n        // Master Mix: Dry signal + Processed Reverb tail\n        mixBuffer[i] = dry + lpState;\n    }\n}\n\nvoid setup() {\n    // Safely allocate memory in RAM (Heap)\n    reverbTape = (int32_t*)heap_caps_calloc(TAPE_LEN, sizeof(int32_t), MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);\n    synth.begin(2, SMODE_I2S, 4, 15, I2S_32BIT);\n    \n    // INJECT THE REVERB INTO THE ENGINE!\n    if (reverbTape) synth.setCustomDSP(reverbDSP);\n}\n```\n\n### B. Writing Your Own Oscillator (Custom Waves)\nIf the standard waves aren't enough, create your own sonic math using `setCustomWave()`. And because of our Jump Table design, **you can switch back to a normal wave at any moment** using `setWave(voice, WAVE_SINE)`. The engine handles the transition flawlessly.\n\nHere is an example of an **FM Feedback Sine Wave**. The wave modulates its own phase based on its previous output, creating a rich, metallic resonance commonly found in classic FM synthesizers (like the Yamaha DX7).\n\n```cpp\nvoid IRAM_ATTR waveFMSine(Voice* vo, int32_t* mixBuffer, int samples, int32_t startEnv, int32_t envStep) {\n    int32_t currentEnv = startEnv;\n    int32_t volBase = ((uint32_t)vo-\u003evol * vo-\u003etrmModGain) \u003e\u003e 8;\n    uint32_t ph = vo-\u003ephase;\n    uint32_t inc = vo-\u003ephaseInc + vo-\u003evibOffset;\n    \n    // We repurpose 'noiseSample' variable to store our previous output state\n    int16_t prevOut = vo-\u003enoiseSample; \n\n    for (int i = 0; i \u003c samples; i++) {\n        // THE MAGIC: Phase modulation via feedback.\n        // By shifting by 15, the feedback twists the wave by ~12%. \n        // This is the \"sweet spot\" for musical resonance before it collapses into noise.\n        uint32_t modPh = ph + ((int32_t)prevOut \u003c\u003c 15); \n        \n        // Fetch from the engine's built-in Sine LUT\n        int32_t s = sineLUT[(modPh \u003e\u003e SINE_SHIFT) \u0026 SINE_LUT_MASK] \u003e\u003e 16;\n        prevOut = (int16_t)s;\n\n        // Apply Envelope and Volume, then sum into the final mix\n        int32_t finalVol = (int32_t)(((uint32_t)(currentEnv \u003e\u003e 12) * volBase) \u003e\u003e 16);\n        mixBuffer[i] += (s * finalVol) \u003e\u003e 16;\n        \n        // Advance phase and envelope\n        ph += inc;\n        currentEnv += envStep;\n    }\n    \n    // Save state for the next render block\n    vo-\u003ephase = ph;\n    vo-\u003enoiseSample = prevOut; \n}\n\nvoid setup() {\n    synth.begin(2, SMODE_I2S, 4, 15, I2S_16BIT);\n    \n    // Bind Voice 0 to use your custom FM Sine function\n    synth.setCustomWave(0, waveFMSine);\n    synth.noteOn(0, c4, 255);\n    \n    // If you want to switch back to a standard saw wave later, just do:\n    // synth.setWave(0, WAVE_SAW);\n}\n```\n\n### C. The Maestro: Generative Algorithms (Custom Control)\nNeed something to control the synth but don't want to block your main `loop()` or deal with millis() timers? `setCustomControl` runs integrated tightly into the Synth's \"Control Rate\" (default 100Hz). Great for sequencers and generative music.\n\n```cpp\nuint32_t ticks = 0;\n\nvoid IRAM_ATTR theMaestroControl() {\n    ticks++;\n    \n    // An LFO controlling Voice 1's PulseWidth (PWM) automatically over time\n    uint8_t breathPWM = 128 + (sin(ticks * 0.05) * 110);\n    synth.setPulseWidth(1, breathPWM);\n\n    // Random generative Arpeggiator every 12 ticks\n    if (ticks % 12 == 0) {\n        if (random(0, 3) == 0) { \n            synth.noteOn(0, c5, random(50, 150)); \n        }\n    }\n}\n\nvoid setup() {\n    synth.begin(2, SMODE_I2S, 4, 15, I2S_32BIT);\n    // Bind the generative function to the control loop\n    synth.setCustomControl(theMaestroControl);\n}\n```\n\u003cbr\u003e\u003chr\u003e\u003cbr\u003e\n\n# 🇧🇷 Documentação em Português\n\n## 1. Visão Geral e Recursos Principais\n\nO **ESP32Synth** não é apenas um gerador de bipes; é uma engine completa de mixagem e síntese, construída \"bare-metal\" sobre o ESP-IDF com qualidade de estúdio e extrema eficiência de CPU.\n\n* **Polifonia Extrema:** Suporta confortavelmente **80 vozes simultâneas** de fábrica, com uma engine capaz de empurrar até **350+ vozes** se necessário.\n* **Acesso de Baixo Nível (NOVO):** Um poderoso sistema de *Hooks* (`setCustomDSP`, `setCustomWave`, `setCustomControl`) que permite injetar seus próprios algoritmos de efeitos (como Reverb, Delays) e geradores de ondas diretamente no loop de renderização I2S.\n* **Lo-Fi Engine (NOVO):** Controle nativo de *Bitcrush* e redução de profundidade de volume (Bit-depth) para criar timbres sujos, retrôs e no mais puro estilo Chiptune.\n* **Osciladores Flexíveis:** Senoidal, Triangular, Dente de Serra, Pulso (com PWM ajustável), Ruído (LCG rápido), *Wavetables*, Samplers de RAM e *Custom Waves*. Alterne entre qualquer tipo de onda instantaneamente em tempo de execução (`O(1)`).\n* **Streaming SD Desacoplado:** Toque até 4 arquivos WAV pesados simultaneamente. O cartão SD é gerenciado por uma task em segundo plano usando Ring Buffers, garantindo que o processamento de áudio principal nunca engasgue.\n* **Modulação Completa:** Envelopes ADSR independentes, LFOs (Vibrato, Tremolo), Portamento (Slides de pitch e volume absolutos) e um Arpejador integrado.\n\n---\n\n## 2. Como Funciona: Sob o Capô (Arquitetura Interna)\n\nPara alcançar uma polifonia massiva em um microcontrolador embarcado, funções lentas como matemática `float`, divisões (`/`) e ramificações complexas (cadeias de `if/else`) foram completamente extirpadas do caminho de áudio. Veja a mágica operando nos bastidores:\n\n### A. Os Limites do Silício (Escala de Polifonia)\nA biblioteca permite configurar `MAX_VOICES` direto no header. Veja como o hardware do ESP32 se comporta nessa escala:\n* **80 Vozes (Padrão):** O \"Sweet Spot\". O áudio é cristalino, o uso de RAM é baixo e a CPU tem muito tempo ocioso para gerenciar Wi-Fi, displays (LVGL) e sensores no Núcleo 0.\n* **140 Vozes (RAM Safe Max):** Ideal para reprodução pesada de trilhas MIDI completas. Consome uma boa fatia do Heap para as estruturas das vozes, mas mantém estabilidade absoluta.\n* **350 Vozes (Limite da Engine):** Empurrando o ESP32 ao limite absoluto da matemática. O áudio ainda renderiza sem falhas, mas o Núcleo 1 fica totalmente ocupado.\n* **364+ Vozes (O Abismo / Starvation):** Neste limite, o loop de renderização demora mais tempo para calcular um bloco de áudio do que o tempo físico para tocá-lo. O FreeRTOS fica sem \"ticks\" de CPU para gerenciar tarefas básicas do sistema. O resultado? Jitter no áudio, interrupção das tasks (Starvation) e pânicos no Watchdog Timer (WDT). *Isso prova a força bruta assustadora da biblioteca — mas respeite os limites da física!*\n\n### B. Matemática de Ponto Fixo (Fixed-Point 16.16) e Acumuladores 32-bits\n*Nunca* usamos `float` ou `double` durante a renderização. O sintetizador mapeia as frequências usando acumuladores de fase de 32-bits. Para controlar pitch e velocidades de leitura da memória, usamos aritmética **16.16 Fixed-Point**: 16 bits para a parte inteira e 16 bits para a fracionária. Multiplicações de volume utilizam castings pesados para `int64_t` temporários seguidos de bit-shifts (`\u003e\u003e 16`). O resultado é uma precisão matemática brutal e instantânea.\n\n### C. Core 1 Dedicado e IRAM_ATTR\nO `renderLoop()` roda em uma **FreeRTOS Task fixada no Núcleo 1** com prioridade máxima. As funções críticas de renderização possuem a flag `IRAM_ATTR`. Isso força o ESP32 a carregar o código na memória RAM interna ultra-rápida, contornando a latência absurda de buscar instruções na memória Flash (Cache Misses).\n\n### D. A Separação: Audio Rate vs Control Rate\nNão faz sentido calcular o envelope (ADSR) ou o LFO (Vibrato) 48.000 vezes por segundo. \n1.  **Audio Rate (48kHz):** Processa apenas osciladores, incrementos de fase e somas de buffer.\n2.  **Control Rate (100Hz Padrão):** Acorda apenas a cada ~480 amostras de áudio para recalcular os slides de pitch, avançar as fases dos LFOs e atualizar a máquina de estados dos Envelopes.\n\n### E. Mixagem Segura (Headroom)\nSomar dezenas de vozes de 16-bits causaria clipagem (distorção) imediata. Para evitar isso, o buffer interno de mixagem (`mixBuffer`) usa números inteiros de **32-bits**. As vozes são somadas livremente com um Headroom gigantesco. Apenas no último estágio de saída, o sinal é escalado pelo Volume Master, processado pelo Bitcrusher (se ativo) e rebaixado limpidamente para caber na saída 16-bit ou 32-bit do hardware I2S.\n\n---\n\n## 3. Requisitos e Instalação\n\n* **Hardware:** ESP32 Clássico ou ESP32-S3 (Dual Core a 240MHz). *Variantes Single-Core (S2, C3) não são recomendadas devido à colisão de tasks no RTOS.*\n* **DAC I2S Externo (Altamente Recomendado):** Módulos como o **PCM5102A** garantem qualidade de estúdio (16 ou 32-bit). O DAC interno do ESP32 tem muito ruído de fundo e é limitado a 8 bits.\n* **Instalação:** \n  1. Baixe como arquivo `.ZIP` ou pesquise por \"ESP32Synth\" no Library Manager do Arduino IDE.\n  2. *Requer ESP32 Board Core versão 3.0.0 ou superior.*\n\n---\n\n## 4. Guia Definitivo da API\n\nAqui explicaremos como dominar o poder absoluto da engine do ESP32Synth.\n\n### Inicialização e Configuração Global\n```cpp\nESP32Synth synth;\n\nvoid setup() {\n    // Inicializa I2S no formato 32-bit. \n    // Pinos recomendados (ex: PCM5102A): BCK=4, WS=15, DATA=2\n    synth.begin(2, SMODE_I2S, 4, 15, I2S_32BIT);\n    \n    // Taxa de recálculo dos envelopes e LFOs (padrão 100Hz)\n    // Valores maiores deixam slides mais suaves, mas usam um pouco mais de CPU.\n    synth.setControlRateHz(200);\n\n    // [NOVO] Reduz a qualidade do áudio master para 8-bits (Efeito Lo-Fi rasgado)\n    synth.setMasterBitcrush(8);\n\n    // [NOVO] Reduz a resolução base de cálculos de volume dinâmico interno\n    synth.setVolDepthBase(8);\n\n    // O volume é uint16_t. Com VolDepthBase em 8 (padrão), o máximo é 255.\n    synth.setMasterVolume(255); \n}\n```\n\n### Acionando Notas e Alternância de Ondas Nativa\nA biblioteca entende notas usando \"CentiHz\" (Hz * 100). Inclua `ESP32SynthNotes.h` para usar constantes como `c4` (Dó 4 = 261.63 Hz). Graças à arquitetura de *Jump Tables* `O(1)`, você pode **alternar os tipos de onda instantaneamente**, inclusive enquanto a nota está tocando, sem estalos ou vazamento de memória!\n\n```cpp\n// Voz 0, Dó 4 (c4), Volume 255\nsynth.noteOn(0, c4, 255);\n\n// Muda a onda da Voz 0 para Quadrada/Pulso AO VIVO!\nsynth.setWave(0, WAVE_PULSE);\n\n// Muda a largura do pulso (PWM) da onda quadrada (0 a 255)\nsynth.setPulseWidth(0, 128); // 128 = 50% = Quadrada perfeita\n\n// Solta a tecla (inicia a fase de Release do envelope ADSR)\nsynth.noteOff(0);\n```\n\n### Deslizando Pitch e Volume (Portamento)\nOs slides usam o algoritmo de linha de Bresenham para uma precisão absoluta em números inteiros, movendo-se graciosamente sem depender de matemática pesada com floats.\n\n```cpp\nsynth.noteOn(0, c4, 255);\n// Desliza do Dó 4 para o Dó 5 em exatamente 1000 milissegundos\nsynth.slideFreqTo(0, c5, 1000);\n\n// Reduz o volume gradualmente para zero ao longo de 2 segundos\nsynth.slideVolTo(0, 0, 2000);\n```\n\n### Configurando o ADSR (Envelope)\nAttack, Decay e Release são definidos em milissegundos. Sustain é uma amplitude.\n```cpp\n// Voz 0 | Attack: 10ms | Decay: 300ms | Sustain Nível: 127 | Release: 1500ms\nsynth.setEnv(0, 10, 300, 127, 1500);\n```\n\n### Streaming Direto de Arquivo WAV (Cartão SD)\nO streaming de arquivos WAV pesados não vai fazer o seu sintetizador travar, mas requer que o seu módulo SD seja inicializado com alta velocidade (16MHz a 20MHz) nos pinos SPI físicos.\n```cpp\n#include \u003cSD.h\u003e\n#include \u003cSPI.h\u003e\n\nvoid setup() {\n    SPI.begin(18, 19, 23, 5);\n    SD.begin(5, SPI, 16000000); // 16MHz é VITAL para a estabilidade do áudio!\n\n    // Reprodução: Voz 1, Caminho, Volume (255), Pitch (c4 mantém a original), Loop (true)\n    synth.playStream(1, SD, \"/loop_bateria.wav\", 255, c4, true);\n\n    // Pula para a marca de 5 segundos (5000 ms) do áudio\n    synth.seekStreamMs(1, 5000);\n}\n```\n## 5. DSP Avançado e Ondas Customizadas\n\nCom a versão 2.4.0, o **ESP32Synth** abriu seu núcleo de processamento para você injetar código de alta performance.\n\n### A. Efeitos Globais Master (Reverb, Delay)\nUsando `setCustomDSP`, você intercepta o buffer de mixagem final de 32-bits ANTES dele ser enviado para o DAC. \n**Atenção:** Este código roda 48.000 vezes por segundo. Ele precisa ser brutalmente otimizado! Não use o operador de módulo (`%`) para dar a volta em buffers e fuja dos `floats`.\n\nAqui está um exemplo prático de um **Reverb de Fita Profissional com Saturação Analógica e DC Blocker**:\n\n```cpp\n#define TAPE_LEN 20000 \nint32_t* reverbTape;\nint writeHead = 0;\n\nint32_t dcBlockerPrevWet = 0;\nint32_t dcBlockerState = 0;\nint32_t lpState = 0; \n\n// IRAM_ATTR força esta função para a memória RAM ultra-rápida!\nvoid IRAM_ATTR reverbDSP(int32_t* mixBuffer, int numSamples) {\n    if (!reverbTape) return; \n\n    for (int i = 0; i \u003c numSamples; i++) {\n        int32_t dry = mixBuffer[i];\n\n        // 1. Leituras Seguras do Buffer Circular (Sem usar o operador lento '%')\n        // Escolhemos tempos primos menores que TAPE_LEN (20000) para evitar ressonâncias fixas.\n        int tap1 = writeHead - 4327;  if (tap1 \u003c 0) tap1 += TAPE_LEN;\n        int tap2 = writeHead - 11003; if (tap2 \u003c 0) tap2 += TAPE_LEN;\n        int tap3 = writeHead - 19013; if (tap3 \u003c 0) tap3 += TAPE_LEN;\n\n        // Soma as 3 cabeças e divide por 4 (\u003e\u003e 2) para não clipar o sinal\n        int32_t wet = (reverbTape[tap1] \u003e\u003e 2) + (reverbTape[tap2] \u003e\u003e 2) + (reverbTape[tap3] \u003e\u003e 2);\n\n        // 2. DC Blocker (Filtro Passa-Alta Crucial!)\n        // Isso mata qualquer energia parada de baixa frequência que causaria um loop infinito de ruído.\n        int32_t dcBlocked = wet - dcBlockerPrevWet + ((dcBlockerState * 253) \u003e\u003e 8);\n        dcBlockerPrevWet = wet;\n        dcBlockerState = dcBlocked;\n\n        // 3. Filtro Low-Pass (Deixa as repetições cada vez mais abafadas e quentes)\n        lpState = ((dcBlocked * 50) + (lpState * 206)) \u003e\u003e 8; \n\n        // 4. Calcula o Feedback para gravar na fita (~78% de feedback)\n        int32_t feedback = (dry \u003e\u003e 1) + ((lpState * 200) \u003e\u003e 8);\n\n        // 5. SATURAÇÃO ANALÓGICA (Soft-Clipping de segurança)\n        // Se a matemática tentar explodir (microfonias), esmagamos o som no limite do 16-bit.\n        if (feedback \u003e 32767) feedback = 32767;\n        else if (feedback \u003c -32768) feedback = -32768;\n\n        // Grava na fita e avança a cabeça de leitura/gravação\n        reverbTape[writeHead] = feedback;\n        writeHead++;\n        if (writeHead \u003e= TAPE_LEN) writeHead = 0;\n\n        // Mixagem Master: Sinal limpo (Dry) + Cauda do Reverb (Wet)\n        mixBuffer[i] = dry + lpState;\n    }\n}\n\nvoid setup() {\n    // Aloca a memória da \"fita\" na RAM (Heap) com segurança\n    reverbTape = (int32_t*)heap_caps_calloc(TAPE_LEN, sizeof(int32_t), MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);\n    synth.begin(2, SMODE_I2S, 4, 15, I2S_32BIT);\n    \n    // INJETA O REVERB NA ENGINE!\n    if (reverbTape) synth.setCustomDSP(reverbDSP);\n}\n```\n\n### B. Escrevendo seu próprio Oscilador (Custom Waves)\nSe as ondas padrão não forem suficientes, crie sua própria matemática sônica usando `setCustomWave()`. E graças à nossa arquitetura de *Jump Table*, **você pode voltar a uma onda normal a qualquer instante** usando `setWave(voz, WAVE_SINE)`. A engine lidará com a transição perfeitamente, sem falhas ou estalos.\n\nAbaixo, um exemplo de uma **Onda Senoidal com Modulação de Fase por Feedback (FM)**. A onda modula sua própria fase com base no resultado anterior, criando a mesma ressonância metálica riquíssima encontrada em sintetizadores FM clássicos (como o Yamaha DX7).\n\n```cpp\nvoid IRAM_ATTR waveFMSine(Voice* vo, int32_t* mixBuffer, int samples, int32_t startEnv, int32_t envStep) {\n    int32_t currentEnv = startEnv;\n    int32_t volBase = ((uint32_t)vo-\u003evol * vo-\u003etrmModGain) \u003e\u003e 8;\n    uint32_t ph = vo-\u003ephase;\n    uint32_t inc = vo-\u003ephaseInc + vo-\u003evibOffset;\n    \n    // Reaproveitamos a variável 'noiseSample' da struct para armazenar nosso último output\n    int16_t prevOut = vo-\u003enoiseSample; \n\n    for (int i = 0; i \u003c samples; i++) {\n        // A MÁGICA: Modulação de fase via feedback.\n        // Com o shift em 15, o feedback torce a onda em ~12%. \n        // Esse é o Ponto de Ressonância Musical perfeito antes de virar ruído!\n        uint32_t modPh = ph + ((int32_t)prevOut \u003c\u003c 15); \n        \n        // Busca o valor na Look-up Table Senoidal de alta velocidade da engine\n        int32_t s = sineLUT[(modPh \u003e\u003e SINE_SHIFT) \u0026 SINE_LUT_MASK] \u003e\u003e 16;\n        prevOut = (int16_t)s;\n\n        // Aplica Envelope e Volume, e soma na mixagem final\n        int32_t finalVol = (int32_t)(((uint32_t)(currentEnv \u003e\u003e 12) * volBase) \u003e\u003e 16);\n        mixBuffer[i] += (s * finalVol) \u003e\u003e 16;\n        \n        // Avança fase e envelope\n        ph += inc;\n        currentEnv += envStep;\n    }\n    \n    // Salva o estado para o próximo bloco de renderização\n    vo-\u003ephase = ph;\n    vo-\u003enoiseSample = prevOut; \n}\n\nvoid setup() {\n    synth.begin(2, SMODE_I2S, 4, 15, I2S_16BIT);\n    \n    // Associa a Voz 0 para usar a sua função Senoidal FM\n    synth.setCustomWave(0, waveFMSine);\n    synth.noteOn(0, c4, 255);\n    \n    // Se quiser voltar para uma onda dente de serra normal depois, basta:\n    // synth.setWave(0, WAVE_SAW);\n}\n```\n\n### C. O Maestro: Algoritmos Gerativos (Custom Control)\nPrecisa de algo controlando o sintetizador, mas não quer bloquear seu `loop()` principal nem brigar com *timers*? O `setCustomControl` roda integrado de forma inteligente ao \"Control Rate\" da engine (padrão 100Hz). Excelente para sequenciadores e geração procedural de música.\n\n```cpp\nuint32_t ticks = 0;\n\nvoid IRAM_ATTR theMaestroControl() {\n    ticks++;\n    \n    // Um LFO automático controlando o PulseWidth (PWM) da Voz 1 no decorrer do tempo\n    uint8_t breathPWM = 128 + (sin(ticks * 0.05) * 110);\n    synth.setPulseWidth(1, breathPWM);\n\n    // Arpejador Aleatório gerativo disparando a cada 12 ticks\n    if (ticks % 12 == 0) {\n        if (random(0, 3) == 0) { \n            synth.noteOn(0, c5, random(50, 150)); \n        }\n    }\n}\n\nvoid setup() {\n    synth.begin(2, SMODE_I2S, 4, 15, I2S_32BIT);\n    // Associa o Maestro ao loop de controle\n    synth.setCustomControl(theMaestroControl);\n}\n```\n\n---\n\n## 🛠 Tools / Ferramentas\n\nDentro da pasta `tools/` deste repositório, você encontra scripts Python projetados para acelerar seu fluxo de trabalho:\n*   `WavetableMaker.py`: Cria *wavetables* perfeitas a partir de equações matemáticas ou áudios e as converte diretamente em arrays C/C++ (`.h`).\n*   `WavToEsp32SynthConverter.py`: Prepara arquivos de áudio externos, aplicando algoritmos inteligentes de downsampling e compressão. Transforma tudo em `.h` para a funcionalidade `WAVE_SAMPLE`, permitindo tocar amostras curtas na velocidade brutal da RAM, poupando a lentidão do cartão SD.\n\n---\n\n## ⚠️ Common Troubleshooting (Solução de Problemas Comuns)\n\n*   **Pops, cliques, engasgos ou áudio robótico:** Tem certeza de que você não colocou um `delay()` gigantesco no seu `loop()`, asfixiando os processos básicos do Core 0? Confirme também se a placa no Arduino IDE está devidamente configurada para rodar a CPU a **240MHz**.\n*   **SD Stream engasgando ou \"Falha de Leitura\":** O barramento SPI do seu Arduino está lento demais. Inicialize o SD Card sempre forçando a velocidade: `SD.begin(5, SPI, 16000000)`. Para streams, o formato ideal do cartão SD é **FAT32** com *cluster size* de 32kb ou 64kb. *Nota: A biblioteca nativa do ESP não lê cartões SDXC (64GB+) no formato exFAT por padrão.*\n*   **Ruído estático constante sem tocar notas:** Típico do DAC interno do ESP32 (que é notoriamente ruim) ou fiação I2S mal isolada. Se usar um módulo como o PCM5102, garanta que ele possua o **GND** firmemente conectado ao terra do ESP32.\n*   **Módulo reiniciando do nada (Deadlock):** Ao desligar o synth usando `synth.end()`, aguarde cerca de 50ms (via `vTaskDelay`) antes de destruir ou reiniciar instâncias pesadas. O RTOS do ESP-IDF necessita de alguns ciclos livres para soltar os *mutexes* das tasks I2S com segurança. A biblioteca trata a maior parte disso, mas seja cauteloso ao desconectar pinos em tempo de execução.\n\n---\n\u003cp align=\"center\"\u003e\u003ci\u003eConstruído com paixão, muito café e matemática pesadamente otimizada. ❤️\u003c/i\u003e\u003c/p\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanilogcrf2-oss%2Fesp32synth","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdanilogcrf2-oss%2Fesp32synth","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanilogcrf2-oss%2Fesp32synth/lists"}