-
Notifications
You must be signed in to change notification settings - Fork 0
/
gui.html
359 lines (328 loc) · 9.67 KB
/
gui.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta charset="UTF-8">
<title>Spectrogram viewer</title>
<style>
body {
margin: 0;
padding: 2px;
}
#canvasdiv {
width: 100%;
overflow-x: scroll;
padding: 0;
position: relative;
}
canvas {
margin: 0;
padding: 0;
}
input {
width: 100%;
}
table {
width: 100%;
}
td.a {
width: 10em;
}
.freq_scale {
position: absolute;
top: 0px;
background-color: #00000080;
color: #80ff80;
border-left: 1px solid red;
}
.time_scale {
position: absolute;
left: 0px;
background-color: #00000080;
color: #80ff80;
border-top: 1px solid red;
}
</style>
</head>
<body>
<div id="canvasdiv"><canvas id="canvas"></canvas></div>
<table>
<tr>
<td class="a">Cursor</td>
<td class="b" id="info">-</td>
</tr>
<tr>
<td class="a">Palette contrast</td>
<td class="b"><input type="range" id="slider1" onchange="update_colors()"
min="0" max="10" step="0.1" value="1"></td>
</tr>
<tr>
<td class="a">Palette offset</td>
<td class="b"><input type="range" id="slider2" onchange="update_colors()"
min="0" max="4000" step="10" value="300"></td>
</tr>
<tr>
<td class="a">Time scroll</td>
<td class="b"><input type="range" id="slider3" onchange="update_zoom()"
min="0" max="1000" step="250" value="0"></td>
</tr>
<tr>
<td class="a">Time zoom</td>
<td class="b"><input type="range" id="slider4" onchange="update_zoom()"
min="-8" max="1" step="1" value="-2"></td>
</tr>
<tr>
<td class="a">Frequency scroll</td>
<td class="b"><input type="range" id="slider5" onchange="update_zoom()"
min="0" max="1000" step="250" value="0"></td>
</tr>
<tr>
<td class="a">Frequency zoom</td>
<td class="b"><input type="range" id="slider6" onchange="update_zoom()"
min="-4" max="1" step="1" value="-2"></td>
</tr>
<tr>
<td class="a">Data file</td>
<td class="b"><input type="file" id="fileinput"></td>
</tr>
</table>
<script type="application/javascript">
/*
* Parameters
*/
var bins; // Number of bins
var nrecords = 0; // Number of records in loaded file
var bin_khz; // Spacing of bins in kHz
var canvasw, canvash; // Size of the canvas
var pixel0_khz; // First pixel in kHz
var pixel_khz; // Spacing of pixels in kHz (in x direction)
var zoomy = 1; // Records per pixel (in y direction)
var databytes = 1; // Bytes per bin in data file
var record_bytes = 0; // Bytes per measurement record
var spectrum_offset = 0; // Offset of spectrum data within a record
var has_time = false; // Does the file have timestamps
/* Various objects */
const reader = new FileReader();
const canvasdiv = document.getElementById("canvasdiv");
const canvas = document.getElementById("canvas");
const slider1 = document.getElementById("slider1");
const slider2 = document.getElementById("slider2");
const slider3 = document.getElementById("slider3");
const slider4 = document.getElementById("slider4");
const slider5 = document.getElementById("slider5");
const slider6 = document.getElementById("slider6");
const info = document.getElementById("info");
const ctx = canvas.getContext("2d");
var img; // ImageData used to draw a line to the canvas
var imgdata; // Uint32Array view to img
var view = null; // View to the data file
/* Intermediate Uint16Array for data after zooming
* but before mapping to a palette */
var zoomed;
/*
* Precalculate color palette.
*
* View both the image data and the palette as Uint32Arrays
* so that all 4 bytes of a pixel can be copied at once
* and indexing becomes simpler.
*/
function generate_palette() {
const colors = [[0,0,0], [0,0,255], [255,128,0], [255,255,255]];
const gradientlen = 0x100;
const palette = new Uint8Array(4 * gradientlen * (colors.length-1));
i = 0;
for (let c=0; c<colors.length-1; c++) {
for(let j=0; j<gradientlen; j++) {
const b = j / gradientlen;
const a = 1.0 - b;
palette[i++] = Math.round(a * colors[c][0] + b * colors[c+1][0]);
palette[i++] = Math.round(a * colors[c][1] + b * colors[c+1][1]);
palette[i++] = Math.round(a * colors[c][2] + b * colors[c+1][2]);
palette[i++] = 255;
}
}
return new Uint32Array(palette.buffer);
};
const paldata = generate_palette();
function set_canvas_size(w, h) {
w = Math.round(w);
h = Math.round(h);
canvas.width = w;
canvas.height = h;
canvasw = w;
canvash = h;
img = ctx.createImageData(w, 1);
zoomed = new Uint16Array(w * h);
imgdata = new Uint32Array(img.data.buffer);
}
set_canvas_size(1280, 800);
/* Calculate relevant parameters based on FFT size and sample rate */
function set_data_parameters(fftsize, fs) {
bins = Math.round(fftsize / 2 + 1);
bin_khz = fs / fftsize / 1000;
}
function update_slider_bounds() {
slider5.value = 0;
slider5.max = bins;
slider3.value = 0;
slider3.max = nrecords;
}
set_data_parameters(16384, 100e6);
update_slider_bounds();
/* Draw an image to the canvas based on zoomed data
* and color palette settings */
function update_colors() {
const multiplier = slider1.value * 1;
const offset = slider2.value * 1;
const palmax = paldata.length - 1;
let zi=0;
for (let y=0; y<canvash; y++) {
for (let x=0; x<canvasw; x++) {
let v = ((zoomed[zi++] - offset) * multiplier);
v = Math.min(Math.max(Math.round(v), 0), palmax);
imgdata[x] = paldata[v];
}
ctx.putImageData(img, 0, y);
}
}
function update_zoom() {
const fx_offset = Math.round(slider5.value);
const fy_offset = Math.round(slider3.value);
pixel0_khz = bin_khz * fx_offset;
const zoomx = Math.pow(0.5, slider6.value);
zoomy = Math.pow(0.5, slider4.value);
const zoomxi = Math.max(1, Math.round(zoomx));
const zoomyi = Math.max(1, Math.round(zoomy));
pixel_khz = bin_khz * zoomx;
if (view == null) return;
let zi=0;
for (let y=0; y<canvash; y++) {
for (let x=0; x<canvasw; x++) {
// Bounds of the rectangle of bins under one pixel
let fx0 = fx_offset + Math.floor(x * zoomx);
let fy0 = fy_offset + Math.floor(y * zoomy);
let fx1 = fx0 + zoomxi;
let fy1 = fy0 + zoomyi;
let m = 0;
if (fx1 <= bins && fy1 <= nrecords) {
// Find the maximum value under one pixel
for (let fy=fy0; fy<fy1; fy++) {
if (databytes == 1) { // 8-bit data
let p = spectrum_offset + record_bytes * fy + fx0;
let p1 = spectrum_offset + record_bytes * fy + fx1;
for (;p < p1;) {
m = Math.max(m, view[p++] << 4);
}
} else { // 16-bit data
let p = spectrum_offset + record_bytes * fy + fx0 * 2;
let p1 = spectrum_offset + record_bytes * fy + fx1 * 2;
for (;p < p1; p += 2) {
m = Math.max(m, (view[p] << 8) | view[p+1]);
}
}
}
}
zoomed[zi++] = m;
}
}
update_colors();
update_scale();
}
/*
* Javascript magic to load a file
*/
var filename;
function file_loaded(ev) {
const d = ev.target.result;
view = new Uint8Array(d);
/* Parse parameters from filename */
s = filename.split(".")[0].split("_");
if (s.length >= 5) {
databytes = (s.length >= 6 && s[5] == "16") ? 2 : 1;
has_time = (s.length >= 7 && s[6] == "T");
set_data_parameters(s[4] * 1, s[3] * 1);
}
record_bytes = bins * databytes;
if (has_time) {
record_bytes += 12;
spectrum_offset = 12;
} else {
spectrum_offset = 0;
}
nrecords = Math.floor(view.length / record_bytes);
update_slider_bounds();
update_zoom();
}
reader.addEventListener("load", file_loaded);
function file_changed(ev) {
filename = ev.target.files[0].name;
reader.readAsArrayBuffer(ev.target.files[0]);
}
document.getElementById("fileinput").
addEventListener("change", file_changed);
// Read little endian number of given length (in bytes) from a view
function read_le(v, p, l) {
let n = 0, shift = 0;
for (let i = 0; i < l; i++) {
n |= v[p++] << shift;
shift += 8;
}
return n;
}
// Get time at a given y axis position in the canvas.
function time_at(y) {
const fy_offset = Math.round(slider3.value);
const fy = fy_offset + Math.floor(y * zoomy);
t_s = read_le(view, record_bytes * fy + 0, 8);
t_ns = read_le(view, record_bytes * fy + 8, 4);
return new Date(1e3 * t_s + 1e-6 * t_ns);
}
// Get frequency at a given x axis position in the canvas.
function freq_at(x) {
return pixel0_khz + pixel_khz * x;
}
function format_time_short(t) {
return t.toUTCString().substr(-12,8);
}
var freq_scale = [];
var time_scale = [];
function create_scale() {
for (let x = 0; x < canvasw; x += 100) {
let e = document.createElement("span");
e.className = "freq_scale";
e.style.left = "" + x + "px";
canvasdiv.appendChild(e);
freq_scale.push([x, e]);
}
for (let y = 25; y < canvash; y += 50) {
let e = document.createElement("span");
e.className = "time_scale";
e.style.top = "" + y + "px";
canvasdiv.appendChild(e);
time_scale.push([y, e]);
}
}
function update_scale() {
for (const e of freq_scale) {
e[1].innerText = "" + freq_at(e[0]).toFixed(0) + " kHz";
}
for (const e of time_scale) {
e[1].innerText = format_time_short(time_at(e[0]));
}
}
/*
* Info field.
*/
function update_info(ev) {
let t = "";
if (has_time) {
t = ", " + time_at(ev.offsetY).toUTCString();
}
info.innerText = "" + freq_at(ev.offsetX).toFixed(1) + " kHz" + t;
}
canvas.addEventListener("mousemove", update_info);
create_scale();
update_zoom();
</script>
</body>
</html>