feat(audio): account for source directivity in diffuse reverb calculations
Previously, diffuse field reverberant gain and critical distance calculations assumed an omnidirectional sound source (Q = 1), regardless of cone settings. Directional sources concentrate sound energy in specific directions, reducing the total acoustic power injected into the room and lowering the reverberant field level relative to the direct sound on-axis. Incorporate source directivity factor Q into the acoustics model: - Compute closed-form directivity factor Q in src/physics/cone.ts by integrating squared cone gain over the unit sphere. - Scale reverberant field gain by 1 / sqrt(Q) (-10·log10(Q) dB) and adjust critical distance by sqrt(Q) in src/audio/reverb.ts. - Update AudioEngine and signal path UI to present directivity index (DI) and adjust room dominance evaluations. - Add unit tests verifying Q against brute-force numerical quadrature and confirming critical distance behavior. Test Plan: - `npm run typecheck` -- passed clean - `npm test` -- 125/125 tests passed (6 test suites) - `git diff --cached --check` -- no whitespace errors
This commit is contained in:
@@ -2,10 +2,12 @@ import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
SURFACES,
|
||||
computeAcoustics,
|
||||
criticalDistanceFor,
|
||||
generateImpulseResponse,
|
||||
getSurface,
|
||||
reverberantGain,
|
||||
} from '../src/audio/reverb';
|
||||
import { directivityFactor } from '../src/physics';
|
||||
import { MockAudioContext } from './mockAudioContext';
|
||||
|
||||
const room = { width: 20, height: 8, depth: 20 };
|
||||
@@ -58,6 +60,33 @@ describe('reverberant field level', () => {
|
||||
expect(reverberantGain(acoustics, 1)).toBeCloseTo(1 / acoustics.criticalDistance, 6);
|
||||
});
|
||||
|
||||
it('still balances at the critical distance once the source is directional', () => {
|
||||
const acoustics = computeAcoustics(room, 0.22);
|
||||
const q = directivityFactor({ innerAngle: 70, outerAngle: 160, outerGain: 0.08 });
|
||||
const rc = criticalDistanceFor(acoustics, q);
|
||||
// The invariant is unchanged, it just moves outward: on axis the dry path is
|
||||
// 1/r and the wet path is flat, so they must still cross at exactly rc.
|
||||
expect(reverberantGain(acoustics, 1, q)).toBeCloseTo(1 / rc, 6);
|
||||
expect(rc).toBeCloseTo(acoustics.criticalDistance * Math.sqrt(q), 6);
|
||||
});
|
||||
|
||||
it('drops the wet level by the directivity index of the cone', () => {
|
||||
const acoustics = computeAcoustics(room, 0.22);
|
||||
const q = directivityFactor({ innerAngle: 70, outerAngle: 160, outerGain: 0.08 });
|
||||
const omni = reverberantGain(acoustics, 1, 1);
|
||||
const directional = reverberantGain(acoustics, 1, q);
|
||||
// 10·log10(Q) = 7.05 dB for the default cone.
|
||||
expect(20 * Math.log10(directional / omni)).toBeCloseTo(-10 * Math.log10(q), 6);
|
||||
expect(10 * Math.log10(q)).toBeCloseTo(7.05, 2);
|
||||
});
|
||||
|
||||
it('is unchanged for an omnidirectional source', () => {
|
||||
const acoustics = computeAcoustics(room, 0.22);
|
||||
const q = directivityFactor({ innerAngle: 360, outerAngle: 360, outerGain: 0.08 });
|
||||
expect(q).toBeCloseTo(1, 12);
|
||||
expect(reverberantGain(acoustics, 1, q)).toBeCloseTo(reverberantGain(acoustics, 1), 12);
|
||||
});
|
||||
|
||||
it('is louder in a more reflective room', () => {
|
||||
const dead = reverberantGain(computeAcoustics(room, 0.9));
|
||||
const live = reverberantGain(computeAcoustics(room, 0.035));
|
||||
|
||||
Reference in New Issue
Block a user