Learn · Troubleshooting

When your vibe-coded plugin breaks

Almost every first plugin hits the same handful of problems: silence, clicks, a delay that screams, chords that distort, a sample that won’t load, a DAW that can’t find the build. Here’s what each one usually means and how to fix it, with the exact code to ask your AI assistant for.

Before you change anything

Most debugging time is lost to guessing. Two habits cut it dramatically:

  • Open the console. In Chrome or Edge press Cmd+Option+J (Mac) or Ctrl+Shift+J (Windows). A red error with a line number is worth more than any description of the symptom.
  • Save a known-good version. Commit with git, or just duplicate the folder, before every new feature. When something breaks you can compare against the last version that worked instead of asking the AI to “fix it” blind.

Quick map: symptom to likely cause

You hear or seeMost likely causeJump to
Nothing at allSuspended AudioContext, or a node never connected to the outputNo sound
A click when a note starts or stopsGain jumping instantly instead of rampingClicks and pops
Crunchy stepping while you turn a knobParameter set directly with no smoothingZipper noise
A delay that keeps getting louderFeedback at or above 1.0Runaway feedback
Fine with one note, distorted with chordsVoices summing past full scaleClipping chords
Gets slower or glitchier the longer you playOld voices never stopped or disconnectedVoices piling up
Sample loads in one setup, not anotherOpening the page from file://Samples won’t load
Native build missing from the DAWWrong folder, no rescan, or failed validationDAW can’t see it

No sound at all

Browsers won’t let a page make sound until the user interacts with it, so an AudioContext created on page load starts suspended. Nothing errors; it just stays silent. Resume it from a click:

const ctx = new AudioContext();

document.querySelector('#start').addEventListener('click', async () => {
  if (ctx.state === 'suspended') await ctx.resume();
  // start your synth here
});

If the context is running and it’s still silent, something isn’t connected to ctx.destination. Log ctx.state, then trace the chain from the source to the output. A gain node left at 0, or a filter with its cutoff at 20 Hz, is just as silent as a missing connection.

Prompt that works: “The page is silent. Log ctx.state after I press Start, and list every node between the oscillator and ctx.destination with its current gain or frequency.”

Clicks and pops on notes

A click is a sudden jump in the waveform. The usual culprit is code that sets gain.value = 1 to start a note and calls osc.stop() while it’s still loud. Give every note a short envelope, even a 5 ms one:

function noteOn(gainNode, t = ctx.currentTime) {
  gainNode.gain.cancelScheduledValues(t);
  gainNode.gain.setValueAtTime(0, t);
  gainNode.gain.linearRampToValueAtTime(0.8, t + 0.01);   // 10 ms attack
}

function noteOff(gainNode, osc, release = 0.3, t = ctx.currentTime) {
  gainNode.gain.cancelScheduledValues(t);
  gainNode.gain.setValueAtTime(gainNode.gain.value, t);
  gainNode.gain.setTargetAtTime(0, t, release / 4);
  osc.stop(t + release + 0.05);                           // stop after it's silent
}

Zipper noise when you move a knob

Assigning filter.frequency.value = x on every slider event makes the value jump in steps you can hear. Glide to the new value instead:

slider.addEventListener('input', e => {
  filter.frequency.setTargetAtTime(+e.target.value, ctx.currentTime, 0.02);
});

The same problem exists in native plugins. In JUCE the fix is juce::SmoothedValue: set a target when the parameter changes and read the smoothed value per sample in processBlock.

Runaway feedback in a delay

If the feedback gain can reach 1.0, each repeat is as loud as the last; above it, they grow until everything distorts. Cap it, and put a filter in the loop so repeats darken and fade naturally:

const delay = ctx.createDelay(2.0);
const fb    = ctx.createGain();
const tone  = ctx.createBiquadFilter();
tone.type = 'lowpass';
tone.frequency.value = 4000;
tone.Q.value = -3.01;   // flat: the default Q adds a resonant peak that makes echoes grow

input.connect(delay);
delay.connect(tone).connect(fb).connect(delay);   // the loop
delay.connect(wet);

function setFeedback(v) {
  fb.gain.setTargetAtTime(Math.min(v, 0.92), ctx.currentTime, 0.02);
}

Set the filter’s Q as well as the cap. Web Audio’s default lowpass Q adds a small resonant peak, and inside a loop that peak can push the repeats louder even at 0.9 feedback. The feedback delay guide shows the measurement.

Write the cap into your scope document so the AI doesn’t “helpfully” raise the slider range to 1.2 later.

Fine with one note, distorted with chords

Four voices at gain 0.8 sum to 3.2, well past the 1.0 the output can handle. Give voices a lower level, route them through one master gain, and add a gentle limiter as a safety net rather than a fix:

const master = ctx.createGain();
master.gain.value = 0.5;
const safety = ctx.createDynamicsCompressor();
safety.threshold.value = -6;
safety.ratio.value = 12;
master.connect(safety).connect(ctx.destination);

// each voice: voiceGain peak around 0.25, connected to master

Voices piling up and CPU climbing

Web Audio oscillators and buffer sources are one-shot: once stopped they can’t restart, and every new note needs a new one. That’s fine, but code that never stops old voices, or rebuilds the whole graph on each slider move, leaks nodes until the audio starts crackling. Stop each voice after its release, disconnect it in onended, and keep long-lived nodes (filters, delays, master gain) created once:

osc.onended = () => { osc.disconnect(); voiceGain.disconnect(); };

Samples won’t load

Double-clicking index.html opens it from file://, and browsers block fetch() of local files from there, so decodeAudioData never gets the sample. Serve the folder instead:

cd your-project
python3 -m http.server 8000
# then open http://localhost:8000

If it still fails, check the console for a 404 (wrong path or filename case) or a decode error (an unusual format; try a 16-bit or 24-bit WAV).

Your native build doesn’t show up in the DAW

Once you move from a browser prototype to a native build (see browser prototype to VST/AU), the problems change. Check these in order:

  1. Right folder. On macOS, Audio Units go in ~/Library/Audio/Plug-Ins/Components and VST3 bundles in ~/Library/Audio/Plug-Ins/VST3. On Windows, VST3 goes in C:\Program Files\Common Files\VST3.
  2. Rescan. Most DAWs cache their plugin list. Use the DAW’s rescan option or restart it. Some hosts need their VST3 system folders switched on in preferences first.
  3. Validation. Logic only lists Audio Units that pass Apple’s auval check. Run auval -a to find your plugin’s codes, then auval -v aufx Abcd Manu with your own codes, and read the first failure. The free pluginval tool tests VST3 and AU builds the same way.
  4. Gatekeeper. A build you made yourself on the same Mac normally loads. An unsigned build copied from elsewhere may be quarantined; removing the quarantine flag with xattr -dr com.apple.quarantine on the bundle is only appropriate for builds you trust.

When the AI keeps breaking working code

This is a process problem more than a code problem, and it has a process fix:

  • One change per prompt. “Add reverb and presets and MIDI” makes it impossible to tell which change broke the sound.
  • Paste the real error. Copy the console message verbatim, with its line number, plus what you expected and what you heard.
  • Point back to the scope. Keep a SCOPE.md in the project and say “re-read SCOPE.md before editing.” See scope your first plugin.
  • Roll back early. If two fix attempts haven’t worked, return to the last good version and ask for the feature again in smaller steps.

A good bug report to your assistant: “After adding the filter LFO, notes click on release. Expected: smooth 300 ms fade. Console is clean. Here is noteOff(): … Fix only the release; don’t change the LFO.”

Keep going

Related on the blog: Common mistakes · Web Audio API for musicians · Prompt chain for audio plugins

Want the full troubleshooting guide?

Spec → Scaffold includes a longer troubleshooting guide (auval and pluginval failures, CMake and Xcode errors, blank WebView UIs, denormals, reading logs), three worked examples, and a step-by-step browser-to-JUCE path, alongside a scoped spec, playable browser prototype and prompt chain for your own idea. It is not a compiled VST/AU. Questions? [email protected]