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.
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.
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.
Key observations:
- Partial positions match within 1 Hz, confirming the frequency extraction accuracy.
- Relative amplitudes at the strike onset are well-matched.
- Decay shapes follow similar contours, though the synthesis lacks the inter-partial coupling visible in the reference.
- Noise floor differs: the synthesis has a clean background (additive sine tones), while the reference shows broadband environmental noise.
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.