🎻

ObieWebApp

Β· Technical Reference
Tips ← User Guide

Thoughts on the Analysis of the Violin

In-depth explanation of the signal processing, mathematics, and file formats used in ObieApp. Originally written by Chris Rogers; equations rendered from the source LaTeX.

Contents

1 Β· Frequency Response Functions

1.1 What is a FRF?

We want to express the violin mathematically as a transfer function (also called the frequency response function) β€” it converts some input (in this case a force from the hammer) to some output (in this case sound or a pressure fluctuation):

$$f(t) \;\to\; p(t)$$

Taking the Fourier Transform, we can find the transfer function for the violin as:

$$H(f) = \frac{P(f)}{F(f)}$$

where \(P(f)\) and \(F(f)\) are the Fourier transforms of pressure and force and have real and imaginary parts. The Fourier Transform is defined as:

$$G(f) = \int_{-\infty}^{\infty} g(t)\, e^{-i2\pi ft}\, dt$$

We can simplify β€” get the denominator to be real β€” by multiplying numerator and denominator by \(F^*\):

$$H(f) = \frac{P(f)}{F(f)} = \frac{PF^*}{FF^*} = H_1 \qquad \frac{PP^*}{FP^*} = H_2$$

1.2 Least Squares Fit β€” H1 and H2

In reality, we take 5–10 hits per position so that we can get a "best fit" reading. We know that:

$$P(f) = H(f)\, F(f)$$

So the error of the best fit for measured values \(F_i\) and \(P_i\) is:

$$\epsilon_i(f) = P_i - H(f)F_i$$

Realizing that \(H(f)\) is a complex number \((a+ib)\) and that \(F, P\) are complex as well:

$$\begin{aligned} \epsilon_i^2(f) &= (P_i - H(f)F_i)(P_i - H(f)F_i)^*\\ &= |P_i|^2 - H(f)F_iP^*_i - P_iH^*(f)F^*_i + |H(f)F_i|^2 \\ &= |P_i|^2 -(a+ib)F_iP^*_i - P_i(a-ib)F^*_i + |(a+ib)F_i|^2\\ &= |P_i|^2 -(a+ib)F_iP^*_i - P_i(a-ib)F^*_i + (a^2+b^2)F_iF_i^* \end{aligned}$$

Summing over all readings and finding the minimum by differentiating with respect to \(a\) and \(b\) and setting equal to zero:

$$\begin{aligned} \frac{\partial \sum \epsilon_i^2(f)}{\partial a} &= 0 = -\sum F_iP^*_i - \sum P_iF^*_i + 2a\sum F_iF_i^*\\ \frac{\partial \sum \epsilon_i^2(f)}{\partial b} &= 0 = -i\sum F_iP^*_i + i\sum P_iF^*_i +2b\sum F_iF_i^* \end{aligned}$$

Multiplying the second equation by \(i\) and then adding:

$$\begin{aligned} 0 &= -\sum F_iP^*_i - \sum P_iF^*_i + 2a\sum F_iF_i^*\\ 0 &= \sum F_iP^*_i -\sum P_iF^*_i +2bi\sum F_iF_i^* \end{aligned}$$

Adding them together:

$$\begin{aligned} 0 &= -2 \sum P_iF^*_i + 2a\sum F_iF_i^* +2bi\sum F_iF_i^*\\ \sum P_iF^*_i &= H(f)\sum F_iF_i^* \end{aligned}$$

This gives the H1 estimator (minimises output noise):

$$\boxed{H_1(f) = \frac{\sum P_iF^*_i}{\sum F_iF_i^*}}$$

Alternatively, we can minimise the effect of noise on the input by using as the error:

$$\epsilon_i(f) = F_i - \frac{P_i}{H(f)}$$

Expanding the squared error:

$$\begin{aligned} \epsilon_i^2(f) &= \left(F_i - \frac{P_i}{H(f)}\right)\!\left(F_i - \frac{P_i}{H(f)}\right)^*\\ &= F_iF_i^* - F_i\frac{P_i^*}{H^*} - F^*_i\frac{P_i}{H} + \frac{P_i^*}{H^*}\frac{P_i}{H}\\ &= F_iF_i^* - F_iP_i^*(a-ib)^{-1} - F^*_iP_i(a+ib)^{-1} + P_iP_i^*(a^2+b^2)^{-1} \end{aligned}$$

Differentiating and setting to zero:

$$\begin{aligned} \frac{\partial \sum \epsilon_i^2}{\partial a} &= 0 = (a-ib)^{-2}\sum F_iP^*_i +(a+ib)^{-2}\sum P_iF^*_i -(a^2+b^2)^{-2}2a\sum P_iP_i^*\\ \frac{\partial \sum \epsilon_i^2}{\partial b} &= 0 = -i(a-ib)^{-2}\sum F_iP^*_i +i(a+ib)^{-2}\sum P_iF^*_i -(a^2+b^2)^{-2}2b\sum P_iP_i^* \end{aligned}$$

Multiplying the second by \(i\) and adding:

$$\begin{aligned} 0 &= (a-ib)^{-2}\sum F_iP^*_i +(a+ib)^{-2}\sum P_iF^*_i -(a^2+b^2)^{-2}2a\sum P_iP_i^*\\ 0 &= (a-ib)^{-2}\sum F_iP^*_i -(a+ib)^{-2}\sum P_iF^*_i -(a^2+b^2)^{-2}2ib\sum P_iP_i^* \end{aligned}$$

Adding and rearranging:

$$\begin{aligned} H(a^2+b^2)^{-2}\sum P_iP_i^* &= (a-ib)^{-2}\sum F_iP^*_i \\ H\sum P_iP_i^* &= \frac{(a+ib)^2(a-ib)^2}{(a-ib)^2}\sum F_iP^*_i \\ \sum P_iP_i^* &= H\sum F_iP^*_i \end{aligned}$$

This gives the H2 estimator (minimises input noise):

$$\boxed{H_2(f) = \frac{\sum P_iP_i^*}{\sum F_iP^*_i}}$$

If the data is clean and the noise is truly random, H1 and H2 should be very close and their ratio β€” the coherence β€” should be 1. George used the geometric mean as the working estimate:

$$H \sim \sqrt{H_1 H_2}$$

1.3 Coherence

The coherence \(C\) measures how consistently the system behaves across repeated measurements:

$$C = \frac{H_1}{H_2} = \frac{\left|\sum P_i F_i^*\right|^2}{\sum F_i F_i^* \cdot \sum P_i P_i^*}$$

The numerator is the cross-spectrum squared β€” how much \(F\) and \(P\) move together. The denominator is the product of the individual power spectra β€” how much each signal varies on its own. It is the frequency-domain analogue of \(R^2\) from linear regression, and answers: "how much of the output can be explained by the input?"

Coherence is frequency-dependent. A violin might have \(C \approx 1\) near a resonance (where it responds strongly and predictably) but low coherence in a range where it barely responds and the output is dominated by noise. Always plot it across the full frequency range.

2 Β· Band Averages

We can look at the average distribution of energy by breaking the FRF into frequency bands and averaging the magnitudes over each band:

$$\begin{aligned} \bar{b} &= \frac{1}{N}\sum_{f=f_{\mathrm{lo}}}^{f_{\mathrm{hi}}} \sqrt{H(f)H^*(f)} \\[6pt] \bar{b}_{\mathrm{dB}} &= 20 \log_{10}\!\left(\bar{b}\right) \end{aligned}$$

We also calculate the spectral centroid of the band β€” the first moment β€” which tells you where the energy is concentrated within the band:

$$c = \frac{\displaystyle\sum_{f=f_{\mathrm{lo}}}^{f_{\mathrm{hi}}} f\,\sqrt{H(f)H^*(f)}} {\displaystyle\sum_{f=f_{\mathrm{lo}}}^{f_{\mathrm{hi}}} \sqrt{H(f)H^*(f)}}$$

In Python (Python/processing/bands.py):

f_lo  = float(band['start'])
f_hi  = float(band['end'])
mask     = (freq >= f_lo) & (freq <= f_hi)
H_band   = H_mag[mask]          # linear magnitude, NOT in dB
f_band   = freq[mask]
avg_db   = 20.0 * math.log10(max(float(np.mean(H_band)), 1e-12))
centroid = float(np.sum(f_band * H_band) / np.sum(H_band))

3 Β· Convolutions

We can estimate what an instrument will sound like by convolving its FRF with a recording β€” ideally captured with a force sensor at the bridge rather than a microphone. The convolution of two functions \(f(t)\) and \(g(t)\) is:

$$y(t) = \int_{-\infty}^\infty f(\tau)\, g(t-\tau)\, d\tau$$

What it does

A violin has resonances β€” certain frequencies ring longer and louder than others. The FRF is a fingerprint of exactly how the violin shapes sound. Convolution applies that fingerprint to any audio signal. Feed in a Tchaikovsky recording; get out what it would sound like coloured by that violin's resonance structure.

FFT acceleration

Computing the integral directly is slow. The convolution theorem says we can instead multiply the two signals in the frequency domain:

$$\begin{aligned} y(t) &= \mathrm{iFFT}\!\bigl(\mathrm{FFT}(g(t))\times \mathrm{FFT}(f(t))\bigr)\\[4pt] &= \int_{-\infty}^\infty \left[\int_{-\infty}^\infty g(\tau)e^{-i2\pi f\tau}\,d\tau\right] \left[\int_{-\infty}^\infty f(\tau)e^{-i2\pi f\tau}\,d\tau\right] e^{i2\pi ft}\,df \end{aligned}$$

In Python (Python/processing/convolution.py):

y = np.fft.irfft(
    np.fft.rfft(wav, n_fft) * np.fft.rfft(ir.astype(np.float64), n_fft),
    n_fft,
)[:N].real

The output is the input signal with the violin's character stamped onto it β€” its resonant peaks amplified, its dead spots suppressed, its natural decay times imposed on every note.

IR length

The impulse response (IR) length determines frequency resolution. ObieApp uses an adaptive length: the next power of 2 above \(\text{sample\_rate} / \Delta f\), capped at 65536:

def _adaptive_ir_length(freqs, sample_rate):
    df = freqs[1] - freqs[0]
    natural = int(sample_rate / df)
    next_pow2 = 1 << (natural - 1).bit_length()
    return min(next_pow2, 65536)

Minimum-phase reconstruction

For real-only FRF files (AvR format, no phase information), the phase can be estimated by treating the system as minimum-phase β€” for such systems the log-magnitude and phase are a Hilbert transform pair, so knowing one determines the other. In practice ObieApp stores all FRFs as complex (AvC or TRF), so minimum-phase reconstruction is not required.


4 Β· File Formats

All multi-byte fields are little-endian. Fields prefixed f in the MtxVec TSingleArray descriptor are 32-bit single-precision floats; all others are as specified. Sources: george.pas (Dew Research / DSP Master) and the LabVIEW writer code.

4.1 TRF / TRV β€” Transfer Function Format

Binary format used by Rational Acoustics / MtxVec. The header is 110 bytes, followed immediately by the data payload.

TFRFHeader (110 bytes, packed β€” no padding)

FieldBytesTypeDescription
TFRFHeader packed record
index4IntegerChannel or measurement index. β‰₯ 0.
Xr8DoubleReference (input) channel or physical X location.
Yr8DoubleResponse (output) channel or physical Y location.
XActual8DoublePhysical X-axis value (excitation location).
YActual8DoublePhysical Y-axis value (response location).
FRFType1TFRFType (U8)0 = ftTransfer (.trf), 1 = ftPoint (.pnt).
DataType1TDataType (U8)0 = Accelerance, 1 = Mobility, 2 = Receptance, 3 = Microphone.
HzRes8DoubleFrequency resolution Ξ”f (Hz/bin). > 0.
StartFrequency8DoubleStart frequency (Hz). β‰₯ 0.
EndFrequency8DoubleEnd frequency (Hz). > StartFrequency.
TSingleArray (MtxVec array descriptor β€” all fields 4 bytes)
fComplex4Single0.0 = real magnitude, 1.0 = complex Re/Im, 2.0 = complex + coherence Ξ³Β² (Acquire format).
fLength4SingleNumber of frequency bins N (stored as float).
a[2]4U32Reserved / alignment padding.
fConditionCheck4SingleInternal integrity value.
Precision4U320 = Single (32-bit), 1 = Double (64-bit).
MtxVecVersion4U32Version of MtxVec library that wrote the file.
SizeOf(TSample)4U32Byte size of each data sample.
Tag4U32User-defined tag or identifier.
MtxVecFileCode4U32Magic number identifying a MtxVec file.
a[9], a[10]4+4U32Reserved / alignment padding.
Payload
caption4StringShort label, null-padded.
FRF data (fComplex=0)N Γ— 8float64[]N real magnitude values, little-endian.
FRF data (fComplex=1)N Γ— 16float64[]N complex samples as interleaved Re/Im pairs.
FRF data (fComplex=2)N Γ— 24float64[]N samples as interleaved Re/Im/Ξ³Β² triples (Acquire).
Optional metadata block (Acquire-written files only)
sentinel11bytesb'\x00OBIE_META\n' β€” marks start of metadata. Old readers stop before this and ignore it.
key: value linesvarUTF-8sample_rate, bit_depth, n_hits, threshold, ham_cutoff, mic_cutoff, device.

4.2 AvR / AvC β€” Averaged Spectrum Format

AvR files store a real-valued averaged spectrum; AvC files store a complex-valued averaged spectrum. The file layout is identical except for the data section, which is \(N \times 8\) bytes for AvR and \(N \times 16\) bytes for AvC. \(N\) is given by fLength in the TSingleArray descriptor.

$$\text{AvR: } N \times 8\text{ bytes} \qquad \text{AvC: } N \times 16\text{ bytes} \qquad N = \texttt{int(fLength)}$$
FieldBytesTypeDescription
Header
DataType1TDataType (U8)Response quantity (same enum as TRF above).
HzRes8DoubleFrequency resolution Ξ”f (Hz/bin). > 0.
StartFrequency8DoubleStart frequency (Hz). β‰₯ 0.
EndFrequency8DoubleEnd frequency (Hz). > StartFrequency.
ScaleFactor8DoubleScale factor applied to the averaged data.
nAverages4U32Number of averages used. Typically 1–1000.
AverageType1Enum (U8)0 = RMS, 1 = Mean, 2 = Complex, 3 = Geometric, 4 = None.
TSingleArray (MtxVec array descriptor β€” all fields 4 bytes)
fComplex4Single0.0 = real (AvR), 1.0 = complex (AvC).
fLength4SingleNumber of data points N (stored as float).
a[2]4U32Reserved.
fConditionCheck4SingleInternal integrity value.
Precision4U320 = Single, 1 = Double.
MtxVecVersion4U32MtxVec library version.
SizeOf(TSample)4U32Byte size of each sample.
Tag4U32User-defined tag.
MtxVecFileCode4U32Magic number.
a[9], a[10]4+4U32Reserved.
caption4StringShort label, null-padded.
Data section
AvR dataN Γ— 8float64[]N real-valued doubles, little-endian. Present when fComplex = 0.
AvC dataN Γ— 16float64[]N complex samples as interleaved Re/Im double pairs. Present when fComplex = 1.
Notes (appended after data)
notesvarStringFree-text annotation starting immediately after the data section.

5 Β· Acoustic Wave Equation

This section derives the linearised acoustic wave equation from first principles and applies it to the mechanics of violin plates and the sound hole.

5.1 Wave Equation Derivation

Assume that the pressure is \(p\), the speed of a fluid particle is \(u\) with a mean of zero, and the density everywhere is \(\rho_0 + \rho\). The 1D continuity equation gives:

$$\frac{\partial(\rho_0 + \rho)}{\partial t} + \frac{\partial(\rho_0 + \rho)u}{\partial x} = 0$$

Expanding and dropping the nonlinear term \(\partial(\rho u)/\partial x\):

$$\frac{\partial \rho}{\partial t} + \rho_0\frac{\partial u}{\partial x} = 0$$

Differentiating again with respect to \(t\):

$$\frac{\partial^2 \rho}{\partial t^2} + \rho_0\frac{\partial^2 u}{\partial x\,\partial t} = 0$$

The momentum equation (1D, no gravity or friction) linearised for small fluctuations:

$$\rho_0 \frac{\partial u}{\partial t} + \rho_0 u\frac{\partial u}{\partial x} + \frac{\partial p}{\partial x} = 0$$

Differentiating with respect to \(x\) and dropping the middle (nonlinear) term:

$$\rho_0 \frac{\partial^2 u}{\partial t\,\partial x} + \frac{\partial^2 p}{\partial x^2} = 0$$

Combining the continuity and momentum equations:

$$\frac{\partial^2 p}{\partial x^2} = \frac{\partial^2 \rho}{\partial t^2}$$

Adiabatic closure

For an adiabatic process \(p = K\rho^\gamma\), so:

$$\frac{\partial p}{\partial x} = K\gamma\rho^{\gamma-1}\frac{\partial \rho}{\partial x} = c^2\frac{\partial \rho}{\partial x}$$

Linearising the second time derivative:

$$\frac{\partial^2 p}{\partial t^2} = \gamma K\rho^{\gamma-1}\frac{\partial^2 \rho}{\partial t^2} = \gamma RT\frac{\partial^2 \rho}{\partial t^2}$$

Substituting into the combined equation gives the acoustic wave equation:

$$\boxed{\frac{\partial^2 p}{\partial t^2} = \gamma RT\,\frac{\partial^2 p}{\partial x^2}}$$

where the speed of sound \(c = \sqrt{\gamma RT}\).

5.2 Acoustic Impedance

Specific acoustic impedance \(z\) is the ratio of pressure to acoustic particle velocity: \(p = zu\), analogous to voltage driving a current.

For a plate with mass \(m\), stiffness \(k\), and damping \(\beta\), area \(A\), the equation of motion is:

$$\sum F = ma = pA - kx - \beta u$$

Solving for impedance:

$$z = \frac{ma + kx + \beta u}{Au} = \frac{m}{A}\frac{\ddot{x}}{\dot{x}} + \frac{\beta}{A} + \frac{k}{A}\frac{x}{\dot{x}}$$

Assuming a harmonic solution \(u = \dot{x} = Ce^{i\omega t}\):

$$z = r + i\omega M - \frac{i}{\omega C}$$

where the lumped parameters are:

$$r = \frac{\beta}{A} \qquad M = \frac{m}{A} \qquad C = \frac{A}{k}$$

This is the direct mechanical analogue of an electrical RLC circuit: resistance \(r\), inductance (mass) \(M\), and compliance (capacitance) \(C\).

For the sound hole, modelled with frictional resistance \(R_h\), and the top plate with restoring force:

$$m_{\text{hole}}\,\ddot{x}_h = A_h\,\Delta p - R_h\,\dot{x}_h$$ $$m_{\text{plate}}\,\ddot{x}_p = F - k_p x_p - R_p\,\dot{x}_p + A_p\,\Delta p$$

5.3 Sound Intensity

Sound intensity is defined as the sound power per unit surface area. For a point source of power \(P\):

$$I = \frac{P}{4\pi r^2}$$

Sound Power Level and Sound Intensity Level are referenced to \(P_0 = 10^{-12}\,\mathrm{W}\) and \(I_0 = 10^{-12}\,\mathrm{W/m^2}\) respectively:

$$\mathrm{SWL} = 10\log_{10}\!\left(\frac{P}{P_0}\right) \qquad \mathrm{SIL} = 10\log_{10}\!\left(\frac{I}{I_0}\right)$$

Since \(I = pu = p^2/z\), the Sound Pressure Level is:

$$\mathrm{SPL} = 10\log_{10}\!\left(\frac{p^2}{p_0^2}\right) = 20\log_{10}\!\left(\frac{p}{p_0}\right)$$

referenced to \(p_0 = 20\,\mu\mathrm{Pa}\).

5.4 Quality Factor

The quality factor \(Q\) characterises the sharpness of a resonance peak. For the damped oscillator \(m\ddot{x} + R\dot{x} + kx = 0\):

$$Q = \frac{f_{\text{center}}}{\Delta f} = \frac{\sqrt{mk}}{R} = \frac{\omega_0}{2r}$$

where \(\Delta f\) is the half-power bandwidth (the βˆ’3 dB width of the peak), \(r\) is the exponential decay rate, and \(R\) is the damping coefficient. Higher \(Q\) means a sharper, more persistent resonance.



7 Β· Circle Fitting β€” SDOF Nyquist Circle Method

Circle Fit (Web/tools/circlefit, backed by Python/processing/circlefit.py) extracts a natural frequency, damping, and modal constant for each resonance directly from a measured accelerance FRF, using the classical Kennedy–Pancu circle-fitting method (Ewins, Modal Testing: Theory and Practice).

7.1 Receptance vs. Accelerance

For a single-degree-of-freedom (SDOF) resonance with hysteretic (structural) damping, it is receptance β€” displacement per unit force β€” that traces an exact circle in the Nyquist (Re vs Im) plane:

$$X(\omega) = \frac{A_r}{\omega_r^2 - \omega^2 + i\,\eta_r\,\omega_r^2}$$

where \(A_r\) is the complex modal constant (residue), \(\omega_r\) the natural frequency (rad/s), and \(\eta_r\) the hysteretic loss factor. Acquire's hammer-impact measurements are accelerance, \(A(\omega) = -\omega^2 X(\omega)\), so Circle Fit first converts:

$$X(\omega) = \frac{A(\omega)}{-\omega^2}$$

7.2 Algebraic Circle Fit

Within a user-picked candidate band, the receptance points \((x_i,y_i)=(\mathrm{Re}_i,\mathrm{Im}_i)\) are fit to a circle \(x^2+y^2+Dx+Ey+F=0\) by ordinary least squares (KΓ₯sa method) directly on the design matrix \([x,\,y,\,1]\):

$$\begin{bmatrix}x_i & y_i & 1\end{bmatrix} \begin{bmatrix}D\\E\\F\end{bmatrix} = -(x_i^2+y_i^2)$$

giving the centre and radius:

$$x_0=-\frac{D}{2}, \qquad y_0=-\frac{E}{2}, \qquad r=\sqrt{x_0^2+y_0^2-F}$$

Fit quality is reported as the RMS radial residual as a fraction of the radius (resid_rms_frac) β€” points that don't actually lie on a circle (no real resonance in the band, or a badly chosen band) show up as a large value here.

7.3 Natural Frequency β€” Fastest Angular Sweep

Physically, the phasor sweeps around the Nyquist circle fastest exactly at resonance. The angle of each point relative to the fitted centre, unwrapped to avoid a branch-cut jump, is:

$$\theta(\omega) = \operatorname{unwrap}\bigl(\operatorname{atan2}(y-y_0,\,x-x_0)\bigr)$$

The natural frequency \(\omega_r\) is taken as the point of maximum \(\left|\dfrac{d\theta}{d\omega}\right|\) (central difference across interior points), with a 3-point parabolic interpolation around the discrete peak for sub-bin precision.

7.4 Damping β€” Half-Angle Relation

For every pair of points \(a\) (below resonance) and \(b\) (above resonance), the half-angles \(\theta_a,\theta_b\) subtended at the circle centre between each point and the resonance point give an independent damping estimate:

$$\eta_{ab} = \frac{\omega_b^2-\omega_a^2} {\omega_r^2\bigl(\tan(\theta_a/2)+\tan(\theta_b/2)\bigr)}$$

Pairs where either half-angle is below a 5Β° minimum are discarded (\(\tan(\theta/2)\to0\) is numerically unstable right at resonance), and the final \(\eta_r\) is the median across all surviving pairs β€” robust to the occasional noisy outlier pair in real hammer data.

7.5 Modal Constant and Sign

The residue magnitude follows directly from the circle diameter:

$$|A_r| = 2r\,\eta_r\,\omega_r^2$$

Its sign is read off the circle centre's vertical position rather than assumed: because the receptance denominator \(\omega_r^2-\omega^2+i\eta_r\omega_r^2\) traces a horizontal line at constant positive imaginary part as \(\omega\) varies, and inverting a line not through the origin (\(z\to1/z\)) always produces a circle on the opposite side of the real axis β€” a positive real \(A_r\) always puts the fitted circle's centre at \(y_0<0\), and a negative \(A_r\) at \(y_0>0\).

7.6 Residual Compensation and Multi-Pass Fitting

A real structure has more than one mode, and modes outside the current band still contribute a slowly-varying, near-real background to the data inside it. Each already-identified mode \(s\)'s approximate contribution at \(\omega\) is:

$$\text{residual}(\omega) = \sum_{s} \frac{\mathrm{Re}(A_s)}{\omega_s^2-\omega^2}$$

which is subtracted from the real part of the band's data before each circle fit. Because every mode's residual depends on every other mode's fit, Circle Fit iterates β€” up to 5 passes β€” refitting every band using the latest fits of all the others, stopping early once the largest relative frequency change across modes falls below a tolerance (default \(10^{-4}\)).

7.7 Per-Node Residue Extraction

Once a mode's frequency \(\omega_r\) and damping \(\eta_r\) are fixed from the reference (averaged) FRF, its contribution to any individual node's receptance is linear in that node's own unknown complex residue \(A_r\). This lets every node's residue β€” and hence a damping-corrected, per-mode mode shape β€” be solved directly from the whole spectrum in one complex least-squares system, rather than band by band:

$$H(\omega) \approx \sum_{s} \frac{A_s}{\omega_s^2-\omega^2+i\eta_s\omega_s^2} + C + \frac{M}{\omega^2}$$

where \(C\) and \(M/\omega^2\) absorb the near-constant and low-frequency-mass-like contributions of modes outside the fitted set.

Not yet wired into the UI fit_node_residues() implements this solve and is exposed to the browser as pyCFFitNodeResidues, but Circle Fit's UI doesn't call it yet β€” Modal Analysis currently animates mode shapes from each node's raw interpolated FRF (Β§6) rather than a damping-corrected residue. It's available for a future feature.

8 Β· Live Averaged Spectrum (Spectrum Monitor)

Spectrum Monitor's averaged spectrum is a simpler, live-only cousin of the H1 estimator in Β§1 β€” there's no reference "input" channel to divide out, just one microphone channel's own magnitude spectrum, smoothed over the last few snippets so a steady tone or resonance holds still instead of jittering frame to frame.

8.1 Windowed FFT per Snippet

Every snippet (default 0.3 s, user-adjustable) is Hann-windowed and transformed independently:

$$X[k] = \mathrm{FFT}\bigl(x[n]\cdot w[n]\bigr), \qquad P[k] = \left(\frac{|X[k]|}{\sum_n w[n] / 2}\right)^{\!2}$$

Dividing by half the window's sum before squaring keeps the resulting power roughly amplitude-correct regardless of window shape or FFT length β€” the same normalisation convention used for a single-sided amplitude spectrum. This is implemented directly in Web/tools/spectrum/main.py (_process_snippet) using numpy.fft.rfft β€” a plain FFT, not a reuse of any canonical Python/processing/ module, since no existing module computes a single channel's own magnitude spectrum this way.

8.2 Power Averaging, Not dB Averaging

The displayed spectrum block-averages the last \(N\) snippets' power, then converts to dB once at the end:

$$\bar P[k] = \frac{1}{N}\sum_{i=1}^{N} P_i[k], \qquad \mathrm{dB}[k] = 10\log_{10}\bigl(\bar P[k]\bigr)$$
Why not average the dB values directly? Averaging already-logarithmic values understates the true average power whenever the signal fluctuates (by Jensen's inequality, the log of a mean is always \(\geq\) the mean of the logs) β€” a peak that only appears in some snippets would read quieter than it really is. Averaging in linear power first, exactly as an FRF's auto-spectrum \(S_{ff}\) in Β§1 is averaged across hits before any dB conversion, avoids that bias.

Changing the channel resets this running average immediately (mixing two channels' power together mid-average would be meaningless); changing \(N\) or the snippet length does not force a reset β€” the averaging window just grows, shrinks, or (if the FFT size itself changes) silently restarts once the accumulated spectra no longer match the new bin count.

The Spectrogram tab is unrelated math β€” it reuses Python/processing/spectrogram.py's compute_spectrogram STFT unchanged, the same function Convolve uses for its input/output spectrogram panes.