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