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?"
- \(C \approx 1\): output is well explained by input β clean measurement, linear system.
- \(C \approx 0\): output has little to do with input at that frequency β noise dominated, or the system simply doesn't respond there.
- \(0 < C < 1\): partial explanation β could be noise, nonlinearity, or multiple inputs contributing to the output.
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)
| Field | Bytes | Type | Description |
|---|---|---|---|
| TFRFHeader packed record | |||
| index | 4 | Integer | Channel or measurement index. β₯ 0. |
| Xr | 8 | Double | Reference (input) channel or physical X location. |
| Yr | 8 | Double | Response (output) channel or physical Y location. |
| XActual | 8 | Double | Physical X-axis value (excitation location). |
| YActual | 8 | Double | Physical Y-axis value (response location). |
| FRFType | 1 | TFRFType (U8) | 0 = ftTransfer (.trf), 1 = ftPoint (.pnt). |
| DataType | 1 | TDataType (U8) | 0 = Accelerance, 1 = Mobility, 2 = Receptance, 3 = Microphone. |
| HzRes | 8 | Double | Frequency resolution Ξf (Hz/bin). > 0. |
| StartFrequency | 8 | Double | Start frequency (Hz). β₯ 0. |
| EndFrequency | 8 | Double | End frequency (Hz). > StartFrequency. |
| TSingleArray (MtxVec array descriptor β all fields 4 bytes) | |||
| fComplex | 4 | Single | 0.0 = real magnitude, 1.0 = complex Re/Im, 2.0 = complex + coherence Ξ³Β² (Acquire format). |
| fLength | 4 | Single | Number of frequency bins N (stored as float). |
| a[2] | 4 | U32 | Reserved / alignment padding. |
| fConditionCheck | 4 | Single | Internal integrity value. |
| Precision | 4 | U32 | 0 = Single (32-bit), 1 = Double (64-bit). |
| MtxVecVersion | 4 | U32 | Version of MtxVec library that wrote the file. |
| SizeOf(TSample) | 4 | U32 | Byte size of each data sample. |
| Tag | 4 | U32 | User-defined tag or identifier. |
| MtxVecFileCode | 4 | U32 | Magic number identifying a MtxVec file. |
| a[9], a[10] | 4+4 | U32 | Reserved / alignment padding. |
| Payload | |||
| caption | 4 | String | Short label, null-padded. |
| FRF data (fComplex=0) | N Γ 8 | float64[] | N real magnitude values, little-endian. |
| FRF data (fComplex=1) | N Γ 16 | float64[] | N complex samples as interleaved Re/Im pairs. |
| FRF data (fComplex=2) | N Γ 24 | float64[] | N samples as interleaved Re/Im/Ξ³Β² triples (Acquire). |
| Optional metadata block (Acquire-written files only) | |||
| sentinel | 11 | bytes | b'\x00OBIE_META\n' β marks start of metadata. Old readers stop before this and ignore it. |
| key: value lines | var | UTF-8 | sample_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.
| Field | Bytes | Type | Description |
|---|---|---|---|
| Header | |||
| DataType | 1 | TDataType (U8) | Response quantity (same enum as TRF above). |
| HzRes | 8 | Double | Frequency resolution Ξf (Hz/bin). > 0. |
| StartFrequency | 8 | Double | Start frequency (Hz). β₯ 0. |
| EndFrequency | 8 | Double | End frequency (Hz). > StartFrequency. |
| ScaleFactor | 8 | Double | Scale factor applied to the averaged data. |
| nAverages | 4 | U32 | Number of averages used. Typically 1β1000. |
| AverageType | 1 | Enum (U8) | 0 = RMS, 1 = Mean, 2 = Complex, 3 = Geometric, 4 = None. |
| TSingleArray (MtxVec array descriptor β all fields 4 bytes) | |||
| fComplex | 4 | Single | 0.0 = real (AvR), 1.0 = complex (AvC). |
| fLength | 4 | Single | Number of data points N (stored as float). |
| a[2] | 4 | U32 | Reserved. |
| fConditionCheck | 4 | Single | Internal integrity value. |
| Precision | 4 | U32 | 0 = Single, 1 = Double. |
| MtxVecVersion | 4 | U32 | MtxVec library version. |
| SizeOf(TSample) | 4 | U32 | Byte size of each sample. |
| Tag | 4 | U32 | User-defined tag. |
| MtxVecFileCode | 4 | U32 | Magic number. |
| a[9], a[10] | 4+4 | U32 | Reserved. |
| caption | 4 | String | Short label, null-padded. |
| Data section | |||
| AvR data | N Γ 8 | float64[] | N real-valued doubles, little-endian. Present when fComplex = 0. |
| AvC data | N Γ 16 | float64[] | N complex samples as interleaved Re/Im double pairs. Present when fComplex = 1. |
| Notes (appended after data) | |||
| notes | var | String | Free-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.
6 Β· Modal Analysis β Mode Shape Reconstruction
Modal Analysis (Web/tools/modeshape) animates how a plate physically deforms
at a chosen frequency, using nothing more than each stencil node's measured complex FRF at
that frequency. Because every node's hammer-impact FRF is referenced to the same fixed
input, comparing their complex values at a single frequency reveals both relative
amplitude and relative phase across the surface β which is exactly what a mode shape is.
6.1 The Complex FRF as a Phasor
At frequency \(f\), node \(i\)'s measured FRF is a complex number \(H_i(f) = \mathrm{Re}_i + i\,\mathrm{Im}_i\). Treat this as a phasor: under sinusoidal excitation at that frequency, the steady-state physical displacement of node \(i\) over time is the real part of \(H_i e^{i\theta}\), where \(\theta=\omega t\) is the drive phase:
$$\mathrm{disp}_i(\theta) = \mathrm{Re}\bigl(H_i\,e^{i\theta}\bigr) = \mathrm{Re}_i \cos\theta - \mathrm{Im}_i \sin\theta$$Sweeping \(\theta\) from \(0\) to \(2\pi\) animates one full vibration cycle. Points where \(H_i \approx 0\) barely move (nodal lines); points with large \(|H_i|\) move the most (antinodes); points whose \(H_i\) have opposite sign move in opposite directions at the same instant, which is how the animation shows a plate's characteristic node lines.
6.2 Building a Continuous Surface
Stencil nodes sit on a rectangular row/column grid at known physical (x, y) positions. For the currently selected frequency, Modal Analysis:
- Linearly interpolates each node's own FRF between its two nearest measured frequency bins to get \(H_i(f)\) at the exact requested frequency.
- Arranges the per-node Re and Im values into two 2-D grids indexed by row and column.
- Fits a tensor-product natural cubic spline to each grid β first across columns per
row, then across rows per fine column β producing smooth, \(C^2\)-continuous
\(\mathrm{Re}(x,y)\) and \(\mathrm{Im}(x,y)\) surfaces. This is equivalent to
scipy.interpolate.RectBivariateSpline, reimplemented in JS so it can run synchronously inside the animation loop. - Normalises both surfaces by the peak magnitude \(\max\sqrt{\mathrm{Re}^2+\mathrm{Im}^2}\) over the fine grid, so the animated peak amplitude is always 1 regardless of instrument or frequency β mode shapes stay visually comparable across runs.
Each animation frame then evaluates the phasor formula per fine-grid point:
// modeshape.js β one animation frame disp[y][x] = amp * (Re_fine[y][x]*cos(ΞΈ) - Im_fine[y][x]*sin(ΞΈ))
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.
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)$$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.
User Guide
For instructions on using Explore, Acquire, and Convolve, see the ObieWebApp User Guide.