Powrót do strony głównej

Tworzenie dodatku runtime z WASM dla odtwarzacza muzycznego

Szczegółowy rozbiór tworzenia dodatku runtime ChromaSync dla odtwarzacza muzycznego: od przywrócenia motywu do implementacji rdzenia WASM dla efektów audio-reaktywnych. Obejmuje architekturę, system ustawień, optymalizację i przyszłe kierunki rozwoju.

Dodatek runtime z WASM: jak przebudowałem architekturę pod efekty audio-reaktywne
Advertisement 728x90

# Jak przekształcić motyw dla odtwarzacza muzycznego w dodatek runtime z WASM i efektami reagującymi na audio

Projekt rozpoczął się jako próba naprawy uszkodzonego motywu dla popularnego odtwarzacza Electron, a zakończył stworzeniem pełnoprawnego dodatku runtime ChromaSync — z dynamiczną adaptacją kolorów, pulsacją reagującą na audio, trybem Zen i rdzeniem obliczeniowym opartym na WebAssembly. Wszystko to działa na bazie PulseSync — platformy do modyfikacji interfejsu za pomocą JS/CSS, gdzie autor napotkał ograniczenia frameworka i konieczność skalowania logiki poza CSS.

Architektura: od motywu do orkiestratora

ChromaSync początkowo miał być wizualną łatką, ale szybko przerodził się w wielowarstwowy system. Kluczowe komponenty:

  • metadata.json — punkt wejścia dla PulseSync;
  • handleEvents.json — deklaratywne opisanie ustawień;
  • script.js — centralny orkiestrator stanu i efektów;
  • style.css — podstawowe style i animacje;
  • moduł WASM — rdzeń do ciężkich obliczeń (pulsacja, wygładzanie);
  • warstwa backendowa — opcjonalne funkcje sieciowe.

Połączenie PulseSync → handleEvents → script.js → DOM/CSS/WASM pozwala na dynamiczne stosowanie zmian bez przeładowania. Na przykład przy zmianie utworu skrypt wyciąga dominujące kolory z okładki, przekazuje je do zmiennych CSS i jednocześnie uruchamia pulsację reagującą na audio za pośrednictwem rdzenia WASM. Wszystko to dzieje się w czasie rzeczywistym, bez zauważalnych opóźnień.

Google AdInline article slot

System ustawień: 70 parametrów pod kontrolą

PulseSync pozwala użytkownikom konfigurować dodatki bez edycji kodu. Każde ustawienie jest opisane w strukturze JSON z obowiązkowym id, za pomocą którego łączy się z logiką runtime. Przykładowa konfiguracja trybu zmiany tła:

{
  "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"
}

Gdy takich parametrów jest 70, pojawia się potrzeba normalizacji stanu. script.js przejmuje to zadanie: zbiera wartości, weryfikuje je, przekształca w jednolity obiekt stanu i stosuje do interfejsu. Bez tej warstwy system stałby się nie do opanowania — zwłaszcza przy współzależnych ustawieniach, na przykład gdy włączenie „Zen Mode” automatycznie wyłącza cząstki i obniża intensywność pulsacji.

Ewolucja script.js: od 700 do ponad 8000 linii

Początkowo script.js był prostym plikiem liczącym 700 linii. Dziś przekroczył 8000 linii — nie z powodu nadęcia, lecz potrzeby koordynacji coraz większej liczby podsystemów. Ten plik pełni rolę orkiestratora:

Google AdInline article slot
  • Odbiera i normalizuje ustawienia z PulseSync;
  • Śledzi stan odtwarzacza (utwór, postęp, pełny ekran);
  • Zarządza elementami DOM i zmiennymi CSS;
  • Uruchamia i zatrzymuje efekty reagujące na audio;
  • Wywołuje funkcje WASM do obliczeń matematycznych;
  • Obsługuje zdarzenia myszy i klawiatury dla paralaksy i interaktywności.

Przykładowa wczesna logika — paralaksa tła na podstawie ruchu myszy:

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}`;
    });
};

Później pojawiły się bardziej złożone funkcje — na przykład dynamiczne wyciąganie palety kolorów z okładki utworu:

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: kiedy JavaScript już nie wystarcza

Przełomowy moment nastąpił, gdy pulsacja przestała być prostą animacją i stała się systemem obciążonym obliczeniowo z wieloma trybami, wygładzaniem i wyzwalaczami. JavaScript nie radził sobie z wymaganą wydajnością — zwłaszcza na słabszych urządzeniach. Rozwiązanie — przeniesienie rdzenia do Rust/WASM.

Google AdInline article slot

Ładowanie modułu:

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;
}

Przykładowa eksportowana funkcja w Rust:

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

WASM jest używany do:

  • Obliczania amplitudy ścieżki dźwiękowej w czasie rzeczywistym;
  • Płynnego wygładzania przejść między klatkami;
  • Optymalizacji częstotliwości wywołań klatek animacji;
  • Izolacji ciężkiej logiki od głównego wątku UI.

Rezultat — stabilne 60 FPS nawet przy aktywnych efektach i złożonych filtrach, takich jak pikselizacja czy szum.

Co ważne

  • PulseSync — platforma umożliwiająca przekształcanie motywów w pełnoprawne dodatki runtime bez dostępu do kodu źródłowego odtwarzacza.
  • WASM jest kluczowy dla efektów reagujących na audio: zapewnia wydajność niedostępną w czystym JavaScript.
  • script.js — to nie zwykły skrypt, lecz orkiestrator łączący ustawienia, DOM, CSS i rdzeń obliczeniowy.
  • Dynamiczna adaptacja kolorów z okładki utworu realizowana jest poprzez analizę obrazu w runtime i wstrzykiwanie zmiennych CSS.
  • Architektura projektu pozwala na skalowanie: nowe efekty dodawane są jako niezależne moduły, bez naruszania istniejącej logiki.

Optymalizacja: piękno nie może kosztować FPS

Każdy nowy efekt to ryzyko spadku wydajności. Autor kładzie szczególny nacisk na profilowanie i optymalizację:

  • Użycie requestAnimationFrame zamiast setTimeout do animacji;
  • Debouncing zdarzeń myszy i przewijania;
  • Leniva inicjalizacja ciężkich modułów (np. WASM ładowany tylko przy aktywacji pulsacji);
  • Wyłączanie nieużywanych efektów w trybie tła;
  • Buforowanie wyników analizy okładek utworów.

Testy wykazały, że nawet przy włączonych wszystkich efektach (pulsacja, cząstki, paralaksa, filtry) zużycie CPU nie przekracza 8–12% na średnim laptopie. Dzięki temu interfejs pozostał responsywny, a animacje płynne.

Przyszłość: w jakim kierunku iść

Autor rozważa kilka kierunków rozwoju:

  • Integracja z systemowymi API do sterowania głośnością i klawiszami multimedialnymi;
  • Obsługa niestandardowych shaderów poprzez WebGL;
  • Eksport ustawień do chmury i synchronizacja między urządzeniami;
  • Automatyczna adaptacja efektów do gatunku muzyki (np. bardziej agresywna pulsacja dla elektroniki, płynne przejścia dla jazzu).

Główna zasada — nie dodawać funkcji dla samej funkcji. Każdy nowy element musi rozwiązywać konkretny problem użytkownika lub znacząco poprawiać doświadczenie bez szkody dla wydajności.

— Editorial Team

Advertisement 728x90

Czytaj dalej