Skip to content

Modal Resonator

Tier: Color | ComponentType: 41 | Params: 7

Bank of 32 decaying sinusoidal modes with 6 preset modal signatures, modeling resonant physical bodies.

Overview

ModalResonator synthesizes the resonant behavior of physical objects by summing up to 32 independently decaying sinusoidal modes. Each mode represents a natural vibration frequency of the modeled body, with its own frequency ratio, amplitude, and decay rate defined by one of six material presets. This is the additive synthesis approach to physical modeling -- rather than simulating wave propagation (as in waveguides), it directly constructs the frequency-domain response of a vibrating object.

The six presets model distinct material categories: Marimba (harmonic ratios, fast upper-mode decay), Bell (inharmonic ratios from thick-walled cylinder modes), Gong (densely packed inharmonic modes with long sustain), Drum (membrane modes with quick decay), Glass (sparse modes with very long decay), and Plate (nearly harmonic with progressive detuning). The Inharmonicity parameter stretches or compresses the frequency ratios around the fundamental, morphing between harmonic and inharmonic timbres.

The component supports both continuous excitation from input signal and triggered excitation via the excite() method. In continuous mode, the input signal's amplitude feeds energy into each mode proportional to its preset amplitude and brightness weighting. In triggered mode (used by the bridge's exciteWithAmplitude:brightness: method), all active modes receive an immediate energy impulse. The damp() method reduces all mode energies, simulating palm muting or material damping.

File Locations

Path
Header Sources/FolioDSP/include/FolioDSP/Color/ModalResonator.h
Implementation Sources/FolioDSP/src/Color/ModalResonator.cpp
Tests Tests/FolioDSPTests/ModalResonatorTests.swift
Bridge Sources/FolioDSPBridge/src/FolioDSPBridge.mm (ModalResonatorBridge)

Parameters

Index Name Description Min Max Default Min Default Max Default Unit
0 Fundamental Base frequency for mode 0 20 8000 60 2000 440 Hz
1 Brightness High-mode amplitude rolloff (1.0 = no rolloff) 0.0 1.0 0.0 1.0 1.0
2 Inharmonicity Stretch/compress of frequency ratios relative to fundamental 0.0 2.0 0.0 1.0 1.0
3 Decay Scale Multiplier applied to all mode decay times 0.1 10.0 0.5 2.0 1.0
4 Preset Modal signature (0=Marimba, 1=Bell, 2=Gong, 3=Drum, 4=Glass, 5=Plate) 0 5 0 5 0
5 Active Modes Number of modes to synthesize (1--32) 1 32 1 32 12
6 Mix Dry/wet blend 0 100 0 100 50 %

Processing Algorithm

The process() function executes these steps for each input sample:

1. Mode Synthesis

For each active mode \(i\) (where \(0 \leq i < N_{\text{active}}\)), the output is the sum of decaying sinusoids:

\[f_i = f_0 \cdot (1 + (r_i - 1) \cdot I)\]

where \(f_0\) is the fundamental, \(r_i\) is the preset's frequency ratio for mode \(i\), and \(I\) is the inharmonicity parameter. When \(I = 1\), the preset's ratios are used directly. When \(I = 0\), all modes collapse to the fundamental. When \(I = 2\), the ratios are stretched to twice their deviation from unity.

2. Brightness Rolloff

Each mode's amplitude is scaled by a linear rolloff factor:

\[a_i^{\text{bright}} = a_i \cdot \max\left(1 - (1 - B) \cdot \frac{i}{N_{\text{active}}},\; 0\right)\]

where \(B\) is the brightness parameter and \(a_i\) is the preset amplitude. At \(B = 1.0\), all modes retain their preset amplitudes. At \(B = 0\), upper modes are progressively attenuated to zero.

3. Output Summation

\[y_{\text{modes}} = \frac{1}{N_{\text{active}}} \sum_{i=0}^{N_{\text{active}}-1} \sin(\varphi_i) \cdot E_i \cdot a_i^{\text{bright}}\]

The division by \(N_{\text{active}}\) prevents amplitude buildup when many modes are active.

4. Phase Advance and Energy Decay

Per mode per sample:

\[\varphi_i \mathrel{+}= \frac{2\pi \cdot f_i}{f_s}\]
\[E_i \mathrel{\times}= e^{-1 / (\tau_i \cdot D \cdot f_s)}\]

where \(\tau_i\) is the preset's base decay time for mode \(i\) and \(D\) is the decay scale parameter.

5. Continuous Excitation

When the input signal has non-trivial amplitude (\(|x| > 0.001\)), energy is continuously fed into each active mode:

\[E_i \mathrel{+}= |x| \cdot a_i \cdot b_i \cdot 0.01\]

where \(b_i\) is the brightness factor for mode \(i\). Energy is clamped to a maximum of 1.0 per mode.

6. DC Blocker

\[R = 1 - \frac{2\pi \cdot 10}{f_s}\]
\[y_{\text{dc}}[n] = y_{\text{modes}}[n] - y_{\text{modes}}[n-1] + R \cdot y_{\text{dc}}[n-1]\]

7. Wet/Dry Mix

\[y = x \cdot (1 - m) + y_{\text{dc}} \cdot m\]

where \(m = \text{mix} / 100\).

Core Equations

\[f_i = f_0 \cdot (1 + (r_i - 1) \cdot I)\]
\[y = \frac{1}{N} \sum_{i} \sin(\varphi_i) \cdot E_i \cdot a_i^{\text{bright}}\]
\[E_i[n+1] = E_i[n] \cdot e^{-1/(\tau_i \cdot D \cdot f_s)}\]

Snapshot Fields

Field Type Range Unit Description
Fundamental Float 20--8000 Hz Current fundamental frequency (smoothed)
Brightness Float 0--1 Current brightness value (smoothed)
Inharmonicity Float 0--2 Current inharmonicity value (smoothed)
Active Modes Uint8 1--32 Number of active modes
Preset Uint8 0--5 Current preset index
Output Level Float 0--1 Smoothed output amplitude
Total Energy Float 0--1 Sum of all mode energies
Mode Energies Float[32] 0--1 Per-mode energy levels
Mode Frequencies Float[32] 20--20000 Hz Per-mode computed frequencies
Ringing Bool 0--1 Whether total energy exceeds 0.001

Implementation Notes

  • Preset data is stored as static PresetData arrays containing 32 frequency ratios, amplitudes, and decay rates per preset.
  • ParamSmoother is used for fundamental, brightness, inharmonicity, and mix to prevent zipper noise on rapid parameter changes.
  • DC blocker uses the standard first-order highpass: y = x - xprev + R * y where R = 1 - (2pi*10/sr). This is essential because modal synthesis can produce DC offset depending on phase relationships.
  • Energy clamping at 1.0 per mode prevents unbounded energy accumulation from continuous excitation.
  • Low-energy skip: modes with energy below 0.0001 are skipped entirely, saving computation when modes have decayed to silence.
  • The excite() method resets all mode phases to 0 for a clean attack transient.
  • The damp() method multiplies all mode energies by (1 - amount), allowing graduated damping from gentle to complete.
  • All atomic parameters use std::memory_order_relaxed for lock-free thread safety.
  • Snapshot emission is decimated to ~60 fps (every 735 samples at 44.1 kHz).

Equation Summary

y = sum(sin(phi) * energy * decay), 32 modes