Files
micarrsim/packages/acoustics-core/signal.js
T

105 lines
4.6 KiB
JavaScript

import { ceilSampleIndex, dbToLinear, TAU } from "./math.js";
import { hashString, pinkNoiseAt, whiteNoiseAt } from "./noise.js";
function sampleAsset(signal, assetBuffers, time) {
const asset = assetBuffers.get(signal.assetId);
if (!asset) throw new Error(`오디오 asset을 찾을 수 없습니다: ${signal.assetId}`);
const channel = asset.channels[signal.channel ?? 0];
if (!channel) throw new Error(`${signal.assetId}의 channel ${signal.channel ?? 0}을 찾을 수 없습니다.`);
let sourceIndex = time * asset.sampleRate;
if (signal.loop && channel.length > 0) sourceIndex %= channel.length;
if (sourceIndex < 0 || sourceIndex >= channel.length) return 0;
const left = Math.floor(sourceIndex);
const fraction = sourceIndex - left;
const rightValue = channel[Math.min(left + 1, channel.length - 1)];
return channel[left] + (rightValue - channel[left]) * fraction;
}
/**
* Return the interval in which an entry can emit non-zero samples according to
* its declared duration and, for a non-looping asset, the decoded channel
* duration. Sample contents themselves are intentionally not inspected.
*/
export function effectiveSignalDurationSeconds(
entry,
scene,
assetBuffers = new Map(),
) {
const declaredDuration = entry.duration ?? scene.duration;
const signal = entry.signal;
if (signal?.type !== "asset" || signal.loop) return declaredDuration;
const asset = assetBuffers.get(signal.assetId);
const channel = asset?.channels?.[signal.channel ?? 0];
if (
!channel ||
!(asset.sampleRate > 0) ||
!Number.isFinite(asset.sampleRate)
) {
return declaredDuration;
}
return Math.min(declaredDuration, channel.length / asset.sampleRate);
}
/**
* Render one signal entry into the scene-sized output buffer.
*
* `timeMapping` lets a short, rebased scene window retain the original signal
* timeline. `sceneSampleOffset` maps output sample zero to an absolute sample
* in the original scene, while `signalStartTime`/`signalStartSample` preserve
* the entry's original tone/asset time origin and deterministic noise index.
* Omitting it keeps the historical whole-scene behaviour exactly unchanged.
*/
export function renderSignal(entry, scene, assetBuffers = new Map(), seedSalt = 0, timeMapping = {}) {
const sampleCount = ceilSampleIndex(scene.duration, scene.sampleRate);
const output = new Float32Array(sampleCount);
const startTime = entry.startTime ?? 0;
const duration = entry.duration ?? scene.duration;
const gain = dbToLinear(entry.gainDb ?? 0);
const startSample = Math.max(0, ceilSampleIndex(startTime, scene.sampleRate));
const endSample = Math.min(
sampleCount,
ceilSampleIndex(startTime + duration, scene.sampleRate),
);
const sceneSampleOffset = timeMapping.sceneSampleOffset ?? 0;
const signalStartTime = timeMapping.signalStartTime ?? startTime;
const signalStartSample =
timeMapping.signalStartSample ?? ceilSampleIndex(startTime, scene.sampleRate);
if (!Number.isSafeInteger(sceneSampleOffset) || sceneSampleOffset < 0) {
throw new RangeError("sceneSampleOffset must be a non-negative safe integer.");
}
if (!Number.isFinite(signalStartTime)) {
throw new TypeError("signalStartTime must be finite.");
}
if (!Number.isSafeInteger(signalStartSample) || signalStartSample < 0) {
throw new RangeError("signalStartSample must be a non-negative safe integer.");
}
const signal = entry.signal;
const seed = (scene.seed + (signal.seedOffset ?? 0) + hashString(entry.id ?? "signal") + seedSalt) >>> 0;
for (let sampleIndex = startSample; sampleIndex < endSample; sampleIndex += 1) {
const absoluteSampleIndex = sceneSampleOffset + sampleIndex;
const localIndex = absoluteSampleIndex - signalStartSample;
const localTime = absoluteSampleIndex / scene.sampleRate - signalStartTime;
let value;
if (signal.type === "tone") {
value = Math.sin(TAU * signal.frequency * localTime + ((signal.phaseDeg ?? 0) * Math.PI) / 180);
} else if (signal.type === "noise") {
value = signal.color === "pink" ? pinkNoiseAt(seed, localIndex) : whiteNoiseAt(seed, localIndex);
} else if (signal.type === "asset") {
value = sampleAsset(signal, assetBuffers, localTime);
} else {
throw new Error(`지원하지 않는 signal type: ${signal.type}`);
}
output[sampleIndex] = value * gain;
}
return output;
}
export function sampleLinear(buffer, sampleIndex) {
if (sampleIndex < 0 || sampleIndex >= buffer.length) return 0;
const left = Math.floor(sampleIndex);
const fraction = sampleIndex - left;
const rightValue = buffer[Math.min(left + 1, buffer.length - 1)];
return buffer[left] + (rightValue - buffer[left]) * fraction;
}