<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nagisa Dozono</title>
    <description>The latest articles on DEV Community by Nagisa Dozono (@7g3n).</description>
    <link>https://dev.to/7g3n</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3984466%2F5c287cd7-f9c8-4efc-bf2c-bba9a03fae38.jpeg</url>
      <title>DEV Community: Nagisa Dozono</title>
      <link>https://dev.to/7g3n</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/7g3n"/>
    <language>en</language>
    <item>
      <title>How I Built an Audio-Reactive 3D Visualizer with Three.js and the Web Audio API</title>
      <dc:creator>Nagisa Dozono</dc:creator>
      <pubDate>Sun, 14 Jun 2026 23:25:42 +0000</pubDate>
      <link>https://dev.to/7g3n/how-i-built-an-audio-reactive-3d-visualizer-with-threejs-and-the-web-audio-api-6an</link>
      <guid>https://dev.to/7g3n/how-i-built-an-audio-reactive-3d-visualizer-with-threejs-and-the-web-audio-api-6an</guid>
      <description>&lt;p&gt;I recently released &lt;strong&gt;Audio Reactive 3D Visualizer v1.0.0&lt;/strong&gt;, an open-source browser application that turns uploaded music and artwork into real-time visuals.&lt;/p&gt;

&lt;p&gt;Users can generate synchronized 3D objects, particles, waveforms, and image effects, then export the result as a &lt;strong&gt;1920×1080 MP4 at 30 FPS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The complete media-processing workflow runs locally in the browser.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://waveform.tranjectories.xyz/" rel="noopener noreferrer"&gt;https://waveform.tranjectories.xyz/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/7g3n/phase-viz" rel="noopener noreferrer"&gt;https://github.com/7g3n/phase-viz&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v1.0.0 release:&lt;/strong&gt; &lt;a href="https://github.com/7g3n/phase-viz/releases/tag/v1.0.0" rel="noopener noreferrer"&gt;https://github.com/7g3n/phase-viz/releases/tag/v1.0.0&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1y3k203r0swnk7r3ghrj.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1y3k203r0swnk7r3ghrj.webp" alt="Audio Reactive 3D Visualizer interface" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, I will explain the main ideas behind the project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Decoding audio with the Web Audio API&lt;/li&gt;
&lt;li&gt;Converting frequency data into stable visual signals&lt;/li&gt;
&lt;li&gt;Mapping audio data to Three.js scenes&lt;/li&gt;
&lt;li&gt;Creating audio-reactive particle systems&lt;/li&gt;
&lt;li&gt;Separating real-time playback from video export&lt;/li&gt;
&lt;li&gt;Exporting MP4 files with WebCodecs&lt;/li&gt;
&lt;li&gt;Falling back to ffmpeg.wasm&lt;/li&gt;
&lt;li&gt;Processing uploaded media locally&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;The code examples below are simplified to explain the architecture. The complete implementation is available in the GitHub repository.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;I work as both a frontend developer and a music producer.&lt;/p&gt;

&lt;p&gt;Independent musicians often need visual content for music releases, social media, live performances, vocal synth projects, and music-video mockups.&lt;/p&gt;

&lt;p&gt;However, producing a complete music video can be expensive and time-consuming.&lt;/p&gt;

&lt;p&gt;There is also a communication problem.&lt;/p&gt;

&lt;p&gt;A musician may know exactly where a visual should pulse, distort, expand, or collapse, but translating that timing and atmosphere into instructions for another creator can be difficult.&lt;/p&gt;

&lt;p&gt;I wanted to create a tool that could quickly transform the structure and energy of a track into a visual starting point.&lt;/p&gt;

&lt;p&gt;My main requirements were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No desktop video editor required&lt;/li&gt;
&lt;li&gt;Real-time audio-reactive rendering&lt;/li&gt;
&lt;li&gt;Multiple visual styles&lt;/li&gt;
&lt;li&gt;Customizable parameters&lt;/li&gt;
&lt;li&gt;A fullscreen Live / VJ mode&lt;/li&gt;
&lt;li&gt;Full HD MP4 export&lt;/li&gt;
&lt;li&gt;Local processing of uploaded media&lt;/li&gt;
&lt;li&gt;An open-source codebase that developers can study and extend&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Technology stack
&lt;/h2&gt;

&lt;p&gt;The application is built with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React 19&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Vite&lt;/li&gt;
&lt;li&gt;Three.js&lt;/li&gt;
&lt;li&gt;React Three Fiber&lt;/li&gt;
&lt;li&gt;React Three Drei&lt;/li&gt;
&lt;li&gt;WebGL&lt;/li&gt;
&lt;li&gt;Canvas 2D&lt;/li&gt;
&lt;li&gt;Web Audio API&lt;/li&gt;
&lt;li&gt;Zustand&lt;/li&gt;
&lt;li&gt;Material UI&lt;/li&gt;
&lt;li&gt;WebCodecs&lt;/li&gt;
&lt;li&gt;mp4-muxer&lt;/li&gt;
&lt;li&gt;ffmpeg.wasm&lt;/li&gt;
&lt;li&gt;Cloudflare Workers Static Assets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It currently includes three main visual modes.&lt;/p&gt;

&lt;h3&gt;
  
  
  3D Visualizer
&lt;/h3&gt;

&lt;p&gt;The 3D mode provides multiple scene presets, particle systems, geometry controls, camera movement, morphing, shaders, and post-processing effects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wave Visualizer
&lt;/h3&gt;

&lt;p&gt;The Wave mode renders the uploaded audio as horizontal, circular, or bar-based waveform visualizations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Image FX
&lt;/h3&gt;

&lt;p&gt;Image FX applies audio-reactive glow, blur, RGB shift, noise, distortion, and pulse effects to uploaded artwork.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decoding audio in the browser
&lt;/h2&gt;

&lt;p&gt;The first step is loading and decoding an audio file with the Web Audio API.&lt;/p&gt;

&lt;p&gt;A simplified implementation looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;audioContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AudioContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;arrayBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;audioBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decodeAudioData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting &lt;code&gt;AudioBuffer&lt;/code&gt; contains decoded PCM audio data.&lt;/p&gt;

&lt;p&gt;This is useful for offline processing such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Building waveform previews&lt;/li&gt;
&lt;li&gt;Sampling the complete track&lt;/li&gt;
&lt;li&gt;Detecting peaks&lt;/li&gt;
&lt;li&gt;Estimating BPM&lt;/li&gt;
&lt;li&gt;Preparing deterministic export data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For real-time playback, an &lt;code&gt;AnalyserNode&lt;/code&gt; provides continuously updated frequency and waveform data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;analyser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;audioContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createAnalyser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fftSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;smoothingTimeConstant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frequencyData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;frequencyBinCount&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;waveformData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fftSize&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During playback, the arrays are updated with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByteFrequencyData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frequencyData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByteTimeDomainData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;waveformData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These arrays form the connection between the audio engine and the rendering system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Turning FFT data into visual signals
&lt;/h2&gt;

&lt;p&gt;Raw FFT data is not automatically useful for animation.&lt;/p&gt;

&lt;p&gt;The renderer usually needs a smaller set of normalized signals, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Overall energy&lt;/li&gt;
&lt;li&gt;Bass energy&lt;/li&gt;
&lt;li&gt;Midrange energy&lt;/li&gt;
&lt;li&gt;High-frequency energy&lt;/li&gt;
&lt;li&gt;Peak intensity&lt;/li&gt;
&lt;li&gt;Waveform displacement&lt;/li&gt;
&lt;li&gt;Smoothed amplitude&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A basic helper for averaging a range of frequency bins can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;averageRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;safeEnd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;safeEnd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;safeEnd&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The spectrum can then be divided into broad frequency bands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;averageRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frequencyData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;averageRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frequencyData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;highs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;averageRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frequencyData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ideal ranges depend on the FFT size, sample rate, music style, and desired visual response.&lt;/p&gt;

&lt;p&gt;For electronic music, bass transients work well for large-scale movement, while high frequencies can control particles, flashes, noise, and smaller details.&lt;/p&gt;




&lt;h2&gt;
  
  
  Smoothing audio-reactive movement
&lt;/h2&gt;

&lt;p&gt;Directly mapping frequency values to visual objects often produces unstable movement.&lt;/p&gt;

&lt;p&gt;Audio data changes extremely quickly, so I smooth the signals before sending them to the renderer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;smoothValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;factor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;factor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;smoothedBass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;smoothValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;smoothedBass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;smoothedMids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;smoothValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;smoothedMids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;smoothedHighs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;smoothValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;smoothedHighs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;highs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.06&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Different smoothing values create different visual characteristics.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fast smoothing feels responsive and aggressive&lt;/li&gt;
&lt;li&gt;Slow smoothing feels fluid and atmospheric&lt;/li&gt;
&lt;li&gt;Separate attack and release speeds create sharp impacts with smoother decay&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This signal-conditioning stage is one of the most important parts of an audio-reactive system.&lt;/p&gt;

&lt;p&gt;The visual result often depends more on normalization and smoothing than on the complexity of the 3D scene itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mapping audio data to Three.js
&lt;/h2&gt;

&lt;p&gt;React Three Fiber makes it possible to manage Three.js scenes through React components.&lt;/p&gt;

&lt;p&gt;A simplified reactive mesh might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useFrame&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@react-three/fiber&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Mesh&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;three&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ReactiveMeshProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;getEnergy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ReactiveMesh&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;getEnergy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ReactiveMeshProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;meshRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Mesh&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useFrame&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;meshRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;energy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getEnergy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;energy&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;meshRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setScalar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;meshRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt;
      &lt;span class="mf"&gt;0.002&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;energy&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;meshRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt;
      &lt;span class="mf"&gt;0.003&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;energy&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.015&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;mesh&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;meshRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;icosahedronGeometry&lt;/span&gt; &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;meshStandardMaterial&lt;/span&gt; &lt;span class="na"&gt;wireframe&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;mesh&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the complete application, audio signals can control:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Geometry deformation&lt;/li&gt;
&lt;li&gt;Particle positions&lt;/li&gt;
&lt;li&gt;Particle size&lt;/li&gt;
&lt;li&gt;Camera movement&lt;/li&gt;
&lt;li&gt;Shader uniforms&lt;/li&gt;
&lt;li&gt;Noise intensity&lt;/li&gt;
&lt;li&gt;Post-processing strength&lt;/li&gt;
&lt;li&gt;Image distortion&lt;/li&gt;
&lt;li&gt;RGB displacement&lt;/li&gt;
&lt;li&gt;Glow and blur&lt;/li&gt;
&lt;li&gt;Waveform radius&lt;/li&gt;
&lt;li&gt;Layer opacity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important architectural decision was to keep audio analysis separate from visual rendering.&lt;/p&gt;

&lt;p&gt;The analysis layer produces normalized signals.&lt;/p&gt;

&lt;p&gt;Each visualizer consumes those signals without needing to understand how the audio was decoded.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building audio-reactive particles
&lt;/h2&gt;

&lt;p&gt;Particle systems are especially useful for music visualization because different frequency bands can control different parts of the scene.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bass controls the overall radius&lt;/li&gt;
&lt;li&gt;Midrange controls turbulence&lt;/li&gt;
&lt;li&gt;High frequencies control flickering&lt;/li&gt;
&lt;li&gt;Overall energy controls particle size&lt;/li&gt;
&lt;li&gt;Waveform values offset individual particle positions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simplified particle update might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;particleCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;waveformIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;waveformData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;waveformValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;waveformData&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;waveformIndex&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;basePositions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="nx"&gt;waveformValue&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;intensity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;basePositions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;mids&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;basePositions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="nx"&gt;bass&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After modifying a Three.js &lt;code&gt;BufferAttribute&lt;/code&gt;, it must be marked for an update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;positionAttribute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;needsUpdate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Updating thousands of particles every frame can become expensive.&lt;/p&gt;

&lt;p&gt;Performance depends on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Particle count&lt;/li&gt;
&lt;li&gt;Attribute update frequency&lt;/li&gt;
&lt;li&gt;Geometry complexity&lt;/li&gt;
&lt;li&gt;Device pixel ratio&lt;/li&gt;
&lt;li&gt;Post-processing passes&lt;/li&gt;
&lt;li&gt;Memory allocations inside the render loop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoiding unnecessary object creation during each frame is particularly important.&lt;/p&gt;




&lt;h2&gt;
  
  
  Managing application state
&lt;/h2&gt;

&lt;p&gt;The visualizer has many adjustable parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Visual mode&lt;/li&gt;
&lt;li&gt;Active preset&lt;/li&gt;
&lt;li&gt;Particle count&lt;/li&gt;
&lt;li&gt;Particle size&lt;/li&gt;
&lt;li&gt;Particle shape&lt;/li&gt;
&lt;li&gt;Camera distance&lt;/li&gt;
&lt;li&gt;Morph intensity&lt;/li&gt;
&lt;li&gt;Waveform style&lt;/li&gt;
&lt;li&gt;Image effects&lt;/li&gt;
&lt;li&gt;Layer order&lt;/li&gt;
&lt;li&gt;Playback position&lt;/li&gt;
&lt;li&gt;Export progress&lt;/li&gt;
&lt;li&gt;Live-mode controls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I use Zustand for shared application state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;VisualizerState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;bass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;mids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;highs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;particleSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nl"&gt;setAudioBands&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="na"&gt;bass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;highs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useVisualizerStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;VisualizerState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;bass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;highs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;particleSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="na"&gt;setAudioBands&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;highs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;bass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;highs&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, not every audio-frame update should trigger a complete React re-render.&lt;/p&gt;

&lt;p&gt;For rapidly changing values, refs, dedicated analysis objects, or direct store access can be more efficient.&lt;/p&gt;

&lt;p&gt;UI state and render-loop state have different performance requirements.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-time playback and video export use different clocks
&lt;/h2&gt;

&lt;p&gt;Real-time playback can use the browser animation loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Video export needs deterministic frames.&lt;/p&gt;

&lt;p&gt;At 30 FPS, frame &lt;code&gt;n&lt;/code&gt; must always represent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;frameIndex&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result should not depend on how quickly the computer happens to render.&lt;/p&gt;

&lt;p&gt;A simplified export loop looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalFrames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;durationSeconds&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;frameIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;frameIndex&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;totalFrames&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;frameIndex&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;frameIndex&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;updateAudioAnalysisAtTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;renderVisualizerFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;encodeCurrentFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frameIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without fixed timestamps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frames may be skipped&lt;/li&gt;
&lt;li&gt;Frames may be duplicated&lt;/li&gt;
&lt;li&gt;Visual timing may drift&lt;/li&gt;
&lt;li&gt;Audio and video may lose synchronization&lt;/li&gt;
&lt;li&gt;Results may differ between devices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The live renderer and exporter can share visual logic, but they should not use the same clock.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exporting MP4 video with WebCodecs
&lt;/h2&gt;

&lt;p&gt;WebCodecs provides low-level access to browser media encoders.&lt;/p&gt;

&lt;p&gt;The export pipeline is approximately:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Render a visual frame&lt;/li&gt;
&lt;li&gt;Create a &lt;code&gt;VideoFrame&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Send it to a &lt;code&gt;VideoEncoder&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Receive encoded chunks&lt;/li&gt;
&lt;li&gt;Mux the video and audio tracks into MP4&lt;/li&gt;
&lt;li&gt;Create a downloadable &lt;code&gt;Blob&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A simplified encoding function looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;encodeCanvasFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLCanvasElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;encoder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VideoEncoder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;frameIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frameIndex&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VideoFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;encoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Closing each &lt;code&gt;VideoFrame&lt;/code&gt; is essential.&lt;/p&gt;

&lt;p&gt;If frames remain open during a long export, memory usage can grow rapidly.&lt;/p&gt;

&lt;p&gt;A possible encoder configuration is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VideoEncoderConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;codec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;avc1.42001f&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;bitrate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;framerate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before starting, the application should check whether the browser supports the requested configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;support&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;VideoEncoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isConfigSupported&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;support&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;supported&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The requested encoder configuration is not supported&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project uses &lt;code&gt;mp4-muxer&lt;/code&gt; to combine encoded video and audio data into the final MP4 container.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I added an ffmpeg.wasm fallback
&lt;/h2&gt;

&lt;p&gt;WebCodecs can be fast, but codec support varies between browsers and operating systems.&lt;/p&gt;

&lt;p&gt;A browser may expose the WebCodecs API while rejecting a specific encoder configuration.&lt;/p&gt;

&lt;p&gt;For that reason, the application retries failed exports with ffmpeg.wasm.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;exportWithWebCodecs&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;WebCodecs export failed. Falling back to ffmpeg.wasm.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;error&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;exportWithFfmpeg&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fallback path is slower and usually requires more memory, but it makes the application usable in more environments.&lt;/p&gt;

&lt;p&gt;The deployed application first attempts to load ffmpeg core assets locally.&lt;/p&gt;

&lt;p&gt;If those files are unavailable, it can retrieve the required runtime assets from jsDelivr.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keeping uploaded media local
&lt;/h2&gt;

&lt;p&gt;Uploaded music and artwork are not sent to an application backend.&lt;/p&gt;

&lt;p&gt;The browser handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Audio decoding&lt;/li&gt;
&lt;li&gt;Frequency analysis&lt;/li&gt;
&lt;li&gt;Waveform sampling&lt;/li&gt;
&lt;li&gt;Image processing&lt;/li&gt;
&lt;li&gt;Three.js rendering&lt;/li&gt;
&lt;li&gt;Video encoding&lt;/li&gt;
&lt;li&gt;MP4 generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The completed MP4 is generated as a browser &lt;code&gt;Blob&lt;/code&gt; and downloaded locally.&lt;/p&gt;

&lt;p&gt;This provides several advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Private or unreleased music does not need to be uploaded&lt;/li&gt;
&lt;li&gt;There is no media upload delay&lt;/li&gt;
&lt;li&gt;Server-side media storage is unnecessary&lt;/li&gt;
&lt;li&gt;Hosting costs stay relatively low&lt;/li&gt;
&lt;li&gt;The application can be deployed mostly as static assets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also introduces limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Large files consume browser memory&lt;/li&gt;
&lt;li&gt;Long exports can be CPU-intensive&lt;/li&gt;
&lt;li&gt;Performance varies by device&lt;/li&gt;
&lt;li&gt;The browser tab must remain open&lt;/li&gt;
&lt;li&gt;Complex scenes are difficult on lower-powered mobile devices&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Live / VJ mode
&lt;/h2&gt;

&lt;p&gt;The application also includes a fullscreen Live / VJ mode.&lt;/p&gt;

&lt;p&gt;It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hides the editing interface&lt;/li&gt;
&lt;li&gt;Expands the visual output&lt;/li&gt;
&lt;li&gt;Supports keyboard controls&lt;/li&gt;
&lt;li&gt;Provides temporary effect boosts&lt;/li&gt;
&lt;li&gt;Allows the visualizer to be used during performance experiments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main challenge was keeping editing controls and performance controls connected to the same state without allowing UI updates to interrupt rendering.&lt;/p&gt;

&lt;p&gt;Separating the application shell from the visual components made this easier.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project structure
&lt;/h2&gt;

&lt;p&gt;The codebase is organized by responsibility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── audio/       # Decoding, FFT, waveform, BPM and analysis
├── export/      # WebCodecs, MP4 muxing and ffmpeg fallback
├── ui/          # Controls and visualizer canvases
├── visual/      # Three.js scenes, particles, shaders and presets
├── App.tsx      # Application shell and orchestration
└── store.ts     # Shared Zustand state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main architectural principle is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Analyze the audio once, normalize the result, and let multiple visual systems consume the same signals.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This allows the 3D, waveform, and image-effect modes to share the same musical timing while rendering in completely different ways.&lt;/p&gt;




&lt;h2&gt;
  
  
  The hardest parts
&lt;/h2&gt;

&lt;p&gt;The most difficult problems were not simply creating objects with Three.js.&lt;/p&gt;

&lt;h3&gt;
  
  
  Synchronizing video export
&lt;/h3&gt;

&lt;p&gt;Real-time rendering and fixed-frame export use different timing systems.&lt;/p&gt;

&lt;p&gt;The exporter needs predictable timestamps and repeatable audio sampling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Browser codec differences
&lt;/h3&gt;

&lt;p&gt;WebCodecs availability does not guarantee support for every video or audio codec configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory management
&lt;/h3&gt;

&lt;p&gt;Full HD rendering, video encoding, audio processing, and WebAssembly can consume significant amounts of memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stabilizing movement
&lt;/h3&gt;

&lt;p&gt;Raw frequency data is noisy.&lt;/p&gt;

&lt;p&gt;Every visual property needs suitable normalization, smoothing, sensitivity, and limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sharing audio logic across visual modes
&lt;/h3&gt;

&lt;p&gt;The 3D, waveform, and image-effect renderers all need access to the same analysis data without becoming tightly coupled.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I want to improve next
&lt;/h2&gt;

&lt;p&gt;The current roadmap includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better keyboard accessibility&lt;/li&gt;
&lt;li&gt;Improved screen-reader support&lt;/li&gt;
&lt;li&gt;Responsive layouts for narrower screens&lt;/li&gt;
&lt;li&gt;Automated tests for audio analysis and export utilities&lt;/li&gt;
&lt;li&gt;Saveable and shareable visual presets&lt;/li&gt;
&lt;li&gt;Better long-duration export guidance&lt;/li&gt;
&lt;li&gt;A maintained browser compatibility matrix&lt;/li&gt;
&lt;li&gt;Additional visual presets&lt;/li&gt;
&lt;li&gt;Performance benchmarks for complex scenes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repository already includes contributing guidelines, issue templates, pull request templates, security instructions, a code of conduct, and an MIT License.&lt;/p&gt;

&lt;p&gt;Contributions are welcome.&lt;/p&gt;




&lt;h2&gt;
  
  
  Minimal starter project
&lt;/h2&gt;

&lt;p&gt;I also released a smaller educational repository that focuses only on the core audio-to-visual pipeline.&lt;/p&gt;

&lt;p&gt;It demonstrates how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Load a local audio file&lt;/li&gt;
&lt;li&gt;Analyze volume, bass, mids, highs, and waveform data&lt;/li&gt;
&lt;li&gt;Keep high-frequency audio updates outside normal React rerenders&lt;/li&gt;
&lt;li&gt;Drive a Three.js mesh and particle system in real time&lt;/li&gt;
&lt;li&gt;Structure the project with React, TypeScript, and React Three Fiber&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/7g3n/web-audio-threejs-starter" rel="noopener noreferrer"&gt;https://github.com/7g3n/web-audio-threejs-starter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Live demo:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://7g3n.github.io/web-audio-threejs-starter/" rel="noopener noreferrer"&gt;https://7g3n.github.io/web-audio-threejs-starter/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to understand the core implementation before exploring the full visualizer, this starter is the best place to begin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the project
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Live demo
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://waveform.tranjectories.xyz/" rel="noopener noreferrer"&gt;https://waveform.tranjectories.xyz/&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Source code
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/7g3n/phase-viz" rel="noopener noreferrer"&gt;https://github.com/7g3n/phase-viz&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  v1.0.0 release
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/7g3n/phase-viz/releases/tag/v1.0.0" rel="noopener noreferrer"&gt;https://github.com/7g3n/phase-viz/releases/tag/v1.0.0&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Music video created with the visualizer
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=R8ItWr2V_ZA" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=R8ItWr2V_ZA&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you are interested in Three.js, the Web Audio API, creative coding, audiovisual tools, or browser-based media processing, I would appreciate your feedback.&lt;/p&gt;

&lt;p&gt;Issues, pull requests, and other contributions are welcome.&lt;/p&gt;

&lt;p&gt;If the project is useful to you, consider starring the repository.&lt;/p&gt;

&lt;p&gt;Thanks for reading.&lt;/p&gt;

</description>
      <category>threejs</category>
      <category>typescript</category>
      <category>webaudio</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
