<?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: yidao</title>
    <description>The latest articles on DEV Community by yidao (@ethanbuilds).</description>
    <link>https://dev.to/ethanbuilds</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%2F3818086%2F60e3e2e2-23db-471f-994e-41a2627f434a.jpg</url>
      <title>DEV Community: yidao</title>
      <link>https://dev.to/ethanbuilds</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ethanbuilds"/>
    <language>en</language>
    <item>
      <title>We Split One Research Loop Across Three Agents. The Handoffs Became the Work.</title>
      <dc:creator>yidao</dc:creator>
      <pubDate>Mon, 07 Sep 2026 07:26:32 +0000</pubDate>
      <link>https://dev.to/ethanbuilds/we-split-one-research-loop-across-three-agents-the-handoffs-became-the-work-15b2</link>
      <guid>https://dev.to/ethanbuilds/we-split-one-research-loop-across-three-agents-the-handoffs-became-the-work-15b2</guid>
      <description>&lt;p&gt;I recently tried to turn a difficult research workflow into a small AI team.&lt;/p&gt;

&lt;p&gt;The task involved improving an SVG reconstruction system. A researcher would inspect failures and propose a cause. An engineer would change the implementation. An evaluator would examine the result and decide whether another round was needed.&lt;/p&gt;

&lt;p&gt;On paper, the roles looked sensible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Researcher -&amp;gt; Engineer -&amp;gt; Evaluator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice, the team became very good at producing handoffs.&lt;/p&gt;

&lt;p&gt;Six execution and handoff files grew to 12,757 lines. Over two days, five core workflow files received 125 commits; 116 of those commits repaired orchestration, bindings, evaluation state, or other workflow mechanics rather than advancing the reconstruction method itself. One version of the workflow called the full Researcher 47 times and spent 12,813 seconds doing so. We could identify only one call that produced a useful new algorithmic conclusion.&lt;/p&gt;

&lt;p&gt;Those numbers come from one internal experiment, not a general benchmark. But they made the failure mode hard to ignore: the system was spending more effort preserving the appearance of a team than preserving the reasoning needed to solve the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The task was one loop, not three jobs
&lt;/h2&gt;

&lt;p&gt;The mistake was easiest to see after we stopped looking at the agent roles and looked at the information flow.&lt;/p&gt;

&lt;p&gt;The real loop was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;observe an output
-&amp;gt; form a causal hypothesis
-&amp;gt; change the algorithm
-&amp;gt; inspect the new output
-&amp;gt; revise the hypothesis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step depends heavily on the previous one. A small visual detail can change the explanation. A code change can reveal that the original diagnosis was wrong. The next useful action often depends on something that is obvious while looking at the output but awkward to encode in a handoff document.&lt;/p&gt;

&lt;p&gt;We had divided the loop according to familiar human job titles. We had not divided it according to independent information boundaries.&lt;/p&gt;

&lt;p&gt;That distinction mattered. The Researcher compressed an observation into a report. The Engineer had to reconstruct the observation from that report before making a change. The Evaluator then reconstructed both the hypothesis and the intent of the change from another bundle of files. Every transition discarded context and introduced another opportunity for the agents to interpret the same state differently.&lt;/p&gt;

&lt;p&gt;The reports kept getting longer because we tried to solve information loss by adding more information. That only made the contracts more expensive to produce and validate.&lt;/p&gt;

&lt;h2&gt;
  
  
  We used language models for compiler work
&lt;/h2&gt;

&lt;p&gt;A second problem was more mundane.&lt;/p&gt;

&lt;p&gt;Agents were repeatedly asked to maintain case identifiers, file paths, manifests, hashes, schema fields, evaluation bindings, and status records. These are exact transformations with exact failure conditions. They belong in ordinary code.&lt;/p&gt;

&lt;p&gt;Instead, the system sometimes called an expensive reasoning agent to discover that a path was stale or a required field was missing. The agent would repair the record, explain the repair, and pass the corrected record to the next agent. The workflow looked active, but no research had happened.&lt;/p&gt;

&lt;p&gt;Once we separated the calls by purpose, many of the so-called research iterations turned out to be handoff repair. A useful rule emerged:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If a machine can verify the answer exactly, do not make an agent negotiate it through prose.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We moved IDs, paths, hashes, schemas, manifests, and basic evaluation contracts into deterministic checks. Agent time was reserved for interpreting evidence and choosing what to try next.&lt;/p&gt;

&lt;h2&gt;
  
  
  More agents were maintaining different versions of reality
&lt;/h2&gt;

&lt;p&gt;The third problem was state.&lt;/p&gt;

&lt;p&gt;Research work changes its own premises. After each experiment, the best explanation of the defect may change. In our design, several agents were operating on compressed snapshots of that moving state. None of them had quite the same picture, and each handoff could lag behind the actual implementation.&lt;/p&gt;

&lt;p&gt;This resembles what Cognition describes in its discussion of &lt;a href="https://cognition.com/blog/multi-agents-working" rel="noopener noreferrer"&gt;multi-agent coding&lt;/a&gt;: parallel writers make implicit choices that can conflict, while context does not naturally move between agents. Their useful pattern is closer to one writer receiving intelligence from other agents, followed by an independent review.&lt;/p&gt;

&lt;p&gt;The comparison helped us see that our problem was not insufficient communication. It was shared decision authority over one changing implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The replacement was deliberately less theatrical
&lt;/h2&gt;

&lt;p&gt;We replaced the role-based team with one continuous owner of the research loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Research Lead
├─ inspect the real output
├─ form and revise the causal model
├─ implement one candidate
└─ optionally open two isolated hypothesis lanes
             |
             v
deterministic checks + independent evaluator
             |
             v
human perceptual review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Research Lead is the only decision owner and the only writer for the active implementation. It keeps observation, hypothesis, implementation, and immediate inspection in the same working context.&lt;/p&gt;

&lt;p&gt;This is not an argument for one enormous prompt or an agent that runs forever. The context has a lifecycle. It stays continuous while investigating one defect or one causal question. Once that question is resolved, rejected, or blocked, we archive the evidence and begin the next investigation with a cleaner context.&lt;/p&gt;

&lt;p&gt;We still use additional agents, but their authority is narrower.&lt;/p&gt;

&lt;p&gt;A read-only agent can search documentation or inspect a separate part of the repository. If two causal explanations can be tested independently, we can open two isolated lanes and compare their evidence. An evaluator receives a frozen target, the candidate output, the diff, and a rubric, but not the Lead's reasoning history. It can reject a candidate or report uncertainty; it cannot silently rewrite the implementation.&lt;/p&gt;

&lt;p&gt;The final visual judgment remains human. A deterministic evaluator can detect empty output, structural damage, regressions, or missed constraints. It cannot reliably declare that a reconstruction simply looks right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-agent systems work when the work is actually parallel
&lt;/h2&gt;

&lt;p&gt;This experiment did not convince me that multi-agent systems are useless. It made the boundary clearer.&lt;/p&gt;

&lt;p&gt;Anthropic reports that its multi-agent Research system outperformed a single agent by 90.2% on an internal research evaluation. The example is breadth-first search: multiple subagents can investigate independent directions at the same time. Anthropic also notes that the architecture consumes substantially more tokens, so the extra coordination needs to buy something real. That is a good fit for parallel discovery, not necessarily for a tightly coupled implementation loop. The full account is worth reading in &lt;a href="https://www.anthropic.com/engineering/multi-agent-research-system" rel="noopener noreferrer"&gt;How we built our multi-agent research system&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A controlled study across 180 agent configurations found a similar task dependency. Centralized coordination improved performance on parallelizable tasks, while every tested multi-agent variant degraded performance on sequential reasoning tasks. The paper, &lt;a href="https://arxiv.org/abs/2512.08296" rel="noopener noreferrer"&gt;Towards a Science of Scaling Agent Systems&lt;/a&gt;, is useful because it treats topology as a property to match to the task rather than a maturity ladder where more agents must be better.&lt;/p&gt;

&lt;p&gt;OpenAI's practical guide makes a simpler recommendation: &lt;a href="https://openai.com/business/guides-and-resources/a-practical-guide-to-building-ai-agents/" rel="noopener noreferrer"&gt;maximize a single agent's capabilities first&lt;/a&gt;, then split when complex logic or tool selection actually requires it.&lt;/p&gt;

&lt;p&gt;That is now our default. Parallelize across independent repositories, pages, migrations, searches, audits, or falsifiable hypotheses. Keep writes single-threaded when several steps depend on the same evolving mental model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure conclusions, not activity
&lt;/h2&gt;

&lt;p&gt;The original workflow generated plenty of visible motion: calls, reports, commits, validations, and candidate outputs. None of those was a good measure of research progress.&lt;/p&gt;

&lt;p&gt;For the next comparison, we care about a smaller set of outcomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;time to the first human-validated improvement&lt;/li&gt;
&lt;li&gt;cost per useful causal conclusion&lt;/li&gt;
&lt;li&gt;percentage of agent calls spent repairing handoffs&lt;/li&gt;
&lt;li&gt;workflow failure rate&lt;/li&gt;
&lt;li&gt;quality improvement under an equal token and time budget&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Throughput still matters when tasks are independent. In a tightly coupled research loop, it can be actively misleading. Ten candidates produced from the same broken causal model are not ten times the progress.&lt;/p&gt;

&lt;p&gt;The main lesson was not that we needed a better manager agent, a stricter schema, or another reviewer. We had placed parallelism at the level of roles, where the information dependencies were strongest.&lt;/p&gt;

&lt;p&gt;The better design was to keep one owner for the changing explanation and move parallelism outward, toward work that could genuinely be isolated.&lt;/p&gt;




&lt;p&gt;*AI assistance disclosure: AI tools assisted with drafting and editing this article. The experiment data, architecture decisions, and final review are the author's.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Don't Send Every Audio File Straight to the AI Model</title>
      <dc:creator>yidao</dc:creator>
      <pubDate>Tue, 01 Sep 2026 04:01:28 +0000</pubDate>
      <link>https://dev.to/ethanbuilds/dont-send-every-audio-file-straight-to-the-ai-model-b2j</link>
      <guid>https://dev.to/ethanbuilds/dont-send-every-audio-file-straight-to-the-ai-model-b2j</guid>
      <description>&lt;p&gt;An audio file can pass every technical checkpoint and still produce a bad AI result.&lt;/p&gt;

&lt;p&gt;The upload completes. The decoder opens it. The model returns a response. The encoder writes a playable file. From the system's point of view, the job succeeded.&lt;/p&gt;

&lt;p&gt;Then the user presses play and hears a voice that is still too quiet, missing pieces, or buried under artifacts.&lt;/p&gt;

&lt;p&gt;It is tempting to blame the model immediately. Sometimes that is fair. But many bad jobs begin earlier, when the product treats every decodable file as a valid model input.&lt;/p&gt;

&lt;p&gt;A recording can be almost silent, heavily clipped, mostly empty, unexpectedly multichannel, or far outside the conditions a model was trained to handle. Sending all of those through the same pipeline with the same settings turns predictable input problems into mysterious model failures.&lt;/p&gt;

&lt;p&gt;I prefer to put a small policy layer in front of inference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;upload -&amp;gt; probe -&amp;gt; inspect -&amp;gt; route -&amp;gt; process -&amp;gt; validate -&amp;gt; preview
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal is not to diagnose audio perfectly. It is to catch obvious problems, choose a safer path when possible, and avoid claiming success when the result cannot be trusted.&lt;/p&gt;

&lt;h2&gt;
  
  
  A container is not the signal
&lt;/h2&gt;

&lt;p&gt;Start with the inexpensive facts. Does the file contain an audio stream? What codec, duration, sample rate, channel count, and channel layout did the decoder find?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ffprobe&lt;/code&gt; can return that metadata as JSON without decoding the whole signal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffprobe &lt;span class="nt"&gt;-v&lt;/span&gt; error &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-select_streams&lt;/span&gt; a:0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-show_entries&lt;/span&gt; &lt;span class="nv"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;codec_name,sample_rate,channels,channel_layout:format&lt;span class="o"&gt;=&lt;/span&gt;duration &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-of&lt;/span&gt; json &lt;span class="se"&gt;\&lt;/span&gt;
  input.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://ffmpeg.org/ffprobe.html" rel="noopener noreferrer"&gt;&lt;code&gt;ffprobe&lt;/code&gt; documentation&lt;/a&gt; covers &lt;code&gt;-show_entries&lt;/code&gt;, stream selection, and its JSON writer in more detail.&lt;/p&gt;

&lt;p&gt;These checks catch more than malformed uploads. They also stop quiet assumptions from spreading through the pipeline. A speech model expecting mono audio should not discover a six-channel layout halfway through inference. Blindly downmixing is not always harmless either: channels can contain different microphones, or partially cancel when combined.&lt;/p&gt;

&lt;p&gt;Metadata still cannot tell us whether the file contains useful audio. For that, inspect at least a bounded portion of the decoded signal. Useful first-pass measurements include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;peak and RMS level&lt;/li&gt;
&lt;li&gt;long silent regions&lt;/li&gt;
&lt;li&gt;samples at or near full scale&lt;/li&gt;
&lt;li&gt;active audio or speech ratio&lt;/li&gt;
&lt;li&gt;channel imbalance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;FFmpeg's &lt;a href="https://ffmpeg.org/ffmpeg-filters.html#astats-1" rel="noopener noreferrer"&gt;&lt;code&gt;astats&lt;/code&gt; and &lt;code&gt;silencedetect&lt;/code&gt; filters&lt;/a&gt; are enough for a basic server-side pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-hide_banner&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-af&lt;/span&gt; &lt;span class="s2"&gt;"astats=metadata=1:reset=0,silencedetect=n=-50dB:d=1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; null -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The silence level and duration above are examples, not standards. A whispered interview, a screen recording, and a field recording should not share thresholds just because they are all audio files. For speech products, a voice activity detector is also more useful than treating every non-silent sound as speech.&lt;/p&gt;

&lt;h2&gt;
  
  
  Low level is a routing decision
&lt;/h2&gt;

&lt;p&gt;One especially awkward case is a recording whose digital level is extremely low.&lt;/p&gt;

&lt;p&gt;The obvious fix is to normalize it before inference. That can help place the signal inside a model's expected operating range, but it does not create information that was never captured. Raising gain lifts the voice and the noise floor together. It does not improve signal-to-noise ratio, repair a poor microphone position, or restore detail lost during recording.&lt;/p&gt;

&lt;p&gt;This is why I would not normalize every upload to the same target. First decide whether the input is unusually low for the pipeline. If it is, apply bounded gain with headroom, then run the model. If the recording is already clipped or contains almost no usable speech, gain is the wrong intervention.&lt;/p&gt;

&lt;p&gt;While working on &lt;a href="https://www.cleanaudio.io/?utm_source=devto" rel="noopener noreferrer"&gt;CleanAudio&lt;/a&gt;, this distinction became important: very low-level inputs may need a calibration step, but that is input conditioning, not a promise that every result should have the same loudness.&lt;/p&gt;

&lt;p&gt;The thresholds belong to the model and the use case. They should come from tested failures, not from numbers copied out of a mastering guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn measurements into a plan
&lt;/h2&gt;

&lt;p&gt;I find it more useful to produce a processing plan than a single &lt;code&gt;valid&lt;/code&gt; boolean. A file can be valid enough to decode but still deserve conservative processing or manual review.&lt;/p&gt;

&lt;p&gt;Here is a simplified policy in TypeScript:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AudioInspection&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;decodable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;audioStreamCount&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;durationSeconds&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;peakDbfs&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;rmsDbfs&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;clippedSampleRatio&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;activeAudioRatio&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="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AudioPolicy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;minDurationSeconds&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;maxDurationSeconds&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;minActiveAudioRatio&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;maxClippedSampleRatio&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;calibrateBelowDbfs&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;targetInputRmsDbfs&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;maxCalibrationGainDb&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;maxAllowedPeakDbfs&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="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ProcessingPlan&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;disposition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;process&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reject&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;standard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;conservative&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;preGainDb&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;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;planAudio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AudioInspection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AudioPolicy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ProcessingPlan&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;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&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;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decodable&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audioStreamCount&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="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="na"&gt;disposition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reject&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;standard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;preGainDb&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;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No decodable audio stream&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="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;input&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;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minDurationSeconds&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;input&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;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxDurationSeconds&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="na"&gt;disposition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reject&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;standard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;preGainDb&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;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Duration is outside the supported range&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="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;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeAudioRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minActiveAudioRatio&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="na"&gt;disposition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;conservative&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;preGainDb&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;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Too little active audio was detected&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="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProcessingPlan&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mode&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;standard&lt;/span&gt;&lt;span class="dl"&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;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clippedSampleRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxClippedSampleRatio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;conservative&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Input contains substantial clipping&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;preGainDb&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rmsDbfs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;calibrateBelowDbfs&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;gainTowardTarget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;targetInputRmsDbfs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rmsDbfs&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;availableHeadroom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxAllowedPeakDbfs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;peakDbfs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;preGainDb&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;0&lt;/span&gt;&lt;span class="p"&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;gainTowardTarget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;availableHeadroom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxCalibrationGainDb&lt;/span&gt;&lt;span class="p"&gt;,&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;preGainDb&lt;/span&gt; &lt;span class="o"&gt;&amp;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;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Low-level input needs bounded gain&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;disposition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;process&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;preGainDb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&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;There are deliberately no magic values in this example. A threshold is part of the product policy, not a universal property of audio. Version it alongside the model, record which route each job takes, and review the files clustered near a boundary.&lt;/p&gt;

&lt;p&gt;This policy layer also makes failures easier to explain. "We could not detect enough usable audio" is more actionable than "processing failed." A conservative route can preserve more of the original signal instead of applying the strongest available effect to a risky input.&lt;/p&gt;

&lt;h2&gt;
  
  
  The output needs a postflight check
&lt;/h2&gt;

&lt;p&gt;A successful inference request only proves that inference ran. Before offering the result, decode it again and check the boring invariants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;expected duration and timeline were preserved&lt;/li&gt;
&lt;li&gt;output is not silent or unexpectedly clipped&lt;/li&gt;
&lt;li&gt;sample rate and channel mapping are supported&lt;/li&gt;
&lt;li&gt;the encoder produced a complete, playable file&lt;/li&gt;
&lt;li&gt;any known model or codec delay was compensated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Compare those measurements with the input, not only with fixed limits. A large, unexplained duration change is suspicious even when both files are individually valid.&lt;/p&gt;

&lt;p&gt;These checks catch catastrophic failures. They cannot tell whether consonants were softened, room tone started pumping, or a voice became less natural. That still requires listening. In a previous article, I described a &lt;a href="https://dev.to/yidao_713c5eeea4f16821823/how-to-build-a-fair-ab-audio-preview-for-ai-processing-52ne"&gt;synchronized A/B preview&lt;/a&gt; for making that comparison without restarting two separate players.&lt;/p&gt;

&lt;p&gt;The distinction matters: automated validation protects the pipeline, while the preview protects the user's judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let the product admit uncertainty
&lt;/h2&gt;

&lt;p&gt;AI interfaces often compress several states into one green message: &lt;code&gt;Enhancement complete&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But there is a meaningful difference between a model completing, a file passing structural checks, and a person deciding that the result is better. Treating them as the same event makes the product sound more certain than the system actually is.&lt;/p&gt;

&lt;p&gt;The original file should remain available. Risky inputs should get a clear explanation. A result that passes only basic checks should still be presented as something to review, not as an unquestionable improvement.&lt;/p&gt;

&lt;p&gt;The model is only one stage in the feature. A small amount of inspection before and after it can prevent predictable failures, make routing decisions visible, and give the user a more honest result.&lt;/p&gt;

&lt;p&gt;Sometimes the best AI processing decision is to do less. Sometimes it is not to run the model at all.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI assistance disclosure: AI tools assisted with drafting and editing this article. The technical direction, product observations, and final review are the author's.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>podcast</category>
      <category>audio</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How to Build a Fair A/B Audio Preview for AI Processing</title>
      <dc:creator>yidao</dc:creator>
      <pubDate>Mon, 24 Aug 2026 09:32:34 +0000</pubDate>
      <link>https://dev.to/ethanbuilds/how-to-build-a-fair-ab-audio-preview-for-ai-processing-52ne</link>
      <guid>https://dev.to/ethanbuilds/how-to-build-a-fair-ab-audio-preview-for-ai-processing-52ne</guid>
      <description>&lt;p&gt;Two audio players do not make a fair before-and-after test.&lt;/p&gt;

&lt;p&gt;If 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.&lt;/p&gt;

&lt;p&gt;That is a weak way to evaluate any audio effect. It is especially weak for AI processing.&lt;/p&gt;

&lt;p&gt;A 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.&lt;/p&gt;

&lt;p&gt;The preview therefore has one job: let the listener switch quickly enough to hear both the improvement and the damage.&lt;/p&gt;

&lt;p&gt;The 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.&lt;/p&gt;

&lt;p&gt;Two independent &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; elements 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.&lt;/p&gt;

&lt;p&gt;A better interface has one transport and one version control:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Play ]  [ Original | Processed ]  00:18 ━━━━━━━ 00:42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transport decides &lt;em&gt;where&lt;/em&gt; playback happens. The segmented control decides &lt;em&gt;which signal&lt;/em&gt; is audible.&lt;/p&gt;

&lt;h2&gt;
  
  
  One transport, two signals
&lt;/h2&gt;

&lt;p&gt;For a short preview, I decode both files into &lt;code&gt;AudioBuffer&lt;/code&gt;s, start them at the same &lt;code&gt;AudioContext&lt;/code&gt; time and offset, and route each through its own &lt;code&gt;GainNode&lt;/code&gt;. Both sources run; only one gain is open.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;decodeAudioData()&lt;/code&gt; decodes complete file data and resamples it to the context's sample rate. The decoded buffers can then share the same audio clock. See the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/decodeAudioData" rel="noopener noreferrer"&gt;MDN documentation&lt;/a&gt; for format and loading details.&lt;/p&gt;

&lt;p&gt;The core is small:&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;const&lt;/span&gt; &lt;span class="nx"&gt;context&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;originalGain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createGain&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;processedGain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createGain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;originalGain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;processedGain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&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;loadBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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;context&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&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="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;original&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;loadBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/audio/original.wav&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;processed&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;loadBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/audio/processed.wav&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;sources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startPair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fromSeconds&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;when&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.03&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;duration&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;buffers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;original&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;buffers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;processed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;stopPair&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;sources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffers&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buffer&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createBufferSource&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;original&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;originalGain&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;processedGain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;when&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fromSeconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;fromSeconds&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;source&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;setAudibleVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activeVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;when&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="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stopPair&lt;/span&gt;&lt;span class="p"&gt;()&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;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;sources&lt;/span&gt; &lt;span class="o"&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;Call &lt;code&gt;context.resume()&lt;/code&gt; and &lt;code&gt;startPair()&lt;/code&gt; from 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 &lt;code&gt;AudioBufferSourceNode&lt;/code&gt; is intentionally single-use, while its decoded &lt;code&gt;AudioBuffer&lt;/code&gt; can be reused. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/AudioBufferSourceNode" rel="noopener noreferrer"&gt;MDN explains the lifecycle here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The switch itself can lie
&lt;/h2&gt;

&lt;p&gt;An 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.&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;const&lt;/span&gt; &lt;span class="nx"&gt;versionGains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;original&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;originalGain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;processed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;processedGain&lt;/span&gt;&lt;span class="p"&gt;,&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;activeVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;original&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setAudibleVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ramp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.012&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;activeVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;version&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;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;versionGains&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;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;version&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancelScheduledValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&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;ramp&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="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValueAtTime&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="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValueAtTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;linearRampToValueAtTime&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="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ramp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#show-original&lt;/span&gt;&lt;span class="dl"&gt;"&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="s2"&gt;click&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setAudibleVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;original&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#show-processed&lt;/span&gt;&lt;span class="dl"&gt;"&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="s2"&gt;click&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setAudibleVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;processed&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 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The player cannot repair a misaligned file
&lt;/h2&gt;

&lt;p&gt;Starting both buffers at the same Web Audio time only works if the files contain corresponding samples at corresponding positions.&lt;/p&gt;

&lt;p&gt;An AI pipeline may introduce delay through frame padding, look-ahead context, silence trimming, resampling, or codec priming.&lt;/p&gt;

&lt;p&gt;If the processed voice begins 80 ms later, the browser will faithfully play the wrong moments together.&lt;/p&gt;

&lt;p&gt;Fix 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.&lt;/p&gt;

&lt;p&gt;Test this with a transient: switch repeatedly and confirm that its position does not move.&lt;/p&gt;

&lt;h2&gt;
  
  
  Most of the file does not matter
&lt;/h2&gt;

&lt;p&gt;Most 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.&lt;/p&gt;

&lt;p&gt;A 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.&lt;/p&gt;

&lt;p&gt;The 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.&lt;/p&gt;

&lt;p&gt;This 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. &lt;code&gt;decodeAudioData()&lt;/code&gt; expects a complete file, too. For a long recording, generate a bounded preview, decode chunks, or move to a more deliberate streaming strategy.&lt;/p&gt;

&lt;p&gt;On the browser side, expect autoplay blocking, mobile interruptions, codec failures, and aggressive toggling. Resume the &lt;code&gt;AudioContext&lt;/code&gt; from a user gesture and make decode failure an ordinary UI state. MDN's &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Autoplay" rel="noopener noreferrer"&gt;autoplay guide&lt;/a&gt; is worth keeping nearby.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes when the source is video?
&lt;/h2&gt;

&lt;p&gt;Do 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.&lt;/p&gt;

&lt;p&gt;The video should drive playback, pause, seek, and rate-change events. After a seek, stop both audio sources and recreate them from &lt;code&gt;video.currentTime&lt;/code&gt;. If playback speed changes, apply the same &lt;code&gt;playbackRate&lt;/code&gt; to both new sources.&lt;/p&gt;

&lt;p&gt;There is an important limit: &lt;code&gt;HTMLMediaElement.currentTime&lt;/code&gt; and &lt;code&gt;AudioContext.currentTime&lt;/code&gt; are different clocks. Listening for events does not guarantee sample-accurate sync across browsers. Check drift during playback and rebuild both audio sources at &lt;code&gt;video.currentTime&lt;/code&gt; when 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.&lt;/p&gt;

&lt;p&gt;The 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The preview is part of error handling
&lt;/h2&gt;

&lt;p&gt;An 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.&lt;/p&gt;

&lt;p&gt;I prefer plain labels such as &lt;code&gt;Original&lt;/code&gt; and &lt;code&gt;Processed&lt;/code&gt;, 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.&lt;/p&gt;

&lt;p&gt;While working on &lt;a href="https://www.cleanaudio.io/" rel="noopener noreferrer"&gt;CleanAudio&lt;/a&gt;, 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.&lt;/p&gt;

&lt;p&gt;Before 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.&lt;/p&gt;

&lt;p&gt;The 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.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webaudioapi</category>
      <category>javascript</category>
      <category>ai</category>
      <category>ux</category>
    </item>
  </channel>
</rss>
