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, } = 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 renderedSources = scene.sources.map((source) => ({ definition: source, samples: renderSignal(source, scene, assetBuffers), })); 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); 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, }; }