Skip to content

Use Claude Code to correct and refine#1

Open
jonhoo wants to merge 12 commits intomainfrom
llm-review-and-iterate
Open

Use Claude Code to correct and refine#1
jonhoo wants to merge 12 commits intomainfrom
llm-review-and-iterate

Conversation

@jonhoo
Copy link
Copy Markdown
Owner

@jonhoo jonhoo commented Apr 4, 2026

As perhaps one useful data point for some of you as to how LLMs can be useful in coding tasks, this branch includes some polish work I did on trough after the stream ended through Claude Code. I highly recommend you read the individual commits (description and contents) in full. I have, and found it both educational and interesting.

I also had Claude summarize the branch as a whole using this prompt:

summarize what has happened on this branch since it branched off of main. the resulting summary will be used as a PR description for the branch as a whole. readers will generally be skeptical but also curious about the utility of LLMs and agentic coding, and i'm hoping that this branch can show them ways in which LLMs can be very helpful, especially in review and in leveraging domain knowledge in a controlled fashion. make sure to also note down how much at-keyboard time the LLM-based work here took (1 hour).

and here's what it came up with:

Total at-keyboard time: ~1 hour.

This branch explores using an LLM (Claude Opus 4.6) as a domain-knowledgeable reviewer and implementer for audio DSP code — a colored noise generator that synthesizes white, pink, brownian, blue, violet, and grey noise via frequency-domain spectral shaping and inverse FFT.

What happened

The workflow was: point the LLM at the existing code, ask it to audit for correctness, review its findings, then direct it to fix the real issues and improve the areas that mattered. The human role was to evaluate each finding, decide what to act on, steer the implementation direction, and verify the results by listening to the output.

Phase 1: Correctness audit

The LLM was asked to assess algorithmic accuracy (not code quality). It found four genuine bugs:

  • RIFF chunk size off by 8 bytes (c38f74e): The WAV header understated the file size. Most players tolerate this, which is why it went unnoticed. The fix was straightforward (3 * 45 * 4 for the actual number of fixed-size fields).

  • Phase evolution biased toward −π/4 (c455c8d): Operator precedence caused the inter-second phase perturbation to drift systematically rather than randomly at low frequencies. Centering the random value to [-0.5, 0.5) before scaling fixed the asymmetry.

  • Off-by-one in grey noise A-weighting (a33dfb4): The closure receives pos[1..], so index 0 is frequency bin 1. The code used the raw index, shifting the entire A-weighting curve down by 1 Hz — significant near 20 Hz where the curve is steep.

  • One-sided imaginary-part assertion (24cd1dc): The conjugate symmetry check only caught positive imaginary values. Added .abs().

Phase 2: Volume normalization

The audit also flagged that each noise color used hand-tuned normalization constants producing wildly different volumes. This was addressed in two steps, both guided by the human noticing the perceptual result and asking the LLM to explain and fix:

  1. RMS normalization (4e58e1f): Replaced per-color magic constants with a single Parseval's-theorem-based energy calculation. Each closure now defines only the spectral shape; noise() scales all bins to a target RMS. This made all colors physically equal in volume.

  2. A-weighted normalization (0688a3b): After RMS normalization, brownian and grey still sounded quieter because their energy sits at low frequencies where human hearing is insensitive. Switching to IEC 61672-1 A-weighted energy for the normalization pass made all colors perceptually similar in loudness.

Phase 3: Literate programming

Finally, the LLM was asked to add comments explaining the why behind the DSP code — targeted at readers who may not have signal processing background. This added Wikipedia-linked explanations of Nyquist, Hermitian symmetry, spectral shaping, phase evolution, and A-weighting, without changing any code (b7e3dc1).

Where the LLM was particularly useful

  • Domain knowledge on tap: The LLM could evaluate A-weighting formulas against the IEC standard, reason about Parseval's theorem for unnormalized IFFTs with conjugate symmetry, and explain why phase continuity matters at low frequencies. This is the kind of knowledge that would otherwise require either deep expertise or significant research time.

  • Catching subtle correctness issues: The off-by-one in A-weighting and the phase drift bias are the kind of bugs that are hard to catch in review because the code looks reasonable and the output sounds mostly fine. Having a reviewer that can trace through the math found issues that listening tests alone wouldn't surface.

  • Controlled iteration: The human directed every step — deciding which audit findings were real, choosing to pursue perceptual normalization after hearing the RMS result, and steering the literate programming pass. The LLM executed within those decisions, not instead of them.

The net code change

src/main.rs: +125 / −44 lines across bug fixes, normalization rewrite, and documentation comments. The spectral shaping closures are simpler (no more per-color magic constants), and the code is substantially better documented for readers unfamiliar with audio DSP.

jonhoo and others added 12 commits April 4, 2026 15:57
Prompt was:

> assess the correctness/accuracy of the WAV format implementation and
> noise generation algorithms of this codebase. don't worry too much
> about code quality or hygiene. i'm only looking for algorithmic
> accuracy right now. consider using multiple sub-agents to analyze
> different algorithms, chunks of code, or facets of correctness in
> parallel.
The RIFF size field must equal the total file size minus the 8 bytes
for the `RIFF` tag and size field itself. The calculation used `3 * 4`
for the fixed-size fields following the RIFF header, but there are
actually 5 such u32 fields: `WAVE`, `fmt ` chunk id, fmt chunk size,
`data` chunk id, and data chunk size. The previous value of `3 * 4`
omitted the two `data`-chunk fields, producing a header that
understated the file size by 8 bytes.

Most WAV players tolerate this, which is likely why it went unnoticed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The inter-interval phase evolution multiplied a `[0, 1)` random value
by a frequency-scaled angle and then subtracted a constant `PI/4`. Due
to operator precedence, the subtraction applied after all the
multiplication, producing a phase offset in the range
`[-PI/4, hz/MAX_FREQ * PI/2 - PI/4]`. At low frequencies where the
random term is negligible, this collapsed to a near-constant `-PI/4`
rotation every interval — a systematic drift rather than a random one.

Centering the random value to `[-0.5, 0.5)` before scaling produces a
symmetric range `[-(hz/MAX_FREQ)*PI/4, +(hz/MAX_FREQ)*PI/4]`, which
drifts in both directions equally and still scales the perturbation
magnitude with frequency (low bins drift slowly, high bins drift more).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The `spectrum_setup` closure receives `pos[1..]`, so closure index 0
corresponds to frequency bin 1. The grey noise loop called
`r_a(hz as f64)` using the raw closure index, which is 1 less than
the actual frequency. This shifted the entire A-weighting curve down
by 1 Hz — significant near 20 Hz where the curve is steep, negligible
at higher frequencies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The assertion that verifies conjugate symmetry (imaginary parts near
zero after inverse FFT) only checked `sample.im < 1.`, missing large
negative values. Use `.abs()` to catch both directions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prompt was:

> analyze the volume normalization for the generated noise across colors
Each noise color previously used hand-tuned normalization constants
that produced wildly different output volumes — measured RMS ranged
from 0.58× (violet) to 4.82× (grey) relative to white noise. Grey
noise in particular peaked at 89% of i16 full-scale, risking clipping
with longer durations or unlucky random seeds.

The closures now define only the spectral *shape* (e.g., `1/sqrt(f)`
for pink, `f` for violet) with arbitrary reference amplitude. After
`spectrum_setup()` populates the bins, `noise()` computes the total
energy via Parseval's theorem and scales all bins to hit a target RMS:

    energy = 2 × Σ |A(k)|²     (factor of 2 from conjugate symmetry)
    scale  = target_rms / sqrt(energy)

The target RMS is chosen to match the historical white noise level
(~5% of i16 full-scale), so white noise sounds identical to before.

This normalization only runs on the first 1-second interval, since
subsequent intervals use phase-only evolution (magnitude 1 polar
multiplier) that preserves bin magnitudes.

Note: this equalizes *physical* RMS, not *perceived* loudness. Colors
with energy concentrated at frequency extremes (brownian, grey) will
still sound quieter because human hearing is less sensitive there.
A-weighted normalization would address this but is left for a follow-up.

Prompted in the same session as b49586e
simply as:

> document this analysis in loudness-analysis.md, then enter plan mode
> to plan the implementation of more robust volume normalization

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous commit (4e58e1f) normalized all colors to equal physical
RMS, but brownian and grey noise sounded noticeably quieter because
their energy is concentrated at low frequencies where human hearing
is least sensitive.

This switches from unweighted to A-weighted (IEC 61672-1) energy when
computing the normalization scale factor. Each bin's energy
contribution is weighted by `W_A(f)²`, where `W_A` is the A-weighting
at that bin's frequency. The spectrum is then scaled so this
A-weighted RMS matches the target. The spectral *shape* is unaffected
— only the overall gain changes per color.

As a drive-by, `r_a` is hoisted from a closure inside the `Grey` arm
to a top-level function, since it's now also used by the normalization
pass in `noise()`.

The trade-off is that colors with low-frequency emphasis (brownian,
grey) now have significantly higher physical RMS to compensate for
perceptual insensitivity, and may approach i16 clipping. Grey peaks
at ~92% of full scale at the current `avg_amplitude = 8`. This is
noted in a comment; reducing `avg_amplitude` is the escape hatch if
clipping is observed.

Prompted in the same session as 4e58e1f
with:

> brownian and grey both now _sound_ quite a lot quieter -- is that expected?

followed by

> enter plan mode to plan the perceptually equal loudness normalization

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The noise generator code assumes familiarity with signal processing
concepts that many readers won't have. Add targeted comments explaining
*why* the code is as it is, with Wikipedia links for further reading,
rather than restating what the code does.

Key areas documented:

- Module header: the frequency-domain synthesis approach (spectrum →
  IFFT → time-domain samples)
- Constants: why 22050 Hz (human hearing / CD quality) and why 2×
  that for sample rate (Nyquist)
- Spectral closures: what `from_polar(amplitude, phase)` encodes and
  why random phase produces noise rather than a chord
- Per-color annotations: the amplitude-vs-frequency relationship and
  how colors relate to each other (pink/blue, brownian/violet)
- Phase Brownian walk: why phases are perturbed rather than
  regenerated between seconds (avoids discontinuity clicks,
  especially at low frequencies)
- Hermitian symmetry: why negative-frequency conjugates are required
  for real-valued audio output
- Fade-in dampen: the startup click it prevents
- DC and Nyquist bins: why both are zeroed

No code changes — comments only.

Prompt sequence:

> apply the principles of literate programming to `src/main.rs`. change
> as little actual code as possible, and instead focus on comments with
> a high signal-to-noise ratio, especially tailored to readers who may
> be less familiar with audio signal processing. our goal is not to have
> a lot of comments, but rather to have comments that provide key
> insights about _why_ the code is as it is.
>
> for any links you put in the code make sure that they link to the
> right place (use a sub-agent using the sonnet model to check each
> one).
>
> The Nyquist constant comment is backwards -- it should be on the
> SAMPLE_RATE constant, which is set to 2x MAX_FREQUENCY because of
> Nyquist. Meanwhile, 22050 is set due to human hearing limits (and CD
> quality match)
>
> should the A-weighting comment for Grey explain what A-weighting is or
> link to something?
>
> let's make all links be wrapped in `<>` as they would be in Markdown
>
> use a sub-agent using the sonnet model to check that all the links in
> the file point to the expected page
>
> write out all the prompts i have given in this session, including
> those i've given in plan rejections, to tmp/literate-prompts.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant