Back to Home

Creating a runtime addon with WASM for a music player

Detailed breakdown of creating the ChromaSync runtime addon for a music player: from theme restoration to implementing WASM core for audio-reactive effects. Covers architecture, settings system, optimization, and future development directions.

Runtime addon with WASM: how I rebuilt the architecture for audio-reactive effects
Advertisement 728x90

# Turning a Music Player Theme into a Runtime Add-on with WASM and Audio-Reactive Effects

The project started as an attempt to fix a broken theme for a popular Electron music player but evolved into a full-fledged runtime add-on called ChromaSync—complete with dynamic color adaptation, audio-reactive pulsing, Zen Mode, and a WebAssembly computational core. All of this runs on top of PulseSync—a platform for customizing the interface via JS/CSS—where the author hit the framework's limitations and needed to scale logic beyond CSS.

Architecture: From Theme to Orchestrator

ChromaSync was originally conceived as a visual patch but quickly grew into a multilayered system. Key components:

  • metadata.json — entry point for PulseSync;
  • handleEvents.json — declarative settings description;
  • script.js — central orchestrator for state and effects;
  • style.css — base styles and animations;
  • WASM module — core for heavy computations (pulsing, smoothing);
  • Backend layer — optional networking features.

The PulseSync → handleEvents → script.js → DOM/CSS/WASM pipeline enables dynamic changes without reloads. For example, when switching tracks, the script extracts dominant colors from the album art, passes them to CSS variables, and simultaneously kicks off audio-reactive pulsing via the WASM core. All in real time, with no noticeable lag.

Google AdInline article slot

Settings System: 70 Parameters Under Control

PulseSync lets users tweak add-ons without touching code. Each setting is defined in a JSON structure with a required id that links it to runtime logic. Example config for background transition mode:

{
  "id": "backgroundTrackTransitionMode",
  "name": "Withmena fona (trek)",
  "description": "Effekt switching fonovoy oblozhki when smene treka: fade (plavnaya podmena) or slide (staraya uezzhaet vlevo, novaya vezzhaet right).",
  "type": "selector",
  "selected": "1",
  "options": [
      {
          "name": "Fade (by umolchaniyu)",
          "id": "fade"
      },
      {
          "name": "Slide (left/right)",
          "id": "slide-horizontal"
      }
  ],
  "defaultParameter": "fade"
}

With 70 such parameters, state normalization becomes essential. script.js handles this: it gathers values, validates them, converts to a unified state object, and applies to the UI. Without this layer, the system would be unmanageable—especially with interdependent settings, like Zen Mode auto-disabling particles and toning down pulsing intensity.

Evolution of script.js: From 700 Lines to a Monolith

script.js started as a simple 700-line file. Now it's over 8,000 lines—not bloat, but coordination of ever-growing subsystems. It acts as the orchestrator:

Google AdInline article slot
  • Fetches and normalizes settings from PulseSync;
  • Tracks player state (track, progress, fullscreen);
  • Manages DOM elements and CSS variables;
  • Starts/stops audio-reactive effects;
  • Calls WASM functions for math-heavy calculations;
  • Handles mouse/keyboard events for parallax and interactivity.

Early example: background parallax based on mouse movement:

const handleParallax = (event) => {
    const offsetX = event.clientX / window.innerWidth - 0.5;
    const offsetY = event.clientY / window.innerHeight - 0.5;
    const posX = `calc(50% + ${offsetX * 25}px)`;
    const posY = `calc(50% + ${offsetY * 25}px)`;

    requestAnimationFrame(() => {
        bgLayer1.style.backgroundPosition = `${posX} ${posY}`;
        bgLayer2.style.backgroundPosition = `${posX} ${posY}`;
    });
};

Later, more advanced features emerged—like dynamically extracting color palettes from track artwork:

const applyChameleonStyles = (palette, sourceChoice) => {
    if (settings.useCustomAccentColor?.value) {
        document.documentElement.style.setProperty(
            '--accent-color',
            settings.customAccentColor?.value || '#8a63b3'
        );
        return;
    }

    const swatch = palette?.[sourceChoice] || palette?.Vibrant;
    if (!swatch) return;
    document.documentElement.style.setProperty('--accent-color', swatch.getHex());
};

WASM: When JavaScript Can't Keep Up

The turning point came when pulsing evolved from simple animation to a compute-intensive system with multiple modes, smoothing, and triggers. JavaScript couldn't handle the performance demands—especially on low-end devices. Solution: move the core to Rust/WASM.

Google AdInline article slot

Module loading:

async function loadPulseWasm() {
    const res = await fetch('/api/chromasync/rust', { cache: 'no-store' });
    const buf = await res.arrayBuffer();
    const { instance } = await WebAssembly.instantiate(buf);
    return instance.exports;
}

Example exported Rust function:

#[no_mangle]
pub extern "C" fn c2(prev: f32, current: f32, weight: f32) -> f32 {
    prev * weight + current * (1.0 - weight)
}

WASM handles:

  • Real-time track amplitude calculation;
  • Smooth frame-to-frame transitions;
  • Animation frame rate optimization;
  • Isolating heavy logic from the main UI thread.

Result: steady 60 FPS even with active effects and complex filters like pixelation or noise.

Key Takeaways

  • PulseSync — platform for turning themes into full runtime add-ons without access to the player's source code.
  • WASM is crucial for audio-reactive effects: delivers performance pure JavaScript can't match.
  • script.js — more than a script; it's the orchestrator tying together settings, DOM, CSS, and compute core.
  • Dynamic color adaptation from track artwork uses runtime image analysis and CSS variables.
  • The architecture scales: new effects add as independent modules without breaking existing logic.

Optimization: Beauty Shouldn't Cost FPS

Every new effect risks performance dips. The author focuses heavily on profiling and optimization:

  • requestAnimationFrame over setTimeout for animations;
  • Debouncing mouse and scroll events;
  • Lazy-loading heavy modules (e.g., WASM only on pulse activation);
  • Disabling unused effects in background;
  • Caching album art analysis results.

Testing shows CPU usage stays under 8–12% on average laptops even with all effects on (pulsing, particles, parallax, filters). This keeps the UI responsive and animations buttery smooth.

The Future: Where to Go Next

The author is eyeing several development directions:

  • Integration with system APIs for volume and media key control;
  • User shader support via WebGL;
  • Cloud export/sync for settings across devices;
  • Auto-adapting effects to music genre (e.g., aggressive pulsing for EDM, smooth transitions for jazz).

Core principle: no features for features' sake. Every addition must solve a real user problem or boost experience without sacrificing performance.

— Editorial Team

Advertisement 728x90

Read Next