git.s-ol.nu xxy-oscilloscope / 503099b
initial import s-ol 5 years ago
2 changed file(s) with 1761 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 <!DOCTYPE html>
1 <!-- saved from url=(0029)https://dood.al/oscilloscope/ -->
2 <html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
3 <title>XXY Oscilloscope</title>
4 <style>
5 audio { width: 100%; }
6 body{font: 12px Courier, Monospace; line-height:70%;}
7 canvas{margin-right: 10px;}
8 table {
9 border-spacing:0;
10 border-collapse: collapse;
11 }
12
13 </style>
14 </head>
15
16 <body bgcolor="silver" text="black" autocomplete="off">
17
18 <script>
19 var controls=
20 {
21 swapXY : false,
22 light : false,
23 sweepOn : false,
24 sweepMsDiv : 1,
25 sweepTriggerValue : 0,
26 signalGeneratorOn : false,
27 mainGain : 0.0,
28 exposureStops : 0.0,
29 audioVolume : 1.0,
30 hue : 125,
31 freezeImage: false,
32 disableFilter: false,
33 aValue : 1.0,
34 aExponent : 0.0,
35 bValue : 1.0,
36 bExponent :0.0,
37 invertXY : false,
38 grid : true,
39 persistence : 0,
40 xExpression : "sin(2*PI*a*t)*cos(2*PI*b*t)",
41 yExpression : "cos(2*PI*a*t)*cos(2*PI*b*t)",
42 }
43
44 Number.prototype.toFixedMinus = function(k)
45 {
46 if (this<0) return this.toFixed(k);
47 //else return '\xa0'+this.toFixed(k);
48 else return '+'+this.toFixed(k);
49 }
50
51 var toggleVisible = function(string)
52 {
53 var element = document.getElementById(string);
54 //console.log(element.style.display);
55 if (element.style.display == "none") element.style.display="block";
56 else element.style.display = "none";
57 }
58
59
60
61 </script>
62
63
64 <table align="center">
65 <tbody><tr>
66
67 <td valign="top">
68 <canvas id="crtCanvas" width="584" height="584" style="z-index: 0;"></canvas>
69 <div id="canvasFailure" style="position: relative; z-index: 1; font:25px arial; top:-40px; color:lightgreen;"></div>
70 </td>
71 <td width="360" valign="top">
72
73 <p style="font-size:5px;">&nbsp;</p>
74
75 <b id="title" style="font-size:26px;">&nbsp;XXY OSCILLOSCOPE </b> <b id="samplerate">44.1kHz</b>
76 <a href="javascript:toggleVisible(&#39;introNotes&#39;);" style="float:right;margin-top:4px"><big>?</big></a>
77 <br>
78
79 <div id="introNotes" style="display:none">
80 <p style="line-height:110%">Version 1.0 (April 2017). Made
81 by <a href="http://venuspatrol.nfshost.com/">Neil Thapen</a>.<br>
82 Thanks to m1el and ompuco for inspiration.<br>
83 Line-drawing code adapted from
84 <a href="https://github.com/m1el/woscope">woscope</a> by m1el.<br>
85 <br>
86 This uses an upsampling filter to simulate a digital-analogue
87 converter between the computer and the oscilloscope. It turns sharp corners
88 in the signal into curves, loops or ringing artefacts.
89 If the page is running slowly, try disabling upsampling.
90 </p></div>
91
92 <br>
93
94 <hr noshade="" style="margin-top:10px">
95
96 <table>
97 <tbody><tr>
98 <td> <!--TOP LEFT CONTROL-->
99 <table>
100 <tbody><tr><td align="center">Gain</td></tr>
101 <tr><td><input id="mainGain" type="range" width="200" min="-1" max="4" value="0.0" step="0.05" oninput="controls.mainGain=mainGain.value; mainGainOutput.value=parseFloat(mainGain.value).toFixedMinus(2)+&#39; &#39;"></td>
102 <td> <output id="mainGainOutput">+0.00&nbsp;</output></td>
103 </tr></tbody></table>
104 </td>
105
106 <td> <!--TOP RIGHT CONTROL-->
107 <table>
108 <tbody><tr><td align="center">Intensity</td></tr>
109 <tr><td><input id="exposure" type="range" width="200" min="-2" max="2" value="0.0" step="0.1" oninput="controls.exposureStops=this.value; exposureOutput.value=parseFloat(this.value).toFixedMinus(1)"></td>
110 <td> <output id="exposureOutput">+0.0</output></td>
111 </tr></tbody></table>
112 </td>
113
114 </tr><tr>
115 <td> <!--BOTTOM LEFT CONTROL-->
116 <table>
117 <tbody><tr><td align="center">Audio volume</td></tr>
118 <tr><td><input id="audioVolume" type="range" width="200" min="0" max="1" value="1.0" step="0.01" oninput="controls.audioVolume=this.value; audioVolumeOutput.value=parseFloat(this.value).toFixed(2)"></td>
119 <td> <output id="audioVolumeOutput">1.00</output></td>
120 </tr></tbody></table>
121 </td>
122
123
124
125 <td> <!--BOTTOM RIGHT CONTROL-->
126 <table>
127 <tbody><tr><td> &nbsp;
128 <input id="swapXY" type="checkbox" onchange="controls.swapXY=this.checked"> Swap x / y
129 </td></tr>
130 <tr><td> &nbsp;
131 <input id="invertXY" type="checkbox" onchange="controls.invertXY=this.checked"> Invert x and y
132 </td></tr>
133 <tr><td> &nbsp;
134 <input id="light" type="checkbox" onchange="controls.light=this.checked"> Graticule light
135 </td></tr>
136 </tbody></table>
137 </td>
138
139 </tr>
140 </tbody></table>
141
142
143
144
145 <hr noshade="">
146
147 <p><b style="font-size:18px">
148 <input id="sweepCheckbox" type="checkbox" onchange="controls.sweepOn=this.checked"> SWEEP</b>
149 <a href="javascript:toggleVisible(&#39;sweepNotes&#39;);" style="float:right;margin-top:3px"><big>?</big></a>
150
151 </p><table>
152 <tbody><tr>
153 <td>
154 <table>
155 <tbody><tr><td align="center">Trigger value</td></tr>
156 <tr><td><input id="trigger" type="range" width="200" min="-1" max="1" value="0.0" step="0.01" oninput="controls.sweepTriggerValue=this.value*Math.sqrt(Math.abs(this.value)); triggerOutput.value=parseFloat(controls.sweepTriggerValue).toFixedMinus(2)+&#39; &#39;"></td>
157 <td> <output id="triggerOutput">+0.00&nbsp;</output></td>
158 </tr></tbody></table>
159 </td>
160
161 <td>
162 <table>
163 <tbody><tr><td align="center">Milliseconds/div</td></tr>
164 <tr><td><input id="msDiv" type="range" width="200" min="0" max="7" value="2" step="1" oninput="controls.sweepMsDiv=Math.pow(2, this.value-2); msDivOutput.value = controls.sweepMsDiv"></td>
165 <td> <output id="msDivOutput">1</output></td>
166 </tr></tbody></table>
167 </td>
168 </tr>
169 </tbody></table>
170
171 <div id="sweepNotes" style="display:none">
172 <p style="line-height:110%">
173 The trace moves to the right at a fixed speed. Once
174 off the screen, it restarts from the left as soon as <i>y</i>
175 moves above the trigger value.
176 </p></div>
177
178 <hr noshade="">
179
180 <p><b style="font-size:18px">
181 <input id="generatorCheckbox" type="checkbox" onchange="controls.signalGeneratorOn=this.checked; AudioSystem.connectMicrophone();"> SIGNAL GENERATOR</b>
182 <a href="javascript:toggleVisible(&#39;generatorNotes&#39;);" style="float:right;margin-top:3px"><big>?</big></a>
183
184 </p><div id="generatorNotes" style="display:none">
185 <p style="line-height:110%">
186 Enter mathematical expressions (in javascript). <br>
187 <i>t</i> is time and <i>n</i> is the number of samples so far.<br>
188 You can use <i>mx</i> and <i>my</i> for the signal from microphone, if it's active.
189 </p></div>
190
191 <p>&nbsp;x = <input type="text" size="37" id="xInput" value="" onkeydown="if (event.keyCode == 13) {UI.compile(); xNote.value=&#39;&#39;; this.style.color=&#39;black&#39;;}" oninput="if (this.value != controls.xExpression) {xNote.value=&#39;*&#39;; this.style.color=&#39;blue&#39;;} else {xNote.value=&#39;&#39;; this.style.color=&#39;black&#39;;}">
192 <output id="xNote"> </output><br>
193
194 &nbsp;y = <input type="text" size="37" id="yInput" value="" onkeydown="if (event.keyCode == 13) {UI.compile(); yNote.value=&#39;&#39;; this.style.color=&#39;black&#39;;}" oninput="if (this.value != controls.yExpression) {yNote.value=&#39;*&#39;; this.style.color=&#39;blue&#39;;} else {yNote.value=&#39;&#39;; this.style.color=&#39;black&#39;;}">
195 <output id="yNote"> </output><br>
196
197
198 </p><table border="0">
199 <tbody><tr>
200 <td width="155"></td> <td width="45"></td><td width="155"></td> <td width="45"></td>
201 </tr><tr>
202 <td align="right">Parameter a</td> <td></td>
203 <td><input id="aExponent" type="range" style="width:90%" min="0" max="3" value="0" step="1" oninput="controls.aExponent=this.value; aExponentOutput.value=[&#39; x1&#39;,&#39; x10&#39;,&#39;x100&#39;,&#39;x1000&#39;][this.value]"></td>
204 <td> <output id="aExponentOutput"> x1</output></td>
205 </tr><tr>
206 <td colspan="3"><input id="aValue" type="range" style="width:95%" min="0.5" max="5.00" value="1.0" step="0.02" oninput="controls.aValue=this.value; aValueOutput.value=parseFloat(this.value).toFixed(2)"><br></td>
207 <td> <output id="aValueOutput">1.00</output></td>
208 </tr>
209 <tr><td height="5"></td></tr>
210 <tr>
211 <td align="right">Parameter b</td> <td></td>
212 <td><input id="bExponent" type="range" style="width:90%" min="0" max="3" value="0" step="1" oninput="controls.bExponent=this.value; bExponentOutput.value=[&#39; x1&#39;,&#39; x10&#39;,&#39;x100&#39;,&#39;x1000&#39;][this.value]"></td>
213 <td> <output id="bExponentOutput"> x1</output></td>
214 </tr><tr>
215 <td colspan="3"><input id="bValue" type="range" style="width:95%" min="0.5" max="5.00" value="1.0" step="0.02" oninput="controls.bValue=this.value; bValueOutput.value=parseFloat(this.value).toFixed(2)"></td>
216 <td> <output id="bValueOutput">1.00</output></td></tr>
217 </tbody></table>
218
219
220
221 <hr noshade="">
222
223 <p><b style="font-size:18px">
224 <input type="checkbox" id="micCheckbox" onchange="if (this.checked) AudioSystem.tryToGetMicrophone(); else AudioSystem.disconnectMicrophone()"> MICROPHONE</b>
225 <output id="microphoneOutput"></output>
226 <a href="javascript:toggleVisible(&#39;micNotes&#39;);" style="float:right;margin-top:3px"><big>?</big></a>
227
228 </p><div id="micNotes" style="display:none">
229 <p style="line-height:110%">
230 <i>x</i> is the left channel, <i>y</i> the right.<br>
231 Unavailable in Safari. Only stereo in Chrome.<br>
232 <br>
233 To get audio from another program,
234 you can either physically connect your audio output to your audio input,
235 or use third party software,
236 such as <a href="http://vb-audio.pagesperso-orange.fr/Cable/">VB-CABLE</a> on Windows
237 or <a href="https://github.com/mattingalls/Soundflower">Soundflower</a> with
238 <a href="https://github.com/mLupine/SoundflowerBed">SoundflowerBed</a> on MacOS.
239 </p></div>
240
241 <hr noshade="">
242
243 <table>
244 <tbody><tr>
245 <td width="200"><b style="font-size:18px"> PLAY FILE<b></b></b></td>
246 <td width="200"><input id="audioFile" type="file" accept="audio/*"></td>
247 <td><a href="javascript:toggleVisible(&#39;fileNotes&#39;);"><big>?</big></a></td>
248 </tr>
249 </tbody></table>
250
251 <p><audio id="audioElement" controls=""></audio>
252
253 <script>
254 var file;
255 audioFile.onchange = function()
256 {
257 if (file) URL.revokeObjectURL(file)
258 var files = this.files;
259 file = URL.createObjectURL(files[0]);
260 audioElement.src = file;
261 audioElement.play();
262 };
263 </script>
264
265 </p><div id="fileNotes" style="display:none">
266 <p style="line-height:110%">
267 <i>x</i> is the left channel, <i>y</i> the right.<br>
268 Have a look at <a href="http://oscilloscopemusic.com/">oscilloscopemusic.com</a>
269 for music written to be displayed like this.
270 </p></div>
271
272 <hr noshade="">
273
274 <a href="javascript:toggleVisible(&#39;extraNotes&#39;);" style="float:right;margin-top:3px"><big>?</big></a>
275
276 <table><tbody><tr>
277
278 <td>
279 <table>
280 <tbody><tr><td align="center">Hue</td></tr>
281 <tr><td><input id="hue" type="range" width="200" min="0" max="359" value="125" step="1" oninput="controls.hue=this.value; hueOutput.value=this.value"></td>
282 <td width="30"> <output id="hueOutput">125</output></td>
283 </tr>
284 <tr><td align="center">Persistence</td></tr>
285 <tr><td><input id="persistence" type="range" width="200" min="-1" max="1" value="0" step="0.01" oninput="controls.persistence=this.value; persistenceOutput.value=parseFloat(this.value).toFixedMinus(1)"></td>
286 <td width="30"> <output id="persistenceOutput">0.00</output></td>
287 </tr>
288 </tbody></table>
289 </td>
290
291 <td>
292 <table>
293 <tbody><tr><td>
294 &nbsp; <input id="freeze" type="checkbox" onchange="controls.freezeImage=this.checked"> Freeze image
295 </td>
296
297 </tr>
298 <tr><td>
299 &nbsp; <input id="disableFilter" type="checkbox" onchange="controls.disableFilter=this.checked"> Disable upsampling
300 </td></tr>
301 <tr><td>
302 &nbsp; <input id="hideGrid" type="checkbox" onchange="controls.grid=!this.checked; if (Render) Render.screenTexture = Render.loadTexture(&#39;noise.jpg&#39;);"> Hide graticule
303 </td></tr>
304 </tbody></table>
305 </td>
306 </tr></tbody></table>
307
308 <input id="urlText" type="text" size="28" style="margin-top:5px" onclick="Controls.generateUrl()" value=" export current settings as a URL">
309 &nbsp;<a href="javascript:Controls.restoreDefaults();">[reset all]</a>
310
311 <div id="extraNotes" style="display:none">
312 <p style="line-height:110%">
313 To share your settings, click on the textbox, copy the URL that appears there, and
314 send it to someone else.
315 </p></div>
316
317 <script>
318
319 var Controls = {
320 generateUrl : function()
321 {
322 var locationString = location.toString();
323 var site = locationString.split('#')[0];
324 var text = this.getControlsArray().toString();
325 var hm = encodeURI(text);
326 urlText.value = site+'#'+hm;
327 urlText.select();
328 },
329
330 getControlsArray : function()
331 {
332 var a = [];
333 a.push(mainGain.value);
334 a.push(exposure.value);
335 //a.push(audioVolume.value);
336 a.push(0+swapXY.checked);
337 a.push(0+invertXY.checked);
338 a.push(0+light.checked);
339 a.push(0+sweepCheckbox.checked);
340 a.push(trigger.value);
341 a.push(msDiv.value);
342 a.push(0+generatorCheckbox.checked);
343 a.push(this.encodeString(xInput.value));
344 a.push(this.encodeString(yInput.value));
345 a.push(aExponent.value);
346 a.push(aValue.value);
347 a.push(bExponent.value);
348 a.push(bValue.value);
349 // don't try to record microphone status
350 a.push(hue.value);
351 a.push(persistence.value);
352 a.push(0+disableFilter.checked);
353 a.push(0+hideGrid.checked);
354 return a;
355 },
356
357 setupControls : function()
358 {
359 var locationString = location.toString();
360 if (!(locationString.includes('#'))) return;
361 var hash = locationString.split('#')[1];
362 var arrayString = decodeURI(hash);
363 var a = arrayString.split(',');
364 this.setupSlider(mainGain, a.shift());
365 this.setupSlider(exposure, a.shift());
366 //this.setupSlider(audioVolume, a.shift());
367 this.setupSlider(audioVolume, "0");
368 this.setupCheckbox(swapXY, a.shift());
369 this.setupCheckbox(invertXY, a.shift());
370 this.setupCheckbox(light, a.shift());
371 this.setupCheckbox(sweepCheckbox, a.shift());
372 this.setupSlider(trigger, a.shift());
373 this.setupSlider(msDiv, a.shift());
374 this.setupCheckbox(generatorCheckbox, a.shift());
375 this.setupString(xInput, a.shift());
376 this.setupString(yInput, a.shift());
377 this.setupSlider(aExponent, a.shift());
378 this.setupSlider(aValue, a.shift());
379 this.setupSlider(bExponent, a.shift());
380 this.setupSlider(bValue, a.shift());
381 this.setupSlider(hue, a.shift());
382 this.setupSlider(persistence, a.shift());
383 this.setupCheckbox(disableFilter, a.shift());
384 this.setupCheckbox(hideGrid, a.shift());
385 UI.compile();
386 },
387
388 encodeString : function(s)
389 {
390 s=s.replace(/ /g,"");
391 s=s.replace(/,/g,";");
392 return s;
393 },
394
395 decodeString : function(s)
396 {
397 s=s.replace(/;/g,",");
398 //now sanitize
399 var toSpaces = s.replace(/[(),+*-/=<>|&!.%]/g, " ");
400 var toSpaces = toSpaces.replace(/[0-9]/g, " ");
401 var words = toSpaces.split(' ');
402 var allowed = ["", "a", "b", "t", "n", "x", "y", "mx", "my", "E", "PI", "abs", "acos",
403 "asin", "atan", "ceil", "cos", "exp", "floor", "log", "max", "min", "pow", "random",
404 "round", "sin", "sqrt", "tan"];
405 for (var i=0; i<words.length; i++)
406 {
407 var found = false;
408 for (var j=0; j<allowed.length; j++)
409 {
410 if (words[i] == allowed[j]) found = true;
411 }
412 if (found == false) s="bad expression";
413 }
414 return s;
415 },
416
417 setupSlider : function(slider, s)
418 {
419 slider.value = parseFloat(s);
420 slider.oninput();
421 },
422
423 setupCheckbox : function(checkbox, s)
424 {
425 checkbox.checked = parseInt(s);
426 checkbox.onchange();
427 },
428
429 setupString : function(inp, s)
430 {
431 inp.value = this.decodeString(s);
432 },
433
434 restoreDefaults : function()
435 {
436 var locationString = location.toString();
437 var site = locationString.split('#')[0];
438 location = site;
439 }
440 }
441
442 </script>
443
444 <div id="extraNotes" style="display:none">
445 </div>
446
447
448 <!-- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -->
449
450
451 </td>
452 </tr>
453 </tbody></table>
454
455
456 <script id="vertex" type="x-shader">
457 attribute vec2 vertexPosition;
458 void main()
459 {
460 gl_Position = vec4(vertexPosition, 0.0, 1.0);
461 }
462 </script>
463
464 <script id="fragment" type="x-shader">
465 precision highp float;
466 uniform vec4 colour;
467 void main()
468 {
469 gl_FragColor = colour;
470 }
471 </script>
472
473 <!-- The Gaussian line-drawing code, the next two shaders, is adapted
474 from woscope by m1el : https://github.com/m1el/woscope -->
475
476 <script id="gaussianVertex" type="x-shader">
477 #define EPS 1E-6
478 uniform float uInvert;
479 uniform float uSize;
480 uniform float uNEdges;
481 uniform float uFadeAmount;
482 uniform float uIntensity;
483 uniform float uGain;
484 attribute vec2 aStart, aEnd;
485 attribute float aIdx;
486 varying vec4 uvl;
487 varying vec2 vTexCoord;
488 varying float vLen;
489 varying float vSize;
490 void main () {
491 float tang;
492 vec2 current;
493 // All points in quad contain the same data:
494 // segment start point and segment end point.
495 // We determine point position using its index.
496 float idx = mod(aIdx,4.0);
497
498 // `dir` vector is storing the normalized difference
499 // between end and start
500 vec2 dir = (aEnd-aStart)*uGain;
501 uvl.z = length(dir);
502
503 if (uvl.z > EPS)
504 {
505 dir = dir / uvl.z;
506 vSize = 0.006/pow(uvl.z,0.08);
507 }
508 else
509 {
510 // If the segment is too short, just draw a square
511 dir = vec2(1.0, 0.0);
512 vSize = 0.006/pow(EPS,0.08);
513 }
514
515 vSize = uSize;
516 vec2 norm = vec2(-dir.y, dir.x);
517
518 if (idx >= 2.0) {
519 current = aEnd*uGain;
520 tang = 1.0;
521 uvl.x = -vSize;
522 } else {
523 current = aStart*uGain;
524 tang = -1.0;
525 uvl.x = uvl.z + vSize;
526 }
527 // `side` corresponds to shift to the "right" or "left"
528 float side = (mod(idx, 2.0)-0.5)*2.0;
529 uvl.y = side * vSize;
530
531 uvl.w = uIntensity*mix(1.0-uFadeAmount, 1.0, floor(aIdx / 4.0 + 0.5)/uNEdges);
532
533 vec4 pos = vec4((current+(tang*dir+norm*side)*vSize)*uInvert,0.0,1.0);
534 gl_Position = pos;
535 vTexCoord = 0.5*pos.xy+0.5;
536 //float seed = floor(aIdx/4.0);
537 //seed = mod(sin(seed*seed), 7.0);
538 //if (mod(seed/2.0, 1.0)<0.5) gl_Position = vec4(10.0);
539 }
540 </script>
541
542 <script id="gaussianFragment" type="x-shader">
543 #define EPS 1E-6
544 #define TAU 6.283185307179586
545 #define TAUR 2.5066282746310002
546 #define SQRT2 1.4142135623730951
547 precision highp float;
548 uniform float uSize;
549 uniform float uIntensity;
550 uniform sampler2D uScreen;
551 varying float vSize;
552 varying vec4 uvl;
553 varying vec2 vTexCoord;
554
555 // A standard gaussian function, used for weighting samples
556 float gaussian(float x, float sigma)
557 {
558 return exp(-(x * x) / (2.0 * sigma * sigma)) / (TAUR * sigma);
559 }
560
561 // This approximates the error function, needed for the gaussian integral
562 float erf(float x)
563 {
564 float s = sign(x), a = abs(x);
565 x = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
566 x *= x;
567 return s - s / (x * x);
568 }
569
570 void main (void)
571 {
572 float len = uvl.z;
573 vec2 xy = uvl.xy;
574 float brightness;
575
576 float sigma = vSize/5.0;
577 if (len < EPS)
578 {
579 // If the beam segment is too short, just calculate intensity at the position.
580 brightness = gaussian(length(xy), sigma);
581 }
582 else
583 {
584 // Otherwise, use analytical integral for accumulated intensity.
585 brightness = erf(xy.x/SQRT2/sigma) - erf((xy.x-len)/SQRT2/sigma);
586 brightness *= exp(-xy.y*xy.y/(2.0*sigma*sigma))/2.0/len;
587 }
588
589 brightness *= uvl.w;
590 gl_FragColor = 2.0 * texture2D(uScreen, vTexCoord) * brightness;
591 gl_FragColor.a = 1.0;
592 }
593 </script>
594
595 <script id="texturedVertex" type="x-shader">
596 precision highp float;
597 attribute vec2 aPos;
598 varying vec2 vTexCoord;
599 void main (void)
600 {
601 gl_Position = vec4(aPos, 0.0, 1.0);
602 vTexCoord = (0.5*aPos+0.5);
603 }
604 </script>
605
606 <script id="texturedVertexWithResize" type="x-shader">
607 precision highp float;
608 attribute vec2 aPos;
609 varying vec2 vTexCoord;
610 uniform float uResizeForCanvas;
611 void main (void)
612 {
613 gl_Position = vec4(aPos, 0.0, 1.0);
614 vTexCoord = (0.5*aPos+0.5)*uResizeForCanvas;
615 }
616 </script>
617
618 <script id="texturedFragment" type="x-shader">
619 precision highp float;
620 uniform sampler2D uTexture0;
621 varying vec2 vTexCoord;
622 void main (void)
623 {
624 gl_FragColor = texture2D(uTexture0, vTexCoord);
625 gl_FragColor.a= 1.0;
626 }
627 </script>
628
629 <script id="blurFragment" type="x-shader">
630 precision highp float;
631 uniform sampler2D uTexture0;
632 uniform vec2 uOffset;
633 varying vec2 vTexCoord;
634 void main (void)
635 {
636 vec4 sum = vec4(0.0);
637 sum += texture2D(uTexture0, vTexCoord - uOffset*8.0) * 0.000078;
638 sum += texture2D(uTexture0, vTexCoord - uOffset*7.0) * 0.000489;
639 sum += texture2D(uTexture0, vTexCoord - uOffset*6.0) * 0.002403;
640 sum += texture2D(uTexture0, vTexCoord - uOffset*5.0) * 0.009245;
641 sum += texture2D(uTexture0, vTexCoord - uOffset*4.0) * 0.027835;
642 sum += texture2D(uTexture0, vTexCoord - uOffset*3.0) * 0.065592;
643 sum += texture2D(uTexture0, vTexCoord - uOffset*2.0) * 0.12098;
644 sum += texture2D(uTexture0, vTexCoord - uOffset*1.0) * 0.17467;
645 sum += texture2D(uTexture0, vTexCoord + uOffset*0.0) * 0.19742;
646 sum += texture2D(uTexture0, vTexCoord + uOffset*1.0) * 0.17467;
647 sum += texture2D(uTexture0, vTexCoord + uOffset*2.0) * 0.12098;
648 sum += texture2D(uTexture0, vTexCoord + uOffset*3.0) * 0.065592;
649 sum += texture2D(uTexture0, vTexCoord + uOffset*4.0) * 0.027835;
650 sum += texture2D(uTexture0, vTexCoord + uOffset*5.0) * 0.009245;
651 sum += texture2D(uTexture0, vTexCoord + uOffset*6.0) * 0.002403;
652 sum += texture2D(uTexture0, vTexCoord + uOffset*7.0) * 0.000489;
653 sum += texture2D(uTexture0, vTexCoord + uOffset*8.0) * 0.000078;
654 gl_FragColor = sum;
655 }
656 </script>
657
658 <script id="outputVertex" type="x-shader">
659 precision highp float;
660 attribute vec2 aPos;
661 varying vec2 vTexCoord;
662 varying vec2 vTexCoordCanvas;
663 uniform float uResizeForCanvas;
664 void main (void)
665 {
666 gl_Position = vec4(aPos, 0.0, 1.0);
667 vTexCoord = (0.5*aPos+0.5);
668 vTexCoordCanvas = vTexCoord*uResizeForCanvas;
669 }
670 </script>
671
672 <script id="outputFragment" type="x-shader">
673 precision highp float;
674 uniform sampler2D uTexture0; //line
675 uniform sampler2D uTexture1; //tight glow
676 uniform sampler2D uTexture2; //big glow
677 uniform sampler2D uTexture3; //screen
678 uniform float uExposure;
679 uniform float graticuleLight;
680 uniform vec3 uColour;
681 varying vec2 vTexCoord;
682 varying vec2 vTexCoordCanvas;
683 void main (void)
684 {
685 vec4 line = texture2D(uTexture0, vTexCoordCanvas);
686 // r components have grid; g components do not.
687 vec4 screen = texture2D(uTexture3, vTexCoord);
688 vec4 tightGlow = texture2D(uTexture1, vTexCoord);
689 vec4 scatter = texture2D(uTexture2, vTexCoord)+0.35;
690 float light = line.r + 1.5*screen.g*screen.g*tightGlow.r;
691 light += 0.4*scatter.g * (2.0+1.0*screen.g + 0.5*screen.r);
692 float tlight = 1.0-pow(2.0, -uExposure*light);
693 float tlight2 = tlight*tlight*tlight;
694 gl_FragColor.rgb = mix(uColour, vec3(1.0), 0.3+tlight2*tlight2*0.5)*tlight;
695 gl_FragColor.rgb = mix(gl_FragColor.rgb, (vec3(0.7)+0.3*uColour)*screen.b, graticuleLight);
696 //gl_FragColor.rgb += 0.4*(vec3(0.7)+0.3*uColour)*screen.b;
697 gl_FragColor.a= 1.0;
698 }
699 </script>
700
701 <script src="./oscilloscope.js"></script>
702 </body></html>
0 /*
1
2 XXY Oscilloscope
3
4 version 1.0, April 2017
5 by Neil Thapen
6 venuspatrol.nfshost.com
7
8 Copyright 2017 Neil Thapen
9
10 Permission is hereby granted, free of charge, to any person obtaining a copy
11 of this software and associated documentation files (the "Software"), to deal
12 in the Software without restriction, including without limitation the rights
13 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 copies of the Software, and to permit persons to whom the Software is
15 furnished to do so, subject to the following conditions:
16
17 The above copyright notice and this permission notice shall be included in
18 all copies or substantial portions of the Software.
19
20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26 THE SOFTWARE.
27
28 */
29
30
31 var AudioSystem =
32 {
33 microphoneActive : false,
34
35 init : function (bufferSize)
36 {
37 window.AudioContext = window.AudioContext||window.webkitAudioContext;
38 this.audioContext = new window.AudioContext();
39 this.sampleRate = this.audioContext.sampleRate;
40 this.bufferSize = bufferSize;
41 this.timePerSample = 1/this.sampleRate;
42 this.oldXSamples = new Float32Array(this.bufferSize);
43 this.oldYSamples = new Float32Array(this.bufferSize);
44 this.smoothedXSamples = new Float32Array(Filter.nSmoothedSamples);
45 this.smoothedYSamples = new Float32Array(Filter.nSmoothedSamples);
46
47 if (!(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia))
48 {
49 microphoneOutput.value = "unavailable in this browser";
50 }
51 },
52
53 startSound : function()
54 {
55 var audioElement = document.getElementById("audioElement");
56 this.source = this.audioContext.createMediaElementSource(audioElement);
57 this.audioVolumeNode = this.audioContext.createGain();
58
59 this.generator = this.audioContext.createScriptProcessor(this.bufferSize, 2, 2);
60 this.generator.onaudioprocess = SignalGenerator.generate;
61
62 this.scopeNode = this.audioContext.createScriptProcessor(this.bufferSize, 2, 2);
63 this.scopeNode.onaudioprocess = doScriptProcessor;
64 this.source.connect(this.scopeNode);
65 this.generator.connect(this.scopeNode);
66
67 this.scopeNode.connect(this.audioVolumeNode);
68 this.audioVolumeNode.connect(this.audioContext.destination);
69 },
70
71 tryToGetMicrophone : function()
72 {
73 if (this.microphoneActive)
74 {
75 this.connectMicrophone();
76 audioVolume.value = 0.0;
77 audioVolume.oninput();
78 return;
79 }
80
81 var constraints = {audio: { mandatory: { echoCancellation: false }}};
82 //var constraints = {audio: {echoCancellation: false} };
83 navigator.getUserMedia = navigator.getUserMedia ||
84 navigator.webkitGetUserMedia ||
85 navigator.mozGetUserMedia;
86 if (navigator.getUserMedia)
87 {
88 navigator.getUserMedia(constraints, onStream, function(){micCheckbox.checked = false;});
89 }
90 else
91 {
92 micCheckbox.checked = false;
93 }
94 },
95
96 disconnectMicrophone : function()
97 {
98 if (this.microphone) this.microphone.disconnect();
99 },
100
101 connectMicrophone : function()
102 {
103 if (!this.microphone) return;
104 if (!micCheckbox.checked) return;
105 this.disconnectMicrophone();
106 if (controls.signalGeneratorOn) AudioSystem.microphone.connect(AudioSystem.generator);
107 else AudioSystem.microphone.connect(AudioSystem.scopeNode);
108 }
109 }
110
111
112
113 onStream = function(stream)
114 {
115 AudioSystem.microphoneActive = true;
116 AudioSystem.microphone = AudioSystem.audioContext.createMediaStreamSource(stream);
117 AudioSystem.connectMicrophone();
118
119 audioVolume.value = 0.0;
120 audioVolume.oninput();
121 };
122
123 var SignalGenerator =
124 {
125 oldA : 1.0,
126 oldB : 1.0,
127 timeInSamples : 0,
128
129 generate : function(event)
130 {
131 var xMic = event.inputBuffer.getChannelData(0);
132 var yMic = event.inputBuffer.getChannelData(1);
133 var xOut = event.outputBuffer.getChannelData(0);
134 var yOut = event.outputBuffer.getChannelData(1);
135 var newA = controls.aValue * Math.pow(10.0, controls.aExponent);
136 var newB = controls.bValue * Math.pow(10.0, controls.bExponent);
137 var oldA = SignalGenerator.oldA;
138 var oldB = SignalGenerator.oldB;
139 //var PI = Math.PI;
140 //var cos = Math.cos;
141 //var sin = Math.sin;
142 with (Math)
143 {
144 var xFunc = eval("(function xFunc(){return "+controls.xExpression+";})");
145 var yFunc = eval("(function yFunc(){return "+controls.yExpression+";})");
146 }
147 var bufferSize = AudioSystem.bufferSize;
148 var timeInSamples = SignalGenerator.timeInSamples;
149 var sampleRate = AudioSystem.sampleRate;
150 var x = 0.0;
151 var y = 0.0;
152 if (!controls.signalGeneratorOn)
153 {
154 for (var i=0; i<bufferSize; i++)
155 {
156 xOut[i] = 0;
157 yOut[i] = 0;
158 }
159 }
160 else if ((newA == oldA) && (newB == oldB))
161 {
162 var n = timeInSamples;
163 for (var i=0; i<bufferSize; i++)
164 {
165 var t = n/sampleRate;
166 var a = newA;
167 var b = newB;
168 var mx = xMic[i];
169 var my = yMic[i];
170 x = xFunc();
171 y = yFunc();
172 xOut[i] = x;
173 yOut[i] = y;
174 n += 1;
175 }
176 }
177 else
178 {
179 var n = timeInSamples;
180 for (var i=0; i<bufferSize; i++)
181 {
182 var t = n/sampleRate;
183
184 var a = oldA;
185 var b = oldB;
186 var mx = xMic[i];
187 var my = yMic[i];
188 var oldX = xFunc();
189 var oldY = yFunc();
190 a = newA;
191 b = newB;
192 var newX = xFunc();
193 var newY = yFunc();
194 var alpha_z = i/bufferSize;
195 x = oldX*(1.0-alpha_z)+newX*alpha_z;
196 y = oldY*(1.0-alpha_z)+newY*alpha_z;
197
198 xOut[i] = x;
199 yOut[i] = y;
200 n += 1;
201 }
202 }
203
204 SignalGenerator.timeInSamples += AudioSystem.bufferSize;
205 SignalGenerator.oldA = newA;
206 SignalGenerator.oldB = newB;
207 }
208
209 }
210
211 var Filter =
212 {
213 lanczosTweak : 1.5,
214
215 init : function(bufferSize, a, steps)
216 {
217 this.bufferSize = bufferSize;
218 this.a = a;
219 this.steps = steps;
220 this.radius = a * steps;
221 this.nSmoothedSamples = this.bufferSize*this.steps + 1;
222 this.allSamples = new Float32Array(2*this.bufferSize);
223
224 this.createLanczosKernel();
225 },
226
227
228 generateSmoothedSamples : function (oldSamples, samples, smoothedSamples)
229 {
230 //this.createLanczosKernel();
231 var bufferSize = this.bufferSize;
232 var allSamples = this.allSamples;
233 var nSmoothedSamples = this.nSmoothedSamples;
234 var a = this.a;
235 var steps = this.steps;
236 var K = this.K;
237
238 for (var i=0; i<bufferSize; i++)
239 {
240 allSamples[i] = oldSamples[i];
241 allSamples[bufferSize+i] = samples[i];
242 }
243
244 /*for (var s= -a+1; s<a; s++)
245 {
246 for (var r=0; r<steps; r++)
247 {
248 if (r==0 && !(s==0)) continue;
249 var kernelPosition = -r+s*steps;
250 if (kernelPosition<0) k = K[-kernelPosition];
251 else k = K[kernelPosition];
252
253 var i = r;
254 var pStart = bufferSize - 2*a + s;
255 var pEnd = pStart + bufferSize;
256 for (var p=pStart; p<pEnd; p++)
257 {
258 smoothedSamples[i] += k * allSamples[p];
259 i += steps;
260 }
261 }
262 }*/
263
264 var pStart = bufferSize - 2*a;
265 var pEnd = pStart + bufferSize;
266 var i = 0;
267 for (var position=pStart; position<pEnd; position++)
268 {
269 smoothedSamples[i] = allSamples[position];
270 i += 1;
271 for (var r=1; r<steps; r++)
272 {
273 var smoothedSample = 0;
274 for (var s= -a+1; s<a; s++)
275 {
276 var sample = allSamples[position+s];
277 var kernelPosition = -r+s*steps;
278 if (kernelPosition<0) smoothedSample += sample * K[-kernelPosition];
279 else smoothedSample += sample * K[kernelPosition];
280 }
281 smoothedSamples[i] = smoothedSample;
282 i += 1;
283 }
284 }
285
286 smoothedSamples[nSmoothedSamples-1] = allSamples[2*bufferSize-2*a];
287 },
288
289 createLanczosKernel : function ()
290 {
291 this.K = new Float32Array(this.radius);
292 this.K[0] = 1;
293 for (var i =1; i<this.radius; i++)
294 {
295 var piX = (Math.PI * i) / this.steps;
296 var sinc = Math.sin(piX)/piX;
297 var window = this.a * Math.sin(piX/this.a) / piX;
298 this.K[i] = sinc*Math.pow(window, this.lanczosTweak);
299 }
300 }
301 }
302
303 var UI =
304 {
305 sidebarWidth : 360,
306
307 init : function()
308 {
309 var kHzText = (AudioSystem.sampleRate/1000).toFixed(1)+"kHz";
310 document.getElementById("samplerate").innerHTML=kHzText;
311 mainGain.oninput();
312 trigger.oninput();
313 this.xInput = document.getElementById("xInput");
314 this.yInput = document.getElementById("yInput");
315 this.xInput.value = controls.xExpression;
316 this.yInput.value = controls.yExpression;
317 },
318
319 compile : function() //doesn't compile anything anymore
320 {
321 controls.xExpression = this.xInput.value;
322 controls.yExpression = this.yInput.value;
323 }
324 }
325
326 var Render =
327 {
328 debug : 0,
329 failed : false,
330
331 init : function()
332 {
333 this.canvas = document.getElementById("crtCanvas");
334 this.onResize();
335 window.onresize = this.onResize;
336 if (!this.supportsWebGl)
337 {
338 Render.failed = true;
339 return;
340 }
341 window.gl = this.canvas.getContext("webgl", {preserveDrawingBuffer: true}, { alpha: false } );
342 gl.viewport(0, 0, this.canvas.width, this.canvas.height);
343 gl.enable(gl.BLEND);
344 gl.blendEquation( gl.FUNC_ADD );
345 gl.clearColor(0.0, 0.0, 0.0, 1.0);
346 gl.clear(gl.COLOR_BUFFER_BIT);
347 gl.colorMask(true, true, true, true);
348
349 var ext1 = gl.getExtension('OES_texture_float');
350 var ext2 = gl.getExtension('OES_texture_float_linear');
351 //this.ext = gl.getExtension('OES_texture_half_float');
352 //this.ext2 = gl.getExtension('OES_texture_half_float_linear');
353 if (!ext1 || !ext2)
354 {
355 Render.failed = true;
356 return;
357 }
358
359 this.fadeAmount = 0.2*AudioSystem.bufferSize/512;
360 this.fullScreenQuad = new Float32Array([
361 -1, 1, 1, 1, 1,-1, // Triangle 1
362 -1, 1, 1,-1, -1,-1 // Triangle 2
363 ]);
364
365 this.simpleShader = this.createShader("vertex","fragment");
366 this.simpleShader.vertexPosition = gl.getAttribLocation(this.simpleShader, "vertexPosition");
367 this.simpleShader.colour = gl.getUniformLocation(this.simpleShader, "colour");
368
369 this.lineShader = this.createShader("gaussianVertex","gaussianFragment");
370 this.lineShader.aStart = gl.getAttribLocation(this.lineShader, "aStart");
371 this.lineShader.aEnd = gl.getAttribLocation(this.lineShader, "aEnd");
372 this.lineShader.aIdx = gl.getAttribLocation(this.lineShader, "aIdx");
373 this.lineShader.uGain = gl.getUniformLocation(this.lineShader, "uGain");
374 this.lineShader.uSize = gl.getUniformLocation(this.lineShader, "uSize");
375 this.lineShader.uInvert = gl.getUniformLocation(this.lineShader, "uInvert");
376 this.lineShader.uIntensity = gl.getUniformLocation(this.lineShader, "uIntensity");
377 this.lineShader.uNEdges = gl.getUniformLocation(this.lineShader, "uNEdges");
378 this.lineShader.uFadeAmount = gl.getUniformLocation(this.lineShader, "uFadeAmount");
379 this.lineShader.uScreen = gl.getUniformLocation(this.lineShader, "uScreen");
380
381 this.outputShader = this.createShader("outputVertex","outputFragment");
382 this.outputShader.aPos = gl.getAttribLocation(this.outputShader, "aPos");
383 this.outputShader.uTexture0 = gl.getUniformLocation(this.outputShader, "uTexture0");
384 this.outputShader.uTexture1 = gl.getUniformLocation(this.outputShader, "uTexture1");
385 this.outputShader.uTexture2 = gl.getUniformLocation(this.outputShader, "uTexture2");
386 this.outputShader.uTexture3 = gl.getUniformLocation(this.outputShader, "uTexture3");
387 this.outputShader.uExposure = gl.getUniformLocation(this.outputShader, "uExposure");
388 this.outputShader.uColour = gl.getUniformLocation(this.outputShader, "uColour");
389 this.outputShader.uResizeForCanvas = gl.getUniformLocation(this.outputShader, "uResizeForCanvas");
390 this.outputShader.graticuleLight = gl.getUniformLocation(this.outputShader, "graticuleLight");
391
392 this.texturedShader = this.createShader("texturedVertexWithResize","texturedFragment");
393 this.texturedShader.aPos = gl.getAttribLocation(this.texturedShader, "aPos");
394 this.texturedShader.uTexture0 = gl.getUniformLocation(this.texturedShader, "uTexture0");
395 this.texturedShader.uResizeForCanvas = gl.getUniformLocation(this.texturedShader, "uResizeForCanvas");
396
397 this.blurShader = this.createShader("texturedVertex","blurFragment");
398 this.blurShader.aPos = gl.getAttribLocation(this.blurShader, "aPos");
399 this.blurShader.uTexture0 = gl.getUniformLocation(this.blurShader, "uTexture0");
400 this.blurShader.uOffset = gl.getUniformLocation(this.blurShader, "uOffset");
401
402 this.vertexBuffer = gl.createBuffer();
403 this.setupTextures();
404 },
405
406 admitFailure : function()
407 {
408 canvasFailure.innerHTML="&nbsp;&nbsp;sorry, it's not working"
409 },
410
411 setupArrays : function(nPoints)
412 {
413 this.nPoints = nPoints;
414 this.nEdges = this.nPoints-1;
415
416 this.quadIndexBuffer = gl.createBuffer();
417 var indices = new Float32Array(4*this.nEdges);
418 for (var i=0; i<indices.length; i++)
419 {
420 indices[i] = i;
421 }
422 gl.bindBuffer(gl.ARRAY_BUFFER, this.quadIndexBuffer);
423 gl.bufferData(gl.ARRAY_BUFFER, indices, gl.STATIC_DRAW);
424 gl.bindBuffer(gl.ARRAY_BUFFER, null);
425
426 this.vertexIndexBuffer = gl.createBuffer();
427 var len = this.nEdges * 2 * 3,
428 indices = new Uint16Array(len);
429 for (var i = 0, pos = 0; i < len;)
430 {
431 indices[i++] = pos;
432 indices[i++] = pos + 2;
433 indices[i++] = pos + 1;
434 indices[i++] = pos + 1;
435 indices[i++] = pos + 2;
436 indices[i++] = pos + 3;
437 pos += 4;
438 }
439 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.vertexIndexBuffer);
440 gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
441 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
442
443
444 this.scratchVertices = new Float32Array(8*nPoints);
445 },
446
447 setupTextures : function()
448 {
449 this.frameBuffer = gl.createFramebuffer();
450 this.lineTexture = this.makeTexture(1024, 1024);
451 this.onResize();
452 this.blur1Texture = this.makeTexture(256,256);
453 this.blur2Texture = this.makeTexture(256, 256);
454 this.blur3Texture = this.makeTexture(32, 32);
455 this.blur4Texture = this.makeTexture(32, 32);
456 this.screenTexture = this.loadTexture('noise.jpg');
457
458 // test floating point textures working
459 this.activateTargetTexture(this.lineTexture);
460 if (gl.FRAMEBUFFER_COMPLETE != gl.checkFramebufferStatus(gl.FRAMEBUFFER)) Render.failed=true;
461 console.log(gl.checkFramebufferStatus(gl.FRAMEBUFFER), gl.FRAMEBUFFER_COMPLETE, this.frameBuffer);
462 },
463
464 onResize : function()
465 {
466 var windowWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0)
467 var windowHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0)
468 var canvasSize = Math.min(windowHeight-21, windowWidth-UI.sidebarWidth-70);
469 Render.canvas.width = canvasSize;
470 Render.canvas.height = canvasSize;
471 if (Render.lineTexture)
472 {
473 var renderSize = Math.min(canvasSize, 1024);
474 Render.lineTexture.width = renderSize;
475 Render.lineTexture.height = renderSize;
476 //testOutputElement.value = windowHeight;
477 }
478
479 },
480
481 drawLineTexture : function(xPoints, yPoints)
482 {
483 this.fadeAmount = Math.pow(0.5, controls.persistence)*0.2*AudioSystem.bufferSize/512 ;
484 this.activateTargetTexture(this.lineTexture);
485 this.fade();
486 //gl.clear(gl.COLOR_BUFFER_BIT);
487 this.drawLine(xPoints, yPoints);
488 gl.bindTexture(gl.TEXTURE_2D, this.targetTexture);
489 gl.generateMipmap(gl.TEXTURE_2D);
490 },
491
492 drawCRT : function()
493 {
494 this.setNormalBlending();
495
496 this.activateTargetTexture(this.blur1Texture);
497 this.setShader(this.texturedShader);
498 gl.uniform1f(this.texturedShader.uResizeForCanvas, this.lineTexture.width/1024);
499 this.drawTexture(this.lineTexture);
500
501 //horizontal blur 256x256
502 this.activateTargetTexture(this.blur2Texture);
503 this.setShader(this.blurShader);
504 gl.uniform2fv(this.blurShader.uOffset, [1.0/256.0, 0.0]);
505 this.drawTexture(this.blur1Texture);
506
507 //vertical blur 256x256
508 this.activateTargetTexture(this.blur1Texture);
509 //this.setShader(this.blurShader);
510 gl.uniform2fv(this.blurShader.uOffset, [0.0, 1.0/256.0]);
511 this.drawTexture(this.blur2Texture);
512
513 //preserve blur1 for later
514 this.activateTargetTexture(this.blur3Texture);
515 this.setShader(this.texturedShader);
516 gl.uniform1f(this.texturedShader.uResizeForCanvas, 1.0);
517 this.drawTexture(this.blur1Texture);
518
519 //horizontal blur 64x64
520 this.activateTargetTexture(this.blur4Texture);
521 this.setShader(this.blurShader);
522 gl.uniform2fv(this.blurShader.uOffset, [1.0/32.0, 1.0/60.0]);
523 this.drawTexture(this.blur3Texture);
524
525 //vertical blur 64x64
526 this.activateTargetTexture(this.blur3Texture);
527 //this.setShader(this.blurShader);
528 gl.uniform2fv(this.blurShader.uOffset, [-1.0/60.0, 1.0/32.0]);
529 this.drawTexture(this.blur4Texture);
530
531 this.activateTargetTexture(null);
532 this.setShader(this.outputShader);
533 var brightness = Math.pow(2, controls.exposureStops-2.0);
534 //if (controls.disableFilter) brightness *= Filter.steps;
535 gl.uniform1f(this.outputShader.uExposure, brightness);
536 gl.uniform1f(this.outputShader.uResizeForCanvas, this.lineTexture.width/1024);
537 var colour = this.getColourFromHue(controls.hue);
538 gl.uniform3fv(this.outputShader.uColour, colour);
539 if (controls.light) gl.uniform1f(this.outputShader.graticuleLight, 0.15);
540 else gl.uniform1f(this.outputShader.graticuleLight, 0.0);
541 this.drawTexture(this.lineTexture, this.blur1Texture, this.blur3Texture, this.screenTexture);
542 },
543
544 getColourFromHue : function(hue)
545 {
546 var alpha = (hue/120.0) % 1.0;
547 var start = Math.sqrt(1.0-alpha);
548 var end = Math.sqrt(alpha);
549 var colour;
550 if (hue<120) colour = [start, end, 0.0];
551 else if (hue<240) colour = [0.0, start, end];
552 else colour = [end, 0.0, start];
553 return colour;
554 },
555
556 activateTargetTexture : function(texture)
557 {
558 if (texture)
559 {
560 gl.bindFramebuffer(gl.FRAMEBUFFER, this.frameBuffer);
561 gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
562 gl.viewport(0, 0, texture.width, texture.height);
563 }
564 else
565 {
566 gl.bindFramebuffer(gl.FRAMEBUFFER, null);
567 gl.viewport(0, 0, this.canvas.width, this.canvas.height);
568 }
569 this.targetTexture = texture;
570 },
571
572 setShader : function(program)
573 {
574 this.program = program;
575 gl.useProgram(program);
576 },
577
578 drawTexture : function(texture0, texture1, texture2, texture3)
579 {
580 //gl.useProgram(this.program);
581 gl.enableVertexAttribArray(this.program.aPos);
582
583 gl.activeTexture(gl.TEXTURE0);
584 gl.bindTexture(gl.TEXTURE_2D, texture0);
585 gl.uniform1i(this.program.uTexture0, 0);
586
587 if (texture1)
588 {
589 gl.activeTexture(gl.TEXTURE1);
590 gl.bindTexture(gl.TEXTURE_2D, texture1);
591 gl.uniform1i(this.program.uTexture1, 1);
592 }
593
594 if (texture2)
595 {
596 gl.activeTexture(gl.TEXTURE2);
597 gl.bindTexture(gl.TEXTURE_2D, texture2);
598 gl.uniform1i(this.program.uTexture2, 2);
599 }
600
601 if (texture3)
602 {
603 gl.activeTexture(gl.TEXTURE3);
604 gl.bindTexture(gl.TEXTURE_2D, texture3);
605 gl.uniform1i(this.program.uTexture3, 3);
606 }
607
608 gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
609 gl.bufferData(gl.ARRAY_BUFFER, this.fullScreenQuad, gl.STATIC_DRAW);
610 gl.vertexAttribPointer(this.program.aPos, 2, gl.FLOAT, false, 0, 0);
611 gl.bindBuffer(gl.ARRAY_BUFFER, null);
612
613 gl.drawArrays(gl.TRIANGLES, 0, 6);
614 gl.disableVertexAttribArray(this.program.aPos);
615
616 if (this.targetTexture)
617 {
618 gl.bindTexture(gl.TEXTURE_2D, this.targetTexture);
619 gl.generateMipmap(gl.TEXTURE_2D);
620 }
621 },
622
623 drawLine : function(xPoints, yPoints)
624 {
625 this.setAdditiveBlending();
626
627 var scratchVertices = this.scratchVertices;
628 //this.totalLength = 0;
629 var nPoints = xPoints.length;
630 for (var i=0; i<nPoints; i++)
631 {
632 var p = i*8;
633 scratchVertices[p]=scratchVertices[p+2]=scratchVertices[p+4]=scratchVertices[p+6]=xPoints[i];
634 scratchVertices[p+1]=scratchVertices[p+3]=scratchVertices[p+5]=scratchVertices[p+7]=yPoints[i];
635 /*if (i>0)
636 {
637 var xDelta = xPoints[i]-xPoints[i-1];
638 if (xDelta<0) xDelta = -xDelta;
639 var yDelta = yPoints[i]-yPoints[i-1];
640 if (yDelta<0) yDelta = -yDelta;
641 this.totalLength += xDelta + yDelta;
642 }*/
643 }
644 //testOutputElement.value = this.totalLength;
645
646 gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
647 gl.bufferData(gl.ARRAY_BUFFER, scratchVertices, gl.STATIC_DRAW);
648 gl.bindBuffer(gl.ARRAY_BUFFER, null);
649
650 var program = this.lineShader;
651 gl.useProgram(program);
652 gl.enableVertexAttribArray(program.aStart);
653 gl.enableVertexAttribArray(program.aEnd);
654 gl.enableVertexAttribArray(program.aIdx);
655
656 gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
657 gl.vertexAttribPointer(program.aStart, 2, gl.FLOAT, false, 0, 0);
658 gl.vertexAttribPointer(program.aEnd, 2, gl.FLOAT, false, 0, 8*4);
659 gl.bindBuffer(gl.ARRAY_BUFFER, this.quadIndexBuffer);
660 gl.vertexAttribPointer(program.aIdx, 1, gl.FLOAT, false, 0, 0);
661
662 gl.activeTexture(gl.TEXTURE0);
663 gl.bindTexture(gl.TEXTURE_2D, this.screenTexture);
664 gl.uniform1i(program.uScreen, 0);
665
666 gl.uniform1f(program.uSize, 0.015);
667 gl.uniform1f(program.uGain, Math.pow(2.0,controls.mainGain)*480/512);
668 if (controls.invertXY) gl.uniform1f(program.uInvert, -1.0);
669 else gl.uniform1f(program.uInvert, 1.0);
670 if (controls.disableFilter) gl.uniform1f(program.uIntensity, 0.005*(Filter.steps+1.5));
671 // +1.5 needed above for some reason for the brightness to match
672 else gl.uniform1f(program.uIntensity, 0.005);
673 gl.uniform1f(program.uFadeAmount, this.fadeAmount);
674 gl.uniform1f(program.uNEdges, this.nEdges);
675
676 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.vertexIndexBuffer);
677 var nEdgesThisTime = (xPoints.length-1);
678
679 /*if (this.totalLength > 300)
680 {
681 nEdgesThisTime *= 300/this.totalLength;
682 nEdgesThisTime = Math.floor(nEdgesThisTime);
683 }*/
684
685 gl.drawElements(gl.TRIANGLES, nEdgesThisTime * 6, gl.UNSIGNED_SHORT, 0);
686
687 gl.disableVertexAttribArray(program.aStart);
688 gl.disableVertexAttribArray(program.aEnd);
689 gl.disableVertexAttribArray(program.aIdx);
690 },
691
692 fade : function(alpha)
693 {
694 this.setNormalBlending();
695
696 var program = this.simpleShader;
697 gl.useProgram(program);
698 gl.enableVertexAttribArray(program.vertexPosition);
699 gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
700 gl.bufferData(gl.ARRAY_BUFFER, this.fullScreenQuad, gl.STATIC_DRAW);
701 gl.vertexAttribPointer(program.vertexPosition, 2, gl.FLOAT, false, 0, 0);
702 gl.bindBuffer(gl.ARRAY_BUFFER, null);
703 gl.uniform4fv(program.colour, [0.0, 0.0, 0.0, this.fadeAmount]);
704 gl.drawArrays(gl.TRIANGLES, 0, 6);
705 gl.disableVertexAttribArray(program.vertexPosition);
706 },
707
708 loadTexture : function(fileName)
709 {
710 var texture = gl.createTexture();
711 gl.bindTexture(gl.TEXTURE_2D, texture);
712 // Fill with grey pixel, as placeholder until loaded
713 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
714 new Uint8Array([128, 128, 128, 255]));
715 // Asynchronously load an image
716 var image = new Image();
717 image.src = fileName;
718 image.addEventListener('load', function()
719 {
720 // Now that the image has loaded copy it to the texture.
721 gl.bindTexture(gl.TEXTURE_2D, texture);
722 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
723 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
724 //gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
725 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
726 gl.generateMipmap(gl.TEXTURE_2D);
727 //hardcoded:
728 texture.width = texture.height = 512;
729 Render.fillBlueChannel(texture);
730 if (controls.grid) Render.drawGrid(texture);
731 });
732 return texture;
733 },
734
735 fillBlueChannel : function(texture)
736 {
737 this.activateTargetTexture(texture);
738 gl.colorMask(false, false, true, true);
739 this.setNormalBlending();
740
741 var program = this.simpleShader;
742 gl.useProgram(program);
743 gl.enableVertexAttribArray(program.vertexPosition);
744 gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
745 gl.bufferData(gl.ARRAY_BUFFER, this.fullScreenQuad, gl.STATIC_DRAW);
746 gl.vertexAttribPointer(program.vertexPosition, 2, gl.FLOAT, false, 0, 0);
747 gl.bindBuffer(gl.ARRAY_BUFFER, null);
748 gl.uniform4fv(program.colour, [0.0, 0.0, 1.0, 1.0]);
749 gl.drawArrays(gl.TRIANGLES, 0, 6);
750 gl.disableVertexAttribArray(program.vertexPosition);
751 gl.colorMask(true, true, true, true);
752 },
753
754 drawGrid : function(texture)
755 {
756 this.activateTargetTexture(texture);
757 this.setNormalBlending();
758 this.setShader(this.simpleShader);
759
760 gl.colorMask(true, false, true, true);
761
762 var data = [];
763
764 for (var i=0; i<11; i++)
765 {
766 var step = 48;
767 var s = i*step;
768 data.splice(0,0, 0, s, 10*step, s);
769 data.splice(0,0, s, 0, s, 10*step);
770 if (i!=0 && i!=10)
771 {
772 for (var j=0; j<51; j++)
773 {
774 t = j*step/5;
775 if (i!=5)
776 {
777 data.splice(0,0, t, s-2, t, s+1);
778 data.splice(0,0, s-2, t, s+1, t);
779 }
780 else
781 {
782 data.splice(0,0, t, s-5, t, s+4);
783 data.splice(0,0, s-5, t, s+4, t);
784 }
785 }
786 }
787 }
788
789 for (var j=0; j<51; j++)
790 {
791 var t = j*step/5;
792 if (t%5 == 0) continue;
793 data.splice(0,0, t-2, 2.5*step, t+2, 2.5*step);
794 data.splice(0,0, t-2, 7.5*step, t+2, 7.5*step);
795 }
796
797
798 var vertices = new Float32Array(data);
799 for (var i=0; i<data.length; i++)
800 {
801 vertices[i]=(vertices[i]+256-step*5)/256-1;
802 }
803
804
805 gl.enableVertexAttribArray(this.program.vertexPosition);
806 gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
807 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
808 gl.vertexAttribPointer(this.program.vertexPosition, 2, gl.FLOAT, false, 0, 0);
809 gl.bindBuffer(gl.ARRAY_BUFFER, null);
810 gl.uniform4fv(this.program.colour, [0.01, 0.1, 0.01, 1.0]);
811
812 gl.lineWidth(1.0);
813 gl.drawArrays(gl.LINES, 0, vertices.length/2);
814
815 gl.bindTexture(gl.TEXTURE_2D, this.targetTexture);
816 gl.generateMipmap(gl.TEXTURE_2D);
817 gl.colorMask(true, true, true, true);
818 },
819
820 makeTexture : function(width, height)
821 {
822 var texture = gl.createTexture();
823 gl.bindTexture(gl.TEXTURE_2D, texture);
824 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.FLOAT, null);
825 //gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, Render.ext.HALF_FLOAT_OES, null);
826 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
827 //gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
828 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
829 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
830 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
831 gl.generateMipmap(gl.TEXTURE_2D);
832 gl.bindTexture(gl.TEXTURE_2D, null);
833 texture.width = width;
834 texture.height = height;
835 return texture;
836 },
837
838 /*xactivateTargetTexture : function(ctx, texture)
839 {
840 gl.bindRenderbuffer(gl.RENDERBUFFER, ctx.renderBuffer);
841 gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, ctx.frameBuffer.width, ctx.frameBuffer.height);
842
843 gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
844 gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, ctx.renderBuffer);
845 assert(GL_FRAMEBUFFER_COMPLETE == glCheckFramebufferStatus(GL_FRAMEBUFFER))
846 gl.bindTexture(gl.TEXTURE_2D, null);
847 gl.bindRenderbuffer(gl.RENDERBUFFER, null);
848 },*/
849
850 drawSimpleLine : function(xSamples, ySamples, colour)
851 {
852 var nVertices = xSamples.length;
853 var vertices = new Float32Array(2*nVertices);
854 for (var i=0; i<nVertices; i++)
855 {
856 vertices[2*i] = xSamples[i];
857 vertices[2*i+1] = ySamples[i];
858 }
859
860 this.setAdditiveBlending();
861
862 var program = this.simpleShader;
863 gl.useProgram(program);
864 gl.enableVertexAttribArray(program.vertexPosition);
865 gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
866 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
867 gl.vertexAttribPointer(program.vertexPosition, 2, gl.FLOAT, false, 0, 0);
868 gl.bindBuffer(gl.ARRAY_BUFFER, null);
869 if (colour=="green") gl.uniform4fv(program.colour, [0.01, 0.1, 0.01, 1.0]);
870 else if (colour == "red") gl.uniform4fv(program.colour, [0.1, 0.01, 0.01, 1.0]);
871
872 gl.lineWidth(3.0);
873 gl.drawArrays(gl.LINE_STRIP, 0, nVertices);
874 },
875
876 setAdditiveBlending : function()
877 {
878 //gl.blendEquation( gl.FUNC_ADD );
879 gl.blendFunc(gl.ONE, gl.ONE);
880 },
881
882 setNormalBlending : function()
883 {
884 //gl.blendEquation( gl.FUNC_ADD );
885 gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
886 },
887
888 createShader : function(vsTag, fsTag)
889 {
890 var vsSource = document.getElementById(vsTag).firstChild.nodeValue;
891 var fsSource = document.getElementById(fsTag).firstChild.nodeValue;
892
893 var vs = gl.createShader(gl.VERTEX_SHADER);
894 gl.shaderSource(vs, vsSource);
895 gl.compileShader(vs);
896 if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS))
897 {
898 var infoLog = gl.getShaderInfoLog(vs);
899 gl.deleteShader(vs);
900 throw new Error('createShader, vertex shader compilation:\n' + infoLog);
901 }
902
903 var fs = gl.createShader(gl.FRAGMENT_SHADER);
904 gl.shaderSource(fs, fsSource);
905 gl.compileShader(fs);
906 if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS))
907 {
908 var infoLog = gl.getShaderInfoLog(fs);
909 gl.deleteShader(vs);
910 gl.deleteShader(fs);
911 throw new Error('createShader, fragment shader compilation:\n' + infoLog);
912 }
913
914 var program = gl.createProgram();
915
916 gl.attachShader(program, vs);
917 gl.deleteShader(vs);
918
919 gl.attachShader(program, fs);
920 gl.deleteShader(fs);
921
922 gl.linkProgram(program);
923
924 if (!gl.getProgramParameter(program, gl.LINK_STATUS))
925 {
926 var infoLog = gl.getProgramInfoLog(program);
927 gl.deleteProgram(program);
928 throw new Error('createShader, linking:\n' + infoLog);
929 }
930
931 return program;
932 },
933
934 supportsWebGl : function()
935 {
936 // from https://github.com/Modernizr/Modernizr/blob/master/feature-detects/webgl.js
937 var canvas = document.createElement('canvas'),
938 supports = 'probablySupportsContext' in canvas ? 'probablySupportsContext' : 'supportsContext';
939 if (supports in canvas)
940 {
941 return canvas[supports]('webgl') || canvas[supports]('experimental-webgl');
942 }
943 return 'WebGLRenderingContext' in window;
944 }
945 }
946
947 var sweepPosition = -1;
948 var belowTrigger = false;
949
950 function doScriptProcessor(event)
951 {
952 var xSamplesRaw = event.inputBuffer.getChannelData(0);
953 var ySamplesRaw = event.inputBuffer.getChannelData(1);
954 var xOut = event.outputBuffer.getChannelData(0);
955 var yOut = event.outputBuffer.getChannelData(1);
956
957 var length = xSamplesRaw.length;
958 for (var i=0; i<length; i++)
959 {
960 xSamples[i] = xSamplesRaw[i];// + (Math.random()-0.5)*controls.noise/2000;
961 ySamples[i] = ySamplesRaw[i];// + (Math.random()-0.5)*controls.noise/2000;
962 }
963
964 if (controls.sweepOn && controls.disableFilter)
965 {
966 var gain = Math.pow(2.0,controls.mainGain);
967 var sweepMinTime = controls.sweepMsDiv*10/1000;
968 var triggerValue = controls.sweepTriggerValue/gain;
969 for (var i=0; i<length; i++)
970 {
971 sweepPosition += 2*AudioSystem.timePerSample/sweepMinTime;
972 if (sweepPosition > 1.1 && belowTrigger && ySamples[i]>=triggerValue)
973 {
974 if (i==0) sweepPosition = -1; //don't bother to calculate
975 else
976 {
977 var delta = (ySamples[i]-triggerValue)/(ySamples[i]-ySamples[i-1]);
978 sweepPosition =-1 + delta*2*AudioSystem.timePerSample/sweepMinTime;
979 }
980 }
981 xSamples[i] = sweepPosition / gain;
982 belowTrigger = ySamples[i]<triggerValue;
983 }
984 }
985
986 if (!controls.freezeImage)
987 {
988 if (!controls.disableFilter)
989 {
990 Filter.generateSmoothedSamples(AudioSystem.oldYSamples, ySamples, AudioSystem.smoothedYSamples);
991 if (!controls.sweepOn) Filter.generateSmoothedSamples(AudioSystem.oldXSamples, xSamples, AudioSystem.smoothedXSamples);
992 else
993 {
994 var xS = AudioSystem.smoothedXSamples;
995 var yS = AudioSystem.smoothedYSamples;
996 var gain = Math.pow(2.0,controls.mainGain);
997 var sweepMinTime = controls.sweepMsDiv*10/1000;
998 var triggerValue = controls.sweepTriggerValue/gain;
999 var smoothedLength = AudioSystem.smoothedYSamples.length;
1000 var timeIncrement = 2*AudioSystem.timePerSample/(sweepMinTime*Filter.steps);
1001 for (var i=0; i<smoothedLength; i++)
1002 {
1003 sweepPosition += timeIncrement;
1004 if (sweepPosition > 1.1 && belowTrigger && yS[i]>=triggerValue)
1005 sweepPosition =-1;
1006 xS[i] = sweepPosition / gain;
1007 belowTrigger = yS[i]<triggerValue;
1008 }
1009 }
1010 if (!controls.swapXY) Render.drawLineTexture(AudioSystem.smoothedXSamples, AudioSystem.smoothedYSamples);
1011 else Render.drawLineTexture(AudioSystem.smoothedYSamples, AudioSystem.smoothedXSamples);
1012 }
1013 else
1014 {
1015 if (!controls.swapXY) Render.drawLineTexture(xSamples, ySamples);
1016 else Render.drawLineTexture(ySamples, xSamples);
1017 }
1018 }
1019
1020 for (var i = 0; i<length; i++)
1021 {
1022 AudioSystem.oldXSamples[i] = xSamples[i];
1023 AudioSystem.oldYSamples[i] = ySamples[i];
1024 xOut[i] = xSamplesRaw[i];
1025 yOut[i] = ySamplesRaw[i];
1026 }
1027
1028 AudioSystem.audioVolumeNode.gain.value = controls.audioVolume;
1029 }
1030
1031 function drawCRTFrame(timeStamp)
1032 {
1033 Render.drawCRT();
1034 requestAnimationFrame(drawCRTFrame);
1035 }
1036
1037 Filter.init(512, 8, 6);
1038 AudioSystem.init(512);
1039 var xSamples = new Float32Array(512);
1040 var ySamples = new Float32Array(512);
1041 //Filter.init(1024, 8, 6);
1042 //AudioSystem.init(1024);
1043 //var xSamples = new Float32Array(1024);
1044 //var ySamples = new Float32Array(1024);
1045 UI.init();
1046 Render.init();
1047 if (Render.failed)
1048 {
1049 Render.admitFailure();
1050 }
1051 else
1052 {
1053 Render.setupArrays(Filter.nSmoothedSamples);
1054 AudioSystem.startSound();
1055 requestAnimationFrame(drawCRTFrame);
1056 Controls.setupControls();
1057 }