1/* Synthetic FT8 spectrum: visual simulation, not encoded or transmitted audio. 2 * Timing and tone spacing follow the WSJT-X FT8 protocol description. 3 */ 4(() => { 5 "use strict"; 6 const stations = [ 7 [356, .65, 0], [822, .88, 1], [1076, .4, 0], [1456, .73, 1], 8 [1860, .48, 0], [2226, .65, 1], [2336, .9, 0], [2856, .54, 1], 9 [465, .8, 1], [610, .65, 0], [715, .85, 1], [940, .6, 0], 10 [1190, .95, 1], [1320, .7, 0], [1570, .85, 0], [1690, .6, 1], 11 [1980, .9, 1], [2110, .55, 0], [2490, .75, 1], [2660, .6, 0] 12 ]; 13 const maximumHz = 3000; 14 const rowSeconds = .4; 15 function noise(seed) { 16 let value = Math.imul(seed ^ 0x9e3779b9, 0x85ebca6b); 17 value = Math.imul(value ^ (value >>> 13), 0xc2b2ae35); 18 return ((value ^ (value >>> 16)) >>> 0) / 4294967296; 19 } 20 function signalAt(seconds, index) { 21 const [base, strength, parity] = stations[index]; 22 const mode = window.dccDemoStation?.mode || "FT8"; 23 const period = mode === "FT4" ? 7.5 : mode === "WSPR" ? 120 : 15; 24 const duration = mode === "FT4" ? 5.04 : mode === "WSPR" ? 110.6 : 12.64; 25 const symbolTime = mode === "FT4" ? .048 : mode === "WSPR" ? .682667 : mode === "RTTY" ? 1/45.45 : .16; 26 const slot = Math.floor(seconds / period); 27 const elapsed = seconds - slot * period - .5; 28 if (elapsed < 0 || elapsed >= duration || ((slot % 2) + 2) % 2 !== parity) return null; 29 const symbol = Math.floor(elapsed / symbolTime); 30 const tones = mode === "RTTY" ? 2 : ["FT4","WSPR"].includes(mode) ? 4 : mode === "PSK31" ? 1 : 8; 31 const spacing = mode === "FT4" ? 20.8333 : mode === "WSPR" ? 1.4648 : mode === "RTTY" ? 170 : 6.25; 32 const tone = Math.floor(noise(index * 100003 + slot * 101 + symbol * 7919) * tones); 33 const fade = .8 + .2 * Math.sin(seconds * .9 + index); 34 return {hz: (mode === "WSPR" ? 1410 + index*8 : base) + tone * spacing, amplitude: strength * fade, symbol}; 35 } 36 function spectrumAt(seconds, width) { 37 const values = new Float32Array(width); 38 const row = Math.floor(seconds / rowSeconds); 39 for (let x = 0; x < width; x++) { 40 values[x] = .045 + .045 * noise(row * 10007 + x * 37) + .01 * Math.sin(x / 70); 41 } 42 stations.forEach((station, index) => { 43 // Integrate several symbols. Single-pixel instantaneous tones disappear 44 // when the app scales its canvas down to the compact waterfall panel. 45 for (const sample of [seconds, seconds - .16, seconds - .32, seconds - .48]) { 46 const signal = signalAt(sample, index); 47 if (!signal) continue; 48 const center = signal.hz / maximumHz * width; 49 const sigma = Math.max(1.1, 5 / maximumHz * width); 50 for (let x = Math.max(0, Math.floor(center - sigma * 4)); x < Math.min(width, Math.ceil(center + sigma * 4)); x++) { 51 const distance = (x - center) / sigma; 52 const texture = .85 + .3 * noise(row * 97 + x * 13 + index * 7919); 53 values[x] += .55 * signal.amplitude * texture * Math.exp(-.5 * distance * distance); 54 } 55 } 56 }); 57 return values; 58 } 59 const palette = [[0, [1, 5, 12]], [.1, [7, 21, 45]], [.2, [0, 85, 150]], [.32, [55, 200, 100]], [.45, [245, 238, 20]], [.7, [255, 120, 0]], [1, [240, 20, 10]]]; 60 function color(value) { 61 const level = Math.max(0, Math.min(1, value)); 62 const upper = palette.findIndex(stop => stop[0] >= level); 63 if (upper <= 0) return palette[0][1]; 64 const [low, a] = palette[upper - 1]; 65 const [high, b] = palette[upper]; 66 return a.map((channel, index) => Math.round(channel + (b[index] - channel) * (level - low) / (high - low))); 67 } 68 window.dccDemoWaterfall = {spectrumAt, signalAt, color, maximumHz, rowSeconds}; 69})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.