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; }