pixelsncodes/lumen — Lumen – A Photograph-to-Sound Synthesizer
Transcript
QuickFacts
Welcome! Today we're exploring Lumen, a wavetable synthesizer built by pixelsncodes. This isn't your typical synth—its signature feature is the ability to drop a photograph onto the window and instantly turn it into a playable sound. Let's see how it all works under the hood.
PlainEnglish
So what does Lumen actually do? It's a wavetable synth that lets you drag a photo into the window, and the image becomes the sound source. The conversion is fully deterministic—no uploads, no randomness—same photo always generates the exact same wavetable. Under the hood you've got a 16-voice polyphonic engine with dual oscillators, filters, three envelopes, and a full effects chain. It's built with JUCE 8 and ships as a VST3 plugin plus a Windows standalone app.
QuickFacts
The tech foundation here is JUCE 8, the industry-standard audio plugin framework. It provides the VST3 hosting, parameter trees, FFT and reverb primitives, even image decoding. The codebase is modern C++20 compiled with MSVC and strict warnings. State is stored in JUCE ValueTrees—every parameter, every preset, even the image frames and thumbnails live in that tree. There's a submodule called Lumena that generates melodies from images. And the build uses CMake with a headless render harness for automated testing and verification.
Architecture
The architecture is layered cleanly. At the top, the VST3 host sends MIDI and audio buffers into the PluginProcessor, which acts as the bridge. The processor owns the SynthEngine—that's the pure DSP core with 16 voices, oscillators, and effects. It also owns the LensController, which decodes images and swaps wavetables into the engine atomically, and the MelodyController, which generates note sequences from the image. Everything persists through JUCE's ValueTree—the Audio Parameter Value Tree State. Parameters, presets, mod matrix, even the image bytes all live in that tree.
Architecture
Let's trace what happens when you drop a photo. The user drags a PNG or JPEG onto the Lens panel. LensController decodes the image and reads the encoded bytes. It calls analyzeImage on the LensEngine, which downscales the image to working size, hashes the pixels to produce a deterministic seed, and then builds 64 wavetable frames—reading each row as luma values that become a bipolar waveform. That 64 by 2048 float array comes back, and LensController builds a wavetable with 10 mip levels to prevent aliasing. It swaps the new table into the SynthEngine with an atomic pointer write—no locks, no waiting. The frames, a thumbnail, and the original source bytes all get persisted into the ValueTree so the image survives preset saves. Finally, the old wavetable is retired with a render-counter fence—it's only freed after the audio thread has moved past the swap by at least two render calls.
Architecture
Here's the module structure. The Source directory holds the core. Engine is the pure DSP layer—it compiles with zero GUI dependencies. Inside you'll find SynthEngine managing voices and modulation, Voice handling oscillators and filters, Wavetable storing frames with mip levels, and FxChain running the effects. The Lens directory is the image conversion subsystem—LensEngine does the pixel-to-waveform math, LensController handles drops and atomic swaps. State manages everything persistent—Parameters defines the 160-parameter layout, ModState handles the modulation matrix, and PresetManager scans factory and user presets. PluginProcessor ties it all together as the JUCE bridge. And Tools provides verification—a headless render harness for determinism checks and a full unit test suite.
Architecture
The modulation system is really elegant. You have 16 sources—three envelopes, three LFOs that can run per-voice or globally, four macro knobs, plus velocity, mod wheel, aftertouch, pitch bend, key number, and random. All of those feed into a 24-slot matrix. Each slot routes one source to one destination with a bipolar depth. The matrix works in normalized zero-to-one space—it sums all the depths times sources, clamps the result, and lands it on 20-millisecond smoothers. Those smoothers prevent clicks and write the final values to the 160 parameters—oscillator morph, filter cutoff, envelope curves, effect mix, you name it. It's a classic modular-synth patching model, but lock-free and thread-safe.
Architecture
Real-time safety is baked into the design. The message thread does all the heavy lifting—decoding images, building wavetables with FFT mip levels, parsing the ValueTree into plain-old-data config structs. It writes those to atomic pointers. The audio thread just reads those pointers with acquire semantics—no locks, no waiting. The process-block path allocates zero memory. All buffers are pre-sized in prepare-to-play, including delay lines and reverb scratch space. Communication between threads uses atomics and lock-free FIFOs. And retired wavetables are freed only after the audio thread's render counter has advanced by at least two, guaranteeing no concurrent access. It's a lock-free, wait-free design end to end.
Community
Looking at the community side, Lumen is actively maintained. The last commit was just four days ago on July seventeenth. Over the past year there's been one commit, and it's a solo project by Kazi Ahmed. There are two branches, no tags yet. It's early days—this feels like a focused personal project still in active development. The code is fresh, the architecture is thoughtful, and the author is clearly iterating.
PlainEnglish
And that's Lumen. It's a beautiful blend of audio DSP and image processing—turning photos into wavetables with a deterministic, lock-free architecture. The code is clean, the design is thoughtful, and the creative possibilities are pretty exciting. Thanks for exploring it with me!
How this was made
Lenzon read pixelsncodes/lumen and generated this walkthrough automatically. The narration above is the transcript of what it says.
Explain a pull request from your own repo
Point Lenzon at a repo or a pull request and get a narrated walkthrough like this one.
Try it