Architecture in one paragraph
Every page is plain HTML generated at build time from one page registry (src/content.js) and bundled with Vite. Cloudflare Workers Static Assets serves the files. A small Worker runs in front of them for three jobs only: permanent redirects (http to https, www to the apex, retired URLs), the /event counter, and the optional /newsletter/ signup. There is no route that accepts audio or video, and no server-side processing of any kind. Once a page has loaded, every tool runs on your device: decoding, the effect itself, encoding and the download link, which is a blob: URL created in your tab.
The media stack
We use three browser building blocks and one fallback:
- Mediabunny (an open-source TypeScript media toolkit) demuxes and muxes containers: MP4/MOV/M4A, WebM/MKV, Ogg, MP3, WAV, FLAC, ADTS AAC and MPEG-TS. It hands the compressed audio to the browser's WebCodecs decoders and encoders, so conversion between formats the browser understands never leaves native code paths.
- Web Audio (
decodeAudioData,OfflineAudioContext) decodes some codecs WebCodecs does not, notably Ogg Vorbis in some browsers, and renders the filter-based effects: equalizer, bass boost, reverb convolution and 8D panning. - Plain JavaScript DSP on
Float32Arrays does the analysis and the sample-level edits: silence detection, loudness measurement, noise reduction, time stretching, key and tempo detection. These modules have no Web Audio dependency, so the same code runs in our Node test suite. - MP3 encoding uses WebCodecs when the browser reports an MP3 encoder. Otherwise we load
@mediabunny/mp3-encoder, a LAME build compiled to WebAssembly. - ffmpeg.wasm is the last resort for containers and codecs nothing above can read: WMA and WMV (ASF), AVI, FLV, MPEG-PS, AMR and others. See the fallback section below.
For converters the order is: Mediabunny with WebCodecs first; if the conversion is not possible that way, Web Audio decodes to PCM and we encode the PCM; if the container cannot be read at all, ffmpeg.wasm converts the file. Editing tools need PCM, so they try decodeAudioData first, then Mediabunny to WAV, then ffmpeg.wasm. The browser audio support test shows which of these paths your own browser takes for each format.
Silence detection
The silence remover splits the audio into 10 ms windows and computes the RMS level of each window for every channel, keeping the loudest channel, so a pause only counts when all channels are quiet. A window is quiet when its level is below the threshold (−35 dBFS by default, −32 dBFS on the video page). Runs of quiet windows shorter than the minimum pause (0.5 s by default, 0.4 s for video) are kept. For longer runs we keep a padding of audio on each side (0.12 s by default, 0.15 s for video) and cut the rest. The kept segments are joined with a 5 ms linear crossfade so the joins do not click. For video, the same list of segments is applied to the picture and the soundtrack, so they stay in sync. It is deliberately simple: an energy gate, not voice activity detection, so steady background noise above the threshold is treated as sound.
Loudness normalization (LUFS)
The normalizer measures integrated loudness as defined in ITU-R BS.1770, the measure that EBU R 128 builds on. Each channel goes through the two-stage K-weighting filter (a high-shelf around 1.68 kHz and a high-pass around 38 Hz), with the biquad coefficients recomputed for the file's sample rate rather than hard-coded for 48 kHz. Mean square energy is measured in 400 ms blocks with a 100 ms hop, summed over channels, then gated twice: an absolute gate at −70 LUFS and a relative gate 10 LU below the ungated average. We then apply one gain to reach the target (for example −16 LUFS for podcasts or −14 LUFS for streaming) and pass peaks through a tanh soft limiter that starts at 70% of the ceiling (−1 dBFS by default). Two honest caveats: all channels are weighted 1.0, which matches BS.1770 for mono and stereo but not for surround, and the ceiling is a sample-peak limit, not a true-peak (oversampled) measurement.
Key detection
The key finder mixes to mono and decimates to about 11 kHz after a 4th-order Butterworth low-pass, since harmony lives well below 4 kHz. It takes a Hann-windowed FFT of about 0.75 s per frame with 50% overlap and picks spectral peaks between 55 Hz and 2.1 kHz. Each peak is compared with the average spectrum within about one semitone around it (a simple whitening step), so drums and the overall spectral tilt do not vote. Peak frequencies are refined by parabolic interpolation on the log magnitude and converted to pitch. A weighted circular mean of the peaks' offsets from the equal-tempered grid estimates the tuning (a track tuned to A = 432 Hz reads about −32 cents). Peaks are folded into a 12-bin chromagram, with peaks that fall between two semitones weighted down by cos², and each frame is normalized. The summed chroma is correlated (Pearson) with the Krumhansl-Kessler major and minor key profiles rotated to all 12 tonics. The best of the 24 keys wins; the confidence shown combines its correlation with its margin over the runner-up.
Tempo (BPM) detection
The BPM finder builds an onset-strength envelope from the positive spectral flux of a log-compressed magnitude spectrogram (about 46 ms frames at a 10 ms hop), removes the local one-second mean and half-wave rectifies it. It autocorrelates that envelope and scores every tempo from 60 to 200 BPM in 0.1 BPM steps by the autocorrelation at one to four beat periods, weighted 1/k, times a log-Gaussian prior centred on 120 BPM that settles half-time versus double-time. If the pattern repeats nearly as strongly at half the period, it reports the double tempo. The final value is refined by fitting a line through the autocorrelation peaks at every multiple of the beat. Below a periodicity of 0.15 it reports that no steady beat was found instead of guessing.
Other processing, briefly
The noise reducer is spectral gating: it estimates a noise spectrum from the quietest 10% of frames (or a range you choose), then attenuates each frequency bin that is not clearly above that noise, smoothing the gains across time and frequency, with optional hum notches and an 80 Hz high-pass. Speed changes use waveform-similarity overlap-add (WSOLA) with 40 ms frames, so voices keep their pitch; pitch shifts resample first and then time-stretch back. Reverb convolves with an impulse response we generate (decaying noise), not a recorded one. None of these use machine learning models.
Lazy loading and what you download
Measured from our production build on 2026-10-27 (minified, with gzip sizes as Vite reports them):
- Every page: the main script, 370 KB (121 KB gzipped), plus CSS, 21 KB (5.2 KB gzipped). Most of the main script is page copy, because the same renderer runs at build time and in the browser. Splitting it per page is on our list.
- When you choose a file or a sample: the audio engine (Mediabunny plus our conversion code), 584 KB (145 KB gzipped).
- Only when the browser has no MP3 encoder of its own: the WebAssembly LAME encoder, 311 KB (133 KB gzipped).
- Only on the key, BPM and spectrogram pages: the analysis module, 9.2 KB (4.2 KB gzipped). Video export adds 2.4 KB (1.2 KB gzipped); the slowed, nightcore and 8D effects add 1.2 KB (0.7 KB gzipped).
- Only when a file needs the fallback: the ffmpeg.wasm wrapper and its worker, about 4.9 KB and 2.5 KB, then the ffmpeg core itself (next section).
All 82 pages and all of these chunks except the ffmpeg core add up to about 2.5 MB, which is what the offline cache stores.
The ffmpeg.wasm fallback and its cost
Browsers cannot read WMA, WMV, AVI, FLV, MPEG program streams, AIFF in some browsers, or AMR voice notes. For those we load ffmpeg.wasm (@ffmpeg/core 0.12.10, single-threaded build) inside a Web Worker. The core is two files: ffmpeg-core.js (111,804 bytes) and ffmpeg-core.wasm (32,232,419 bytes, about 30.7 MiB). The status line says so when the download starts. The file you converted is written to ffmpeg's in-memory file system inside the worker, converted there and read back; it is not sent anywhere.
The core comes from cdn.jsdelivr.net, not from audioclover.com. We wanted to self-host it, but the WebAssembly file is larger than the 25 MiB per-file limit of Cloudflare Workers Static Assets. To keep the third-party CDN honest, both files are pinned with SHA-384 Subresource Integrity hashes in src/legacy.js, so the browser refuses any other bytes. jsDelivr does see an ordinary request for the decoder, with your IP address and browser details, as any web server would. jsDelivr serves the files with a one-year immutable cache header, so the browser normally keeps them after the first download, but browsers may evict large cached files. The fallback is also slower than the native path: it is the single-threaded build of ffmpeg running as WebAssembly.
Offline mode
On your first visit, a small hand-written service worker (/sw.js, no Workbox) stores every page's HTML, the hashed JavaScript and CSS chunks listed above, the manifest and icons. Each build writes a new cache version (a hash of all cached files), and the old cache is deleted when the new worker activates. Pages are served stale-while-revalidate: the saved copy appears at once and a fresh copy is fetched in the background. Hashed chunks are served from the cache, since their names change whenever their content does. Samples are saved the first time you play one.
The service worker only answers for that allowlist. It never touches the /event counter, the newsletter endpoints, analytics, or the ffmpeg core, so formats that need the fallback decoder still need a connection the first time. When the cache is ready, the footer shows a small “works offline” note. Clearing site data for audioclover.com removes the cache. Safari's handling of service worker storage for sites that are not added to the home screen may differ; we have not tested offline mode on iOS yet.
Limits
Editing tools decode the whole file into 32-bit floating-point PCM in memory. One hour of 44.1 kHz stereo is about 1.27 GB of samples before any processing copies, which is why the tools warn above 150 MB and why a phone may run out of memory where a laptop does not. Converters that stay on the Mediabunny and WebCodecs path read the input in chunks and only hold the compressed output in memory, so they need far less. Other limits we know of:
- Encoders come from the browser. Firefox has no WebCodecs AAC encoder, so M4A and M4R output is unavailable there and the page says so. Opus output always runs at 48 kHz.
- Decoders also come from the browser, so the same file can take the native path in one browser and the ffmpeg fallback in another. ALAC in M4A, for example, decodes natively in Safari but not in Chromium (from our test notes; Safari not re-tested for this page).
- DRM-protected files cannot be read by any path.
- Unverified so far: iPhone Safari with Voice Memos M4A files, real AMR voice recordings (our AMR test file is synthetic), microphone recording on mobile, offline mode on iOS, and very large files on phones.
How to verify nothing is uploaded
You do not have to take our word for it. In Chrome, Edge or Firefox:
- Open a tool, then open DevTools (F12, or Cmd+Option+I on a Mac) and choose the Network tab. Tick “Preserve log”, and clear the list.
- Choose a file and run the tool. You will see the lazily loaded JavaScript chunks under
/assets/, a sample under/samples/if you used one, and smallPOST /eventrequests. Click one: its payload is a few hundred bytes of JSON with the tool name, the file type, a size range such as “1-10MB” and a step name. You will also see Google Analytics requests. For WMA, AVI and similar files, you will see the two ffmpeg core downloads fromcdn.jsdelivr.net. - Sort by size. Nothing you send is anywhere near the size of your file: every large transfer is a download to you, not an upload from you.
The airplane-mode test is stronger: visit a tool once while online and wait for the footer to say the site works offline, then turn off Wi-Fi and mobile data, reload the page, choose a file and run the tool. If it finishes and you can play the result, the file could not have gone anywhere. (Formats that need the ffmpeg core need it downloaded once first.)
Source and corrections
The code is not public yet. If something on this page does not match what you see in DevTools, or a format fails in your browser, the support test gives you a copyable report you can share alongside the problem. Our privacy page covers the same data flows in plain language.
Last updated October 27, 2026. Sizes and behavior described here come from the code and build of that date.