Skip to content

Replace the harmonium timbre with a measured spectrum - #995

Merged
frostburn merged 2 commits into
xenharmonic-devs:mainfrom
sohamm20:main
Sep 18, 2026
Merged

frostburn merged 2 commits into
xenharmonic-devs:mainfrom
sohamm20:main

Conversation

@sohamm20

Copy link
Copy Markdown
Contributor

The harmonium waveform was a hand-tuned approximation: a PWM square wave at duty cycle 0.43, shaped by odd-harmonic bias and brightness trim, then smeared across seven detuned copies at arbitrary offsets. It did not sound much like a harmonium.

This replaces it with a spectrum measured from a recording of a real instrument. Analysing one sustained C3 note showed it is not one rank but three sounding together:

  • an 8' bass reed -> ratio 1
  • a 4' octave reed 1202.48 cents above it -> ratio 2.002862, a near-
    octave 2.48 cents wide, so the two ranks beat at 0.375 Hz; this is
    where the shimmer comes from, and why a pure 2:1 sounds lifeless
  • a third quiet rank 153.18 cents flat -> ratio 0.915321, beating
    against the 8' at 11.1 Hz, heard as crispness rather than as pitch

Amplitudes came from short-window tracking; a long-window fit badly underestimates high partials because the instrument's own pitch wobble destroys their phase coherence. The 8' rank's odd partials run about 9 dB above its even ones, the signature of a free reed beating against its frame.

The result is 139 partials against roughly 480 before, so it is also lighter on the aperiodic voice budget.

Selecting the harmonium now also applies a matching envelope: ~120 ms attack (the measured reed speech time), no decay, full sustain, 100 ms release. A harmonium is blown rather than struck, and the app's default envelope decaying to 80% reads as a struck instrument and undoes the point of the measured spectrum.

The harmonium waveform was a hand-tuned approximation: a PWM square wave
at duty cycle 0.43, shaped by odd-harmonic bias and brightness trim, then
smeared across seven detuned copies at arbitrary offsets. It did not
sound much like a harmonium.

This replaces it with a spectrum measured from a recording of a real
instrument. Analysing one sustained C3 note showed it is not one rank but
three sounding together:

  - an 8' bass reed                          -> ratio 1
  - a 4' octave reed 1202.48 cents above it  -> ratio 2.002862, a near-
    octave 2.48 cents wide, so the two ranks beat at 0.375 Hz; this is
    where the shimmer comes from, and why a pure 2:1 sounds lifeless
  - a third quiet rank 153.18 cents flat     -> ratio 0.915321, beating
    against the 8' at 11.1 Hz, heard as crispness rather than as pitch

Amplitudes came from short-window tracking; a long-window fit badly
underestimates high partials because the instrument's own pitch wobble
destroys their phase coherence. The 8' rank's odd partials run about 9 dB
above its even ones, the signature of a free reed beating against its
frame.

The result is 139 partials against roughly 480 before, so it is also
lighter on the aperiodic voice budget.

Selecting the harmonium now also applies a matching envelope: ~120 ms
attack (the measured reed speech time), no decay, full sustain, 100 ms
release. A harmonium is blown rather than struck, and the app's default
envelope decaying to 80% reads as a struck instrument and undoes the
point of the measured spectrum.
@sohamm20

Copy link
Copy Markdown
Contributor Author

@forrcaho please review

Comment thread src/stores/audio.ts Outdated
if (APERIODIC_WAVEFORMS.includes(newValue)) {
aperiodicVoiceParams.aperiodicWave = APERIODIC_WAVES[newValue].value
}
if (newValue === 'harmonium') {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't do this. The user can select an envelope preset. You can add a new one if "Organ" and "Pad" don't serve you.

@frostburn

Copy link
Copy Markdown
Member

Sounds good but it's too loud compared to a plain semisine.

The measured amplitudes are normalised to a peak of 1, but as partials of
three ranks they sum to far more than that, so the timbre played about
9.5 dB louder than a plain semisine and peaked well past unity. Scale them
by HARMONIUM_GAIN = 0.22, which sits about 3.7 dB under semisine in RMS
and keeps the peak below it; a spectrum this dense reads as louder than a
sine at matched RMS.

Also drop the harmonium-specific envelope so selecting the waveform no
longer overrides the user's envelope settings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018kW3MC5W8F1EqdUWHvyvJb
@sohamm20

Copy link
Copy Markdown
Contributor Author

@frostburn fixed comments

@frostburn frostburn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll deploy to QA for community feedback tomorrow.

@frostburn

Copy link
Copy Markdown
Member

@frostburn
frostburn merged commit b38f9c3 into xenharmonic-devs:main Sep 18, 2026
2 checks passed
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.

2 participants