{"slug": "how-to-build-a-fair-a-b-audio-preview-for-ai-processing", "title": "How to Build a Fair A/B Audio Preview for AI Processing", "summary": "A developer outlines a fair A/B audio preview technique for evaluating AI audio processing, using Web Audio API to synchronize two decoded AudioBuffers with a single transport and gain-based switching. The approach ensures both versions play from the same position, enabling listeners to hear both improvements and artifacts without bias.", "body_md": "Two audio players do not make a fair before-and-after test.\n\nIf the second player restarts from zero or takes half a second to load, the user is no longer comparing two versions of the same moment. They are comparing two memories.\n\nThat is a weak way to evaluate any audio effect. It is especially weak for AI processing.\n\nA denoiser can remove a fan while softening consonants. A de-reverb model can reduce the room tail while making the voice sound less natural. The output may be cleaner without being better.\n\nThe preview therefore has one job: let the listener switch quickly enough to hear both the improvement and the damage.\n\nThe rule I use is deliberately boring. Both versions should contain the same edit and play from the same position. Switching should not restart playback or create a pause. The interface should not hint that one version is supposed to win.\n\nTwo independent `<audio>`\n\nelements fail surprisingly quickly. Each owns its playback state, buffering behavior, clock, and seek operation. The user ends up finding the same position twice and comparing one sound with a memory of another.\n\nA better interface has one transport and one version control:\n\n```\n[ Play ]  [ Original | Processed ]  00:18 ━━━━━━━ 00:42\n```\n\nThe transport decides *where* playback happens. The segmented control decides *which signal* is audible.\n\nFor a short preview, I decode both files into `AudioBuffer`\n\ns, start them at the same `AudioContext`\n\ntime and offset, and route each through its own `GainNode`\n\n. Both sources run; only one gain is open.\n\n`decodeAudioData()`\n\ndecodes complete file data and resamples it to the context's sample rate. The decoded buffers can then share the same audio clock. See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/decodeAudioData) for format and loading details.\n\nThe core is small:\n\n``` js\nconst context = new AudioContext();\nconst originalGain = context.createGain();\nconst processedGain = context.createGain();\n\noriginalGain.connect(context.destination);\nprocessedGain.connect(context.destination);\n\nasync function loadBuffer(url) {\n  const response = await fetch(url);\n  return context.decodeAudioData(await response.arrayBuffer());\n}\n\nconst buffers = {\n  original: await loadBuffer(\"/audio/original.wav\"),\n  processed: await loadBuffer(\"/audio/processed.wav\"),\n};\n\nlet sources = [];\n\nfunction startPair(fromSeconds = 0) {\n  const when = context.currentTime + 0.03;\n  const duration = Math.min(\n    buffers.original.duration,\n    buffers.processed.duration,\n  );\n\n  stopPair();\n  sources = Object.entries(buffers).map(([name, buffer]) => {\n    const source = context.createBufferSource();\n    source.buffer = buffer;\n    source.connect(name === \"original\" ? originalGain : processedGain);\n    source.start(when, fromSeconds, duration - fromSeconds);\n    return source;\n  });\n\n  setAudibleVersion(activeVersion, when, 0);\n}\n\nfunction stopPair() {\n  for (const source of sources) source.stop();\n  sources = [];\n}\n```\n\nCall `context.resume()`\n\nand `startPair()`\n\nfrom a click or tap handler. Track the elapsed offset in your transport; pause and seek should stop the pair and create new source nodes at that offset. An `AudioBufferSourceNode`\n\nis intentionally single-use, while its decoded `AudioBuffer`\n\ncan be reused. [MDN explains the lifecycle here](https://developer.mozilla.org/en-US/docs/Web/API/AudioBufferSourceNode).\n\nAn instant gain jump can click when it lands away from a zero crossing. A long crossfade hides the click but makes the comparison less precise because the listener hears a blend. For speech, I start with a ramp of a few milliseconds and tune it on real material.\n\n``` js\nconst versionGains = {\n  original: originalGain,\n  processed: processedGain,\n};\n\nlet activeVersion = \"original\";\n\nfunction setAudibleVersion(version, now = context.currentTime, ramp = 0.012) {\n  activeVersion = version;\n\n  for (const [name, node] of Object.entries(versionGains)) {\n    const target = name === version ? 1 : 0;\n\n    node.gain.cancelScheduledValues(now);\n\n    if (ramp === 0) {\n      node.gain.setValueAtTime(target, now);\n      continue;\n    }\n\n    node.gain.setValueAtTime(node.gain.value, now);\n    node.gain.linearRampToValueAtTime(target, now + ramp);\n  }\n}\n\ndocument.querySelector(\"#show-original\").addEventListener(\"click\", () => {\n  setAudibleVersion(\"original\");\n});\n\ndocument.querySelector(\"#show-processed\").addEventListener(\"click\", () => {\n  setAudibleVersion(\"processed\");\n});\n```\n\nThe 12 ms value is a starting point, not a standard. Listen for clicks, comb filtering during the overlap, and any sense that the button responds late. If the files are not sample-aligned, even a short overlap can sound strange.\n\nStarting both buffers at the same Web Audio time only works if the files contain corresponding samples at corresponding positions.\n\nAn AI pipeline may introduce delay through frame padding, look-ahead context, silence trimming, resampling, or codec priming.\n\nIf the processed voice begins 80 ms later, the browser will faithfully play the wrong moments together.\n\nFix alignment before the files reach the comparison UI. Preserve the original timeline through processing, or estimate the delay against a reference and compensate by trimming or padding. Keep the preview lengths identical; do not quietly loop one version after the other ends.\n\nTest this with a transient: switch repeatedly and confirm that its position does not move.\n\nMost users do not need to compare an entire 40-minute recording. They need to inspect the four seconds where a keyboard overlaps a sentence or traffic passes behind a voice.\n\nA shared timeline and a short loop region are more useful than two large players. Keep the A/B controls keyboard-accessible, show which version is audible, and do not stop the loop when the listener switches.\n\nThe waveform is useful for navigation, but it is not evidence of quality. A smoother waveform does not prove that speech sounds more natural. Avoid using visual cleanup as a substitute for listening.\n\nThis also keeps the buffer-based approach within its natural limits. Decoded PCM is much larger than MP3 or AAC, and the design holds two versions in memory. `decodeAudioData()`\n\nexpects a complete file, too. For a long recording, generate a bounded preview, decode chunks, or move to a more deliberate streaming strategy.\n\nOn the browser side, expect autoplay blocking, mobile interruptions, codec failures, and aggressive toggling. Resume the `AudioContext`\n\nfrom a user gesture and make decode failure an ordinary UI state. MDN's [autoplay guide](https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Autoplay) is worth keeping nearby.\n\nDo not switch between two video players. Keep one video element on screen and treat its timeline as the master clock. Mute its embedded audio, then audition aligned original and processed audio tracks through the A/B graph.\n\nThe video should drive playback, pause, seek, and rate-change events. After a seek, stop both audio sources and recreate them from `video.currentTime`\n\n. If playback speed changes, apply the same `playbackRate`\n\nto both new sources.\n\nThere is an important limit: `HTMLMediaElement.currentTime`\n\nand `AudioContext.currentTime`\n\nare different clocks. Listening for events does not guarantee sample-accurate sync across browsers. Check drift during playback and rebuild both audio sources at `video.currentTime`\n\nwhen it exceeds your tolerance. For short speech previews, this is often sufficient. Frame-accurate editing needs a shared media timeline built with a more controlled pipeline such as MSE or WebCodecs.\n\nThe processing backend must still preserve duration and compensate for model and codec delay. A perfect front-end clock cannot align files whose content starts at different offsets.\n\nAn AI tool naturally wants to say \"enhancement complete\" and move on to download. But the processed version may have less noise and worse speech. Both can be true.\n\nI prefer plain labels such as `Original`\n\nand `Processed`\n\n, with neither option visually dominant. The preview is not decoration after a successful model run. It is how the user catches a bad run before committing to it.\n\nWhile working on [CleanAudio](https://www.cleanaudio.io/), this became a useful product principle for me: when AI changes a user's media, confidence should come from comparison, not from the success message.\n\nBefore shipping, I listen for the unglamorous failures: a transient that moves when I switch, a click at the boundary, drift after seeking a video, or a phone interruption that leaves the controls lying about playback.\n\nThe code is the smaller part. The harder part is resisting all the little ways a comparison can be made to favor the result we want users to choose.\n\n*Disclosure: I am involved in building CleanAudio, an AI audio and video noise-removal tool. The implementation pattern and opinions in this article are presented as general product and engineering guidance.*", "url": "https://wpnews.pro/news/how-to-build-a-fair-a-b-audio-preview-for-ai-processing", "canonical_source": "https://dev.to/yidao_713c5eeea4f16821823/how-to-build-a-fair-ab-audio-preview-for-ai-processing-52ne", "published_at": "2026-08-24 09:32:34+00:00", "updated_at": "2026-08-24 09:43:42.257014+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence"], "entities": ["Web Audio API", "MDN", "AudioBuffer", "AudioContext", "GainNode"], "alternates": {"html": "https://wpnews.pro/news/how-to-build-a-fair-a-b-audio-preview-for-ai-processing", "markdown": "https://wpnews.pro/news/how-to-build-a-fair-a-b-audio-preview-for-ai-processing.md", "text": "https://wpnews.pro/news/how-to-build-a-fair-a-b-audio-preview-for-ai-processing.txt", "jsonld": "https://wpnews.pro/news/how-to-build-a-fair-a-b-audio-preview-for-ai-processing.jsonld"}}