import { BaseSource } from './base-source.js'; function clamp(value, min, max) { return Math.min(max, Math.max(min, value)); } function createDeterministicNoise(seed) { const x = Math.sin(seed * 12.9898) * 43758.5453; return x - Math.floor(x); } export class SyntheticWaveSource extends BaseSource { constructor(config = {}) { super({ sampleRateHz: 60, preset: 'telemetry', amplitude: 1, noise: 0.08, ...config, }); this.sourceType = 'synthetic-wave'; this.lastEmittedPlotTimeMs = 0; } start(startTimeMs = 0) { super.start(); this.lastEmittedPlotTimeMs = startTimeMs; } stop() { super.stop(); } reset(startTimeMs = 0) { this.lastEmittedPlotTimeMs = startTimeMs; } sampleValue(timeMs) { const seconds = timeMs / 1000; const amplitude = this.config.amplitude; const noise = this.config.noise; const grain = (createDeterministicNoise(timeMs * 0.017) - 0.5) * 2 * noise; switch (this.config.preset) { case 'chirp': { const sweep = Math.sin(seconds * seconds * 1.4); return amplitude * (0.7 * sweep + 0.3 * Math.sin(seconds * 7.5)) + grain; } case 'burst': { const burstPhase = (seconds % 6) - 1.5; const burst = Math.sin(seconds * 9.5) * Math.exp(-(burstPhase ** 2) * 0.8); return amplitude * (0.45 * Math.sin(seconds * 2.1) + burst) + grain; } case 'telemetry': default: { const carrier = Math.sin(seconds * 2.2); const secondary = 0.35 * Math.cos(seconds * 6.4 + Math.sin(seconds * 0.8)); const envelope = 0.15 * Math.sin(seconds * 0.33); return amplitude * (carrier + secondary + envelope) + grain; } } } update(currentPlotTimeMs) { if (!this.running) { return []; } const intervalMs = 1000 / clamp(this.config.sampleRateHz, 1, 240); if (currentPlotTimeMs < this.lastEmittedPlotTimeMs) { this.lastEmittedPlotTimeMs = currentPlotTimeMs; return []; } const points = []; while (this.lastEmittedPlotTimeMs + intervalMs <= currentPlotTimeMs) { this.lastEmittedPlotTimeMs += intervalMs; points.push({ timeMs: this.lastEmittedPlotTimeMs, value: this.sampleValue(this.lastEmittedPlotTimeMs), sourceId: 'synthetic-wave', }); } return points; } }