132 lines
5.0 KiB
JavaScript
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,
|
|
};
|
|
}
|