const C = 343; // m/s, dry air at 20 degrees C
// Element positions, centred on the origin.
function positions(n, d) {
return Array.from({ length: n }, (_, i) => (i - (n - 1) / 2) * d);
}
// Normalised delay-and-sum response at arrival angle theta (radians from
// broadside), steered to theta_s. This IS the beamformer: one phasor per
// microphone, steering delay applied, summed, divided by N.
function response(n, d, thetaS, freq, theta) {
const k = (2 * Math.PI * freq) / C;
const u = Math.sin(theta) - Math.sin(thetaS);
let re = 0, im = 0;
for (const x of positions(n, d)) {
re += Math.cos(k * x * u);
im += Math.sin(k * x * u);
}
return Math.hypot(re, im) / n;
}
// Directivity against a spherically isotropic noise field.
// Gamma_mn = sinc(k * |x_m - x_n|); uniform weights make the numerator 1.
function directivityIndexDb(n, d, thetaS, freq) {
const k = (2 * Math.PI * freq) / C;
const x = positions(n, d);
let s = 0;
for (const xm of x) for (const xn of x) {
const delta = xm - xn;
const sinc = Math.abs(k * delta) < 1e-9 ? 1 : Math.sin(k * delta) / (k * delta);
s += sinc * Math.cos(k * delta * Math.sin(thetaS));
}
return 10 * Math.log10((n * n) / s);
}
// The only spacing rule that holds for every steer angle.
const aliasFreeCeilingHz = (d) => C / (2 * d);