<?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>Forem: Thomas Yates</title>
    <description>The latest articles on Forem by Thomas Yates (@thomas_yates_ad4dce8d88f6).</description>
    <link>https://forem.com/thomas_yates_ad4dce8d88f6</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3648008%2F541be465-7fa3-4a1f-8cc5-5f18fedeb144.png</url>
      <title>Forem: Thomas Yates</title>
      <link>https://forem.com/thomas_yates_ad4dce8d88f6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/thomas_yates_ad4dce8d88f6"/>
    <language>en</language>
    <item>
      <title>How I Built a Client-Side Audio Toolkit (No Server Uploads)</title>
      <dc:creator>Thomas Yates</dc:creator>
      <pubDate>Fri, 05 Dec 2025 17:13:00 +0000</pubDate>
      <link>https://forem.com/thomas_yates_ad4dce8d88f6/how-i-built-a-client-side-audio-toolkit-no-server-uploads-1p3b</link>
      <guid>https://forem.com/thomas_yates_ad4dce8d88f6/how-i-built-a-client-side-audio-toolkit-no-server-uploads-1p3b</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;I needed to add cover art to a FLAC file. Simple task, right? &lt;/p&gt;

&lt;p&gt;Not so fast. Every solution I found either required:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Installing desktop software (overkill for a one-time task)&lt;/li&gt;
&lt;li&gt;Uploading my files to someone's server (privacy concerns)&lt;/li&gt;
&lt;li&gt;Paying for a subscription service (unnecessary expense)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a developer, this felt like a solvable problem. So I built a browser-based solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: FFmpeg.wasm
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/ffmpegwasm/ffmpeg.wasm" rel="noopener noreferrer"&gt;FFmpeg.wasm&lt;/a&gt; is a WebAssembly port of FFmpeg that runs entirely in the browser. This means you can perform complex audio/video processing without server-side code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key advantage:&lt;/strong&gt; Files never leave the user's device. Everything processes locally using WebAssembly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;What started as a simple FLAC metadata editor turned into a full audio toolkit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Format Converter&lt;/strong&gt; - Convert between MP3, FLAC, WAV, AAC, OGG, M4A&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata Editor&lt;/strong&gt; - Add cover art and edit tags for audio files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slowed + Reverb Maker&lt;/strong&gt; - Create slowed and reverb audio effects (popular for TikTok/vaporwave)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio Trimmer&lt;/strong&gt; - Cut and trim audio files with fade effects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Video to Audio&lt;/strong&gt; - Extract audio tracks from video files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volume Booster&lt;/strong&gt; - Increase volume or normalize audio loudness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Live demo: &lt;a href="https://soundtools.io" rel="noopener noreferrer"&gt;soundtools.io&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Implementation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FFmpeg.wasm for audio processing&lt;/li&gt;
&lt;li&gt;Vanilla JavaScript (no frameworks - keeping it simple)&lt;/li&gt;
&lt;li&gt;Client-side only - zero backend code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key challenges:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Lazy Loading FFmpeg&lt;/strong&gt;&lt;br&gt;
FFmpeg.wasm is large (~31MB for core + wasm files). Loading it immediately would destroy page performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Lazy load on demand. The page loads instantly with just HTML/CSS. FFmpeg only loads when a user selects a file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadFFmpeg&lt;/span&gt;&lt;span class="p"&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="nx"&gt;ffmpeg&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="c1"&gt;// Already loaded&lt;/span&gt;

    &lt;span class="nf"&gt;showStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Loading audio processor...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FFmpeg&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../ffmpeg/index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ffmpeg&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;FFmpeg&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;coreURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../ffmpeg/ffmpeg-core.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;wasmURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../ffmpeg/ffmpeg-core.wasm&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;span class="nf"&gt;showStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ready to process!&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;span class="c1"&gt;// Only load when user selects a file&lt;/span&gt;
&lt;span class="nx"&gt;fileInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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;ffmpeg&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;loadFFmpeg&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// Now process the file...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Initial page load: ~50KB (fast!)&lt;/li&gt;
&lt;li&gt;FFmpeg loads in background when needed: ~31MB&lt;/li&gt;
&lt;li&gt;Users get instant page, FFmpeg ready by the time they select a file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Memory Management&lt;/strong&gt;&lt;br&gt;
Large audio files (500MB+) can exhaust browser memory. Solution: Process in chunks and clear memory aggressively after each operation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Performance&lt;/strong&gt;&lt;br&gt;
WebAssembly is fast, but not native-fast. For typical audio files (10-100MB), performance is acceptable (10-30 seconds processing time).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. File Format Support&lt;/strong&gt;&lt;br&gt;
Different metadata standards (ID3v2 for MP3, Vorbis Comments for FLAC, MP4 tags for M4A) required format-specific handling.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Client-Side Processing Matters
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Privacy:&lt;/strong&gt; Your audio files contain metadata, personal recordings, or copyrighted content. Client-side processing means zero data leaves your device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; No server infrastructure means no hosting costs. The entire site runs as static HTML/JS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Offline Capability:&lt;/strong&gt; Once the page loads, tools work offline (after FFmpeg.wasm is cached).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance:&lt;/strong&gt; Lazy loading FFmpeg means pages load instantly while the heavy lifting happens in the background.&lt;/p&gt;
&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Working on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Audio joiner (merge multiple files)&lt;/li&gt;
&lt;li&gt;Vocal remover (using AI/ML models in-browser)&lt;/li&gt;
&lt;li&gt;Better mobile optimization&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;Check out the toolkit at &lt;a href="https://soundtools.io" rel="noopener noreferrer"&gt;soundtools.io&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All tools are free, no account required, no tracking.&lt;/p&gt;
&lt;h2&gt;
  
  
  Technical Details
&lt;/h2&gt;

&lt;p&gt;For developers interested in FFmpeg.wasm implementation, the basic pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Load FFmpeg (lazy loaded)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&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;FFmpeg&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Write input file to virtual filesystem&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input.mp3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;audioData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Run FFmpeg command&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-i&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input.mp3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-codec:a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;libmp3lame&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;output.mp3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// Read output file&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&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;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;output.mp3&lt;/span&gt;&lt;span class="dl"&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 real complexity comes from handling different formats, managing memory, building a clean UI around the processing pipeline, and optimizing page load performance with lazy loading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback Welcome
&lt;/h2&gt;

&lt;p&gt;Would love to hear thoughts on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Performance with your audio files&lt;/li&gt;
&lt;li&gt;Which tools you find most useful&lt;/li&gt;
&lt;li&gt;What other audio tools would be helpful&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built this as a side project to solve my own problem. Hopefully useful for others too.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>music</category>
    </item>
  </channel>
</rss>
