Not loaded
75%

Synthesizing a Korean Temple Bell

Additive synthesis with curved envelopes for inharmonic metal percussion

The challenge

Korean temple bells (Beompjong) are large bronze bells struck with a wooden log. They produce a dense cluster of inharmonic partials that beat against one another as they decay over 30–60 seconds. Unlike Western church bells whose partials approximate a harmonic series, the Beompjong spectrum is defined by the thick-walled bronze casting and the nippled surface texture, which scatter energy across non-integer frequency ratios.

The goal: recreate this timbre in Beepscript using additive synthesis from first principles. Each measured partial becomes an independent sine oscillator with its own amplitude envelope. No samples, no binary dependencies — just text.

Spectral analysis

Starting from a field recording, high-resolution FFT analysis (65,536-point Hann window, 0.73 Hz/bin resolution) identified 13 distinct partials:

Role Atom Frequency (Hz) Relative level Decay group
Sub-harmonic l 70.3 0.22 Very long
Hum (lower) b 184.9 0.035 Long
Hum (upper) a 191.6 1.00 Long
Mid-range (lower) d 286.4 0.50 Medium
Mid-range (upper) c 294.1 0.62 Medium
Secondary f 299.6 0.27 Medium
Strike tone e 374.6 0.79 Medium-long
Near-strike n 391.9 0.15 Short
Upper (primary) g 452.6 0.35 Short
Upper (secondary) h 455.2 0.15 Short
Ring (lower) i 501.3 0.04 Short
Ring (upper) j 514.2 0.034 Short
High overtone k 804.6 0.026 Very short

The frequencies are clearly inharmonic: the hum pair sits near 185/192 Hz, the mid-range pair at 286/294 Hz, and the strike tone at 375 Hz — none form integer ratios with each other.

Partial frequency map showing all 13 identified partials with relative amplitudes.

Beating pairs

Several partials form closely spaced pairs whose amplitude modulation (beating) gives the bell its characteristic shifting quality:

Pair Atoms Frequencies (Hz) Beat rate (Hz) Effect
Hum a, b 191.6, 184.9 6.7 Primary “wah-wah”
Mid-range c, d 294.1, 286.4 7.7 Mid-register modulation
Upper g, h 452.6, 455.2 2.6 Slow upper shimmer
Ring i, j 501.3, 514.2 12.9 Fast high-register flutter

These beat frequencies create a complex interference pattern that never exactly repeats.

Decay characterization

Each partial’s amplitude was tracked over time using a sliding-window FFT and fit to an exponential decay model. The decay hierarchy follows the expected pattern for bronze bells: low-frequency modes ring longest, while higher partials dissipate more quickly. The dominant hum (191.6 Hz) has a decay constant of ~60 seconds; the highest overtone (805 Hz) decays in ~32 seconds.

The envelope curve problem

Standard ADSR envelopes produce linear amplitude ramps. Bell synthesis demands exponential decay profiles — energy dissipation in vibrating metal follows a concave curve that falls rapidly at first and approaches silence asymptotically. Linear envelopes sounded synthetic and wrong.

Envelope curve shapes. curve=1.0 is linear; curve=2.5 approximates exponential decay; curve=0.5 produces convex shapes.

This motivated the design of a curve parameter for Beepscript’s envelope system. The parameter applies a power function to the linear phase progress:

shaped(p) = p^curve

where p is the normalized position (0 to 1) within the current envelope phase. For the decay phase (from peak to sustain level):

A(p) = 1.0 - (1.0 - sustain) * p^curve

A curve value of 2.5 produces the concave decay profile characteristic of struck metal. The maximum deviation from true exponential decay is about 8%, occurring in the mid-decay region — perceptually convincing and requiring only a single parameter.

Curve value interpretation

Curve Shape Use case
0.5 Convex Snappy envelopes
1.0 Linear (default) Standard synth sounds
2.0 Concave Gentle decay curves
2.5 Concave Struck metal decay
4.0 Strongly concave Extreme exponential approx.

The curve parameter applies to all ADSR phases (attack, decay, release), keeping the API minimal: one scalar controls the entire shape. The env() function accepts it as an optional sixth parameter, maintaining full backward compatibility.

Iterative refinement

The bell patch evolved through eight iterations, each informed by quantitative spectral comparison against the reference recording:

Iteration Mean spectral error
Initial 6-partial sketch >20 dB
Precise frequencies (65k-FFT) ~15 dB
Per-partial decay tuning ~8 dB
Final linear envelope tuning 3.2 dB
Curved envelopes (curve=2.5) 5.0 dB

Error was measured at eight timepoints after the strike onset (0.5 to 13 seconds) across all 13 partials. Only measurements where the reference amplitude exceeded -80 dB were included.

Spectrogram comparison

The reference recording spectrogram shows the sustained partials as horizontal bands, with the hum (~192 Hz) and strike tone (~375 Hz) most prominent. The synthesis closely matches the partial positions, relative amplitudes, and decay contours.

Reference recording spectrogram, 0–15 seconds from strike onset. Frequency range 30–2000 Hz.
Synthesis spectrogram. Same frequency range and time range for direct comparison.

Key observations:

Hum partial (191.6 Hz) decay comparison: reference (black), linear envelope (red), curved envelope (blue).

Polymorphic variation

The static instrument produces identical strikes every time. In a real Beompjong, no two strikes are alike — the wooden log contacts the bell at slightly different positions and angles, the bronze is at a different temperature, and residual vibration from the previous strike alters the initial conditions.

The polymorphic version wraps the atom definitions in a bell() function that re-randomizes parameters on each invocation:

Helper Parameter varied Range Physical analogy
vf() Frequency +/-0.3% Temperature/tension shifts
vg() Gain (amplitude) +/-12% Hammer strike strength
vt() Time (decay/release) +/-8% Ring duration variation
Attack noise level +/-15% Impact force variation

Each variation is individually sub-threshold (0.3% frequency shift is below the ~1% just-noticeable difference for pure tones), but their aggregate across 13 partials produces clearly audible inter-strike differences. The hum pair’s beat frequency shifts by up to +/-0.04 Hz per strike, subtly altering the perceived pulsation speed.

Beyond adding randomization, the polymorphic version makes targeted adjustments to the hum pair’s base envelope parameters:

Parameter Static value Polymorphic value Rationale
Hum decay (a) 18000 ms 15000 ms Tighter decay, relies on randomization for variety
Hum lo decay (b) 18000 ms 14000 ms Slight asymmetry from beat partner
Hum curve (a) 2.5 1.7 Gentler concavity, more gradual onset
Hum lo curve (b) 2.5 1.8 Slightly different from partner

The reduced curve values (1.7–1.8 vs. uniform 2.5) soften the initial decay slope for the hum pair, allowing the randomized decay times to have more perceptual impact in the critical first few seconds.

The notation re-invokes bell() between strikes via inline Lua blocks, causing each chord group to use freshly randomized atoms.

Complete polymorphic listing

The bell() function wraps all 13 partials plus the attack transient. Three variation helpers — vf(), vg(), vt() — apply controlled jitter using Lua’s math.random(). Each inline {bell()} block re-executes the function with fresh random seeds, so every strike in the sequence has unique frequencies, amplitudes, and decay times. The technique generalizes to any Beepscript instrument where per-note variation is desired.

The static instrument

The bell without polymorphic variation, in 34 lines of Beepscript:

13 sine partials with individually tuned frequencies, amplitudes, decay times, and sustain levels. One noise burst for the strike transient. All shaped by curve=2.5 for exponential-style decay. The result achieves a mean spectral error of 5.0 dB across 61 measurements spanning 13 partials and 8 timepoints.

What this demonstrates

Beepscript’s text-based synthesis can produce realistic acoustic instrument timbres through careful spectral analysis and parameter mapping. The curve parameter — developed during this project — extends the envelope system to cover exponential decay profiles characteristic of struck metal. The polymorphic design shows how Lua scripting transforms a static instrument into a living one, eliminating repetition artifacts without new DSP primitives.

The entire instrument is expressed in human-readable text with no binary dependencies. It can be rendered at any sample rate, parametrically adjusted, and generated by a language model using the LLM reference.