Files
micarrsim/packages/acoustics-core/propagation.js
T
2026-08-28 16:05:22 +09:00

132 lines
5.0 KiB
JavaScript

import { dbToLinear, distance3 } from "./math.js";
import {
addFrequencyDomainDelay,
createDelayWorkspace,
nextPowerOfTwo,
prepareDelaySpectrum,
} from "./fractional-delay.js";
import { renderSignal } from "./signal.js";
export function propagationDelaySeconds(sourcePosition, receiverPosition, speedOfSound) {
return distance3(sourcePosition, receiverPosition) / speedOfSound;
}
export function sphericalAttenuation(distance, referenceDistance = 1, minimumDistance = 0.01) {
return referenceDistance / Math.max(distance, minimumDistance);
}
export function synthesizeMicrophoneSignals(options) {
const {
scene,
arrayDefinition,
assetBuffers = new Map(),
referenceDistance = 1,
minimumDistance = 0.01,
maximumBufferBytes = 512 * 1024 * 1024,
signalOffsets,
} = options;
const sampleCount = Math.ceil(scene.duration * scene.sampleRate);
const retainedBufferCount = arrayDefinition.microphones.length + scene.sources.length + 1;
let maximumDelaySamples = 0;
for (const source of scene.sources) {
for (const microphone of arrayDefinition.microphones) {
maximumDelaySamples = Math.max(
maximumDelaySamples,
(distance3(source.position, microphone.position) / scene.speedOfSound) * scene.sampleRate,
);
}
}
const delayFftSize = nextPowerOfTwo(sampleCount + Math.ceil(maximumDelaySamples) + 2);
const delayWorkingBytes = delayFftSize * Float64Array.BYTES_PER_ELEMENT * 4;
const estimatedBufferBytes =
retainedBufferCount * sampleCount * Float32Array.BYTES_PER_ELEMENT + delayWorkingBytes;
if (estimatedBufferBytes > maximumBufferBytes) {
throw new Error(
`예상 signal buffer ${(estimatedBufferBytes / 1024 ** 2).toFixed(1)} MiB가 ` +
`현재 한도 ${(maximumBufferBytes / 1024 ** 2).toFixed(1)} MiB를 넘습니다. duration 또는 sample rate를 줄이세요.`,
);
}
const channels = arrayDefinition.microphones.map(() => new Float32Array(sampleCount));
const sceneSampleOffset = signalOffsets?.sceneSampleOffset ?? 0;
const renderedSources = scene.sources.map((source, sourceIndex) => ({
definition: source,
samples: renderSignal(source, scene, assetBuffers, 0, {
sceneSampleOffset,
...(signalOffsets?.sources?.[sourceIndex] ?? {}),
}),
}));
renderedSources.forEach(({ definition, samples }) => {
const distances = arrayDefinition.microphones.map((microphone) => distance3(definition.position, microphone.position));
const maximumDelaySamples = (Math.max(...distances) / scene.speedOfSound) * scene.sampleRate;
const spectrum = prepareDelaySpectrum(samples, maximumDelaySamples);
const workspace = createDelayWorkspace(spectrum.fftSize);
arrayDefinition.microphones.forEach((microphone, microphoneIndex) => {
const distance = distances[microphoneIndex];
const delaySamples = (distance / scene.speedOfSound) * scene.sampleRate;
const sensitivity = dbToLinear(microphone.sensitivityDb ?? 0);
const amplitude = sphericalAttenuation(distance, referenceDistance, minimumDistance) * sensitivity;
addFrequencyDomainDelay({
output: channels[microphoneIndex],
spectrum,
delaySamples,
gain: amplitude,
phaseOffsetDeg: microphone.phaseOffsetDeg ?? 0,
workspace,
});
});
});
if (Array.isArray(scene.ambient)) {
scene.ambient.forEach((ambient, ambientIndex) => {
channels.forEach((channel, microphoneIndex) => {
const microphone = arrayDefinition.microphones[microphoneIndex];
const seedSalt = Math.imul(ambientIndex + 1, 0x45d9f3b) ^ Math.imul(microphoneIndex + 1, 0x27d4eb2d);
const ambientSamples = renderSignal(ambient, scene, assetBuffers, seedSalt, {
sceneSampleOffset,
...(signalOffsets?.ambient?.[ambientIndex] ?? {}),
});
const spectrum = prepareDelaySpectrum(ambientSamples, 0);
addFrequencyDomainDelay({
output: channel,
spectrum,
delaySamples: 0,
gain: dbToLinear(microphone.sensitivityDb ?? 0),
phaseOffsetDeg: microphone.phaseOffsetDeg ?? 0,
workspace: createDelayWorkspace(spectrum.fftSize),
});
});
});
}
let preNormalizationPeak = 0;
for (const channel of channels) {
for (const value of channel) {
preNormalizationPeak = Math.max(preNormalizationPeak, Math.abs(value));
}
}
const appliedScale =
scene.mix?.normalization === "peak" && preNormalizationPeak > 0
? scene.mix.targetPeak / preNormalizationPeak
: 1;
let peak = 0;
let clippedSampleCount = 0;
for (const channel of channels) {
for (let sampleIndex = 0; sampleIndex < channel.length; sampleIndex += 1) {
channel[sampleIndex] *= appliedScale;
const magnitude = Math.abs(channel[sampleIndex]);
peak = Math.max(peak, magnitude);
if (magnitude > 1) clippedSampleCount += 1;
}
}
return {
channels,
sampleRate: scene.sampleRate,
peak,
preNormalizationPeak,
appliedScale,
clippedSampleCount,
estimatedBufferBytes,
};
}