├── README.md ├── deps ├── MathBox-bundle.min.js ├── MathBox.glsl.html └── README.txt ├── dsp.js ├── presentation-main.js └── presentation.html /README.md: -------------------------------------------------------------------------------- 1 | A Visual Introduction to DSP for SDR 2 | ==================================== 3 | 4 | This is an animated slide deck providing a tour of digital signal processing topics relevant to implementation of software-defined radios, focusing on building visual/geometric intuition for signals. 5 | 6 | Topics covered: 7 | 8 | * Complex (IQ) and analytic signals. 9 | * Filtering (FIR and IIR). 10 | * Frequency shifting. 11 | * Sampling rates and the Nyquist limit. 12 | * The discrete Fourier transform (DFT) and fast Fourier transform (FFT). 13 | * Digital modulation (OOK, PSK, QPSK, QAM). 14 | 15 | History 16 | ------- 17 | 18 | I originally wrote down the idea for this presentation as follows: 19 | 20 | > sdr tutorial idea: 21 | > starting from interactive slide deck w/ live spiral-graph display 22 | > transform it in multiple ways 23 | > a fft corresponds to successive amounts of untwist + sum (does it?) 24 | > Use the MathBox WebGL framework 25 | 26 | That sat around for a while until Balint Seeber organized a meetup group for SDR local to me, in November 2014, and I saw an excuse to do this thing — and here it is, exactly as originally conceived. 27 | 28 | [Here's the recording of that meetup with me giving this presentation](https://www.youtube.com/watch?v=DUGr_Z04SKs&t=12m30s). Note that this version does not include the material on digital modulations. 29 | 30 | License 31 | ------- 32 | 33 | All source code and other materials, excluding the contents of the `deps/` directory which is third-party code used under license, are Copyright © 2014, 2015, 2018 Kevin Reid, and licensed as follows (the “MIT License”): 34 | 35 | > Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 36 | > 37 | > The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 38 | > 39 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 40 | -------------------------------------------------------------------------------- /deps/MathBox.glsl.html: -------------------------------------------------------------------------------- 1 | 51 | 52 | 65 | 66 | 86 | 87 | 108 | 109 | 118 | 119 | 130 | 131 | 150 | 151 | 160 | 161 | 171 | 172 | 181 | 182 | 191 | 192 | 201 | 202 | 224 | 225 | 249 | 250 | 263 | 264 | 280 | 281 | 302 | 303 | 309 | 310 | 321 | 322 | 339 | 340 | 396 | 397 | 419 | 420 | 435 | 436 | 445 | 446 | 447 | 458 | 459 | 460 | 474 | 475 | 480 | 481 | 489 | 490 | 491 | 507 | 508 | -------------------------------------------------------------------------------- /deps/README.txt: -------------------------------------------------------------------------------- 1 | MathBox* files are from the MathBox.js repository at 2 | at revision 3 | commit dd802725b51260dcb361c65387afbb2a1e99b3ea 4 | Date: Tue Sep 9 22:28:08 2014 5 | They are distributed under the following license: 6 | 7 | The MIT License 8 | 9 | Copyright (c) 2012 Steven Wittens. All rights reserved. 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. -------------------------------------------------------------------------------- /dsp.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2014, 2015 Kevin Reid 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | */ 10 | var VisualDSP_DSP = (function () { 11 | 'use strict'; 12 | 13 | var sin = Math.sin; 14 | var cos = Math.cos; 15 | var PI = Math.PI; 16 | var TWOPI = Math.PI * 2; 17 | 18 | var blocks = {}; 19 | var exports = {}; 20 | 21 | function outputArray(instance) { 22 | return instance instanceof Float32Array ? instance : instance.output; 23 | } 24 | 25 | function ToComplex(audioinb) { 26 | var audioin = outputArray(audioinb); 27 | var limit = audioin.length; 28 | var iqout = new Float32Array(limit * 2); 29 | return { 30 | inputs: [audioinb], 31 | output: iqout, 32 | run: function toComplex() { 33 | for (var i = 0, j = 0; i < limit; i++, j += 2) { 34 | iqout[j] = audioin[i]; 35 | iqout[j+1] = 0; 36 | } 37 | }, 38 | }; 39 | } 40 | blocks.ToComplex = ToComplex; 41 | 42 | function AMModulator(audioinb) { 43 | var audioin = outputArray(audioinb); 44 | var limit = audioin.length; 45 | var iqout = new Float32Array(limit * 2); 46 | return { 47 | inputs: [audioinb], 48 | output: iqout, 49 | run: function amModulator() { 50 | for (var i = 0, j = 0; i < limit; i++, j += 2) { 51 | iqout[j] = 1 + audioin[i]; 52 | iqout[j+1] = 0; 53 | } 54 | } 55 | }; 56 | } 57 | blocks.AMModulator = AMModulator; 58 | 59 | function FMModulator(audioinb, deviation) { 60 | var audioin = outputArray(audioinb); 61 | var limit = audioin.length; 62 | var iqout = new Float32Array(limit * 2); 63 | return { 64 | inputs: [audioinb], 65 | output: iqout, 66 | run: function fmModulator() { 67 | var phase = 0; 68 | for (var i = limit/2, j = limit; i < limit; i++, j += 2) { 69 | phase += audioin[i] * deviation; 70 | iqout[j] = cos(phase); 71 | iqout[j+1] = sin(phase); 72 | } 73 | phase = 0; 74 | for (var i = limit/2 - 1, j = limit - 2; i >= 0; i--, j -= 2) { 75 | phase -= audioin[i] * deviation; 76 | iqout[j] = cos(phase); 77 | iqout[j+1] = sin(phase); 78 | } 79 | } 80 | }; 81 | } 82 | blocks.FMModulator = FMModulator; 83 | 84 | // TODO broken 85 | // step = 1 for real, 2 for complex. 86 | // delay: number of samples to delay input by 87 | // taps: always real 88 | function FIRFilter(inb, step, delay, taps) { 89 | var inarr = outputArray(inb); 90 | var ntaps = taps.length; 91 | var valdelay = delay * step; // delay in array-index units 92 | 93 | // Range of nonzero output indices if delay = 0 94 | var undelayedStart = 0; 95 | var undelayedEnd = inarr.length - (ntaps - 1) * step; 96 | 97 | var outLength = inarr.length; // a choice 98 | var out = new Float32Array(outLength); 99 | 100 | var delayedStart = Math.max(0, Math.min(outLength, undelayedStart - valdelay)); 101 | var delayedEnd = Math.max(0, Math.min(outLength, undelayedEnd - valdelay)); 102 | 103 | //console.log('FIR: 0 .. %d -- %d .. %d | %d in, %d taps', delayedStart, delayedEnd, outLength, inarr.length, ntaps); 104 | 105 | return { 106 | inputs: [inb], 107 | output: out, 108 | run: function filterer() { 109 | var i = 0; 110 | for (; i < delayedStart; i++) { 111 | out[i] = NaN; 112 | } 113 | for (; i < delayedEnd; i++) { 114 | var accum = 0; 115 | for (var j = 0; j < ntaps * step; j += step) { 116 | accum += inarr[i + valdelay + j] * taps[Math.floor(j / step)]; 117 | } 118 | out[i] = accum; 119 | } 120 | for (; i < outLength; i++) { 121 | out[i] = NaN; 122 | } 123 | } 124 | }; 125 | } 126 | blocks.FIRFilter = FIRFilter; 127 | 128 | // real or complex 129 | function Add(in1b, in2b) { 130 | var in1 = outputArray(in1b); 131 | var in2 = outputArray(in2b); 132 | var limit = Math.min(in1.length, in2.length); 133 | var out = new Float32Array(limit); 134 | return { 135 | inputs: [in1b, in2b], 136 | output: out, 137 | run: function adder() { 138 | for (var i = 0; i < limit; i += 1) { 139 | out[i] = in1[i] + in2[i]; 140 | } 141 | } 142 | }; 143 | } 144 | blocks.Add = Add; 145 | 146 | function Multiply(iqin1b, iqin2b) { 147 | var iqin1 = outputArray(iqin1b); 148 | var iqin2 = outputArray(iqin2b); 149 | var limit = Math.min(iqin1.length, iqin2.length); 150 | var iqout = new Float32Array(limit); 151 | return { 152 | inputs: [iqin1b, iqin2b], 153 | output: iqout, 154 | run: function multiply() { 155 | for (var i = 0; i < limit; i += 2) { 156 | iqout[i] = iqin1[i] * iqin2[i] - iqin1[i+1] * iqin2[i+1]; 157 | iqout[i+1] = iqin1[i+1] * iqin2[i] + iqin1[i] * iqin2[i+1]; 158 | } 159 | } 160 | }; 161 | } 162 | blocks.Multiply = Multiply; 163 | 164 | function Rotator(iqinb, radiansPerSample) { 165 | var iqin = outputArray(iqinb); 166 | var limit = iqin.length; 167 | var iqout = new Float32Array(limit); 168 | return { 169 | inputs: [iqinb], 170 | output: iqout, 171 | run: function rotator() { 172 | var phase = 0; 173 | for (var i = 0; i < limit; i += 2) { 174 | var s = sin(phase); 175 | var c = cos(phase); 176 | iqout[i] = c * iqin[i] - s * iqin[i+1]; 177 | iqout[i+1] = s * iqin[i] + c * iqin[i+1]; 178 | phase += radiansPerSample; 179 | } 180 | //phase = phase % TWOPI; 181 | } 182 | }; 183 | } 184 | blocks.Rotator = Rotator; 185 | 186 | function Siggen(samples, radiansPerSampleFn) { 187 | var limit = samples * 2; 188 | var iqout = new Float32Array(limit); 189 | return { 190 | inputs: [], 191 | output: iqout, 192 | run: function siggen() { 193 | var phase = 0; 194 | var radiansPerSample = radiansPerSampleFn(); 195 | for (var i = 0; i < limit; i += 2) { 196 | var c = cos(phase); 197 | iqout[i] = cos(phase); 198 | iqout[i+1] = sin(phase); 199 | phase += radiansPerSample; 200 | } 201 | } 202 | }; 203 | } 204 | blocks.Siggen = Siggen; 205 | 206 | function ArraySource(array, opt_func) { 207 | return { 208 | inputs: [], 209 | output: array, 210 | run: opt_func || function arraySourceNoop() {} 211 | }; 212 | } 213 | blocks.ArraySource = ArraySource; 214 | 215 | function LinearInterpolator(iqinb, interpolation) { 216 | var iqin = outputArray(iqinb); 217 | interpolation = Math.floor(interpolation); 218 | var iqout = new Float32Array(iqin.length * interpolation); 219 | var limit = iqout.length; 220 | return { 221 | inputs: [iqinb], 222 | output: iqout, 223 | run: function linearInterpolator() { 224 | for (var j = 0; j < limit; j += 2) { 225 | var position = j / (interpolation*2); 226 | var index = Math.floor(position); 227 | var fraction = position - index; 228 | var complement = 1 - fraction; 229 | var i = index * 2; 230 | iqout[j] = iqin[i] * complement + iqin[i+2] * fraction; 231 | iqout[j+1] = iqin[i + 1] * complement + iqin[i+3] * fraction; 232 | } 233 | } 234 | }; 235 | } 236 | blocks.LinearInterpolator = LinearInterpolator; 237 | 238 | function ImpulseInterpolator(iqinb, interpolation) { 239 | var iqin = outputArray(iqinb); 240 | interpolation = Math.floor(interpolation); 241 | var half = Math.floor(interpolation / 4) * 2; 242 | var iqout = new Float32Array(iqin.length * interpolation); 243 | var inlimit = iqin.length; 244 | var outlimit = iqout.length; 245 | return { 246 | inputs: [iqinb], 247 | output: iqout, 248 | run: function impulseInterpolator() { 249 | var j; 250 | for (j = 0; j < outlimit; j += 1) { 251 | iqout[j] = 0; 252 | } 253 | for (var i = 0; i < inlimit; i += 2) { 254 | j = half + i * interpolation; 255 | iqout[j] = iqin[i]; 256 | iqout[j + 1] = iqin[i + 1]; 257 | } 258 | } 259 | }; 260 | } 261 | blocks.ImpulseInterpolator = ImpulseInterpolator; 262 | 263 | function RepeatInterpolator(iqinb, interpolation) { 264 | var iqin = outputArray(iqinb); 265 | interpolation = Math.floor(interpolation); 266 | var iqout = new Float32Array(iqin.length * interpolation); 267 | var limit = iqin.length; 268 | return { 269 | inputs: [iqinb], 270 | output: iqout, 271 | run: function repeatInterpolator() { 272 | for (var i = 0; i < limit; i += 2) { 273 | var jlim = (i + 2) * interpolation; 274 | for (var j = i * interpolation; j < jlim; j += 2) { 275 | iqout[j] = iqin[i]; 276 | iqout[j + 1] = iqin[i + 1]; 277 | } 278 | } 279 | } 280 | }; 281 | } 282 | blocks.RepeatInterpolator = RepeatInterpolator; 283 | 284 | function Mapper(inputb, map) { 285 | var input = outputArray(inputb); 286 | var output = new Float32Array(input.length); 287 | var limit = input.length; 288 | return { 289 | inputs: [inputb], 290 | output: output, 291 | run: function mapper() { 292 | for (var i = 0; i < limit; i++) { 293 | output[i] = map[input[i]]; 294 | } 295 | } 296 | }; 297 | } 298 | blocks.Mapper = Mapper; 299 | 300 | // TODO I forget what the proper name for this is 301 | function SymbolModulator(inputb, gain, array) { 302 | array = array.map(function (s) { return [s[0] * gain, s[1] * gain]; }); 303 | var nbits = Math.round(Math.log2(array.length)); 304 | var input = outputArray(inputb); 305 | var limit = Math.round(input.length / nbits); 306 | var output = new Float32Array(limit * 2); 307 | console.log(array.length, nbits, limit); 308 | return { 309 | inputs: [inputb], 310 | output: output, 311 | run: function mapper() { 312 | for (var i = 0; i < limit; i++) { 313 | var code = 0; 314 | for (var j = 0; j < nbits; j++) { 315 | code = (code << 1) + input[i * nbits + j]; 316 | } 317 | var symbol = array[code]; 318 | output[i * 2] = symbol[0]; 319 | output[i * 2 + 1] = symbol[1]; 320 | } 321 | } 322 | }; 323 | } 324 | blocks.SymbolModulator = SymbolModulator; 325 | 326 | exports.blocks = Object.freeze(blocks); 327 | 328 | function Graph(blocks) { 329 | blocks = topologicalSortAndExtend(blocks); 330 | var fns = blocks.map(function (block) { return block.run; }); 331 | var limit = fns.length; 332 | return function graph() { 333 | for (var i = 0; i < limit; i++) { 334 | fns[i](); 335 | } 336 | }; 337 | } 338 | exports.Graph = Graph; 339 | 340 | function topologicalSortAndExtend(startingBlocks) { 341 | var output = []; // blocks in output order 342 | 343 | // parallel arrays 344 | //var arrays = []; 345 | var blocks = []; 346 | var records = []; 347 | 348 | // generate intermediate data structure 349 | var idGen = 0; 350 | function lookup(block) { 351 | var id = blocks.indexOf(block); 352 | if (id >= 0) { 353 | return records[id]; 354 | } 355 | 356 | if (!block.inputs) { 357 | throw new Error('alleged block missing inputs property: ' + block); 358 | } 359 | if (!block.run) { 360 | throw new Error('alleged block missing run method: ' + block); 361 | } 362 | 363 | id = records.length; 364 | var record = { 365 | id: id, 366 | block: block, 367 | visiting: false, 368 | visited: false 369 | }; 370 | blocks[id] = block; 371 | records[id] = record; 372 | return record; 373 | } 374 | 375 | function visit(block) { 376 | var record = lookup(block); 377 | if (record.visited) return; 378 | if (record.visiting) { 379 | throw new Error('cyclic graph'); // TODO give details 380 | } 381 | record.visiting = true; 382 | 383 | var inIds = []; 384 | record.block.inputs.forEach(function (inputBlock) { 385 | visit(inputBlock); 386 | inIds.push(lookup(inputBlock).id); 387 | }); 388 | output.push(record.block); 389 | //console.log(record.id, record.block.run.name, inIds); // debug 390 | 391 | record.visited = true; 392 | } 393 | 394 | startingBlocks.forEach(visit); 395 | return output; 396 | } 397 | 398 | return Object.freeze(exports); 399 | })(); -------------------------------------------------------------------------------- /presentation-main.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2014, 2015 Kevin Reid 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | */ 10 | (function () { 11 | 'use strict'; 12 | 13 | var DSP = VisualDSP_DSP; 14 | 15 | var sin = Math.sin; 16 | var cos = Math.cos; 17 | var PI = Math.PI; 18 | var TWOPI = Math.PI * 2; 19 | 20 | function reportFailure(error) { 21 | var d = document.createElement('dialog'); 22 | d.textContent = String(error); 23 | d.className = 'dialog-on-top'; 24 | 25 | var b = d.appendChild(document.createElement('button')); 26 | b.textContent = 'OK'; 27 | b.addEventListener('click', function (event) { 28 | d.parentNode.removeChild(d); 29 | }, false); 30 | 31 | document.body.appendChild(d); 32 | if (d.show) { // supported 33 | d.show(); 34 | } else { 35 | // nothing needed, will be auto-visible 36 | } 37 | } 38 | 39 | var AudioContext = window.AudioContext || window.webkitAudioContext; 40 | if (!AudioContext) { 41 | reportFailure('This browser does not support Web Audio API.'); 42 | return; 43 | } 44 | 45 | var ctx = new AudioContext(); 46 | var sampleRate = ctx.sampleRate; 47 | var fftnode = ctx.createAnalyser(); 48 | fftnode.smoothingTimeConstant = 0; 49 | fftnode.fftSize = 2048; 50 | // ignore mostly useless high freq bins 51 | var binCount = fftnode.frequencyBinCount / 2; 52 | var sampleCount = 128; // can be up to fftSize but we want to 'zoom in' 53 | 54 | var fftarray = new Float32Array(binCount); 55 | var audioarray = new Float32Array(sampleCount); 56 | 57 | var userGainNode = ctx.createGain(); 58 | var userGainEl = document.getElementById('gain'); 59 | function updateGain() { 60 | userGainNode.gain.value = Math.pow(10, userGainEl.valueAsNumber / 10); 61 | } 62 | updateGain(); 63 | userGainEl.addEventListener('change', updateGain, false); 64 | userGainEl.addEventListener('input', updateGain, false); 65 | 66 | var sources = Object.create(null); 67 | var sourceSelectEl = document.getElementById('source-select'); 68 | sources['sig'] = (function() { 69 | var osc1 = ctx.createOscillator(); 70 | osc1.frequency.value = sampleRate / 30; 71 | osc1.start(); 72 | var gain1 = ctx.createGain(); 73 | gain1.gain.value = 0.5; 74 | var osc2 = ctx.createOscillator(); 75 | osc2.frequency.value = osc1.frequency.value * 5; 76 | osc2.start(); 77 | var gain2 = ctx.createGain(); 78 | gain2.gain.value = 0.5; 79 | osc1.connect(gain1); 80 | osc2.connect(gain2); 81 | gain2.connect(gain1); 82 | return gain1; 83 | }()); 84 | var currentSource = null; 85 | function wireSource() { 86 | if (currentSource !== null) { 87 | currentSource.disconnect(userGainNode); 88 | } 89 | currentSource = sources[sourceSelectEl.value] || null; 90 | if (currentSource === null && sourceSelectEl.value == 'user') { 91 | currentSource = sources['sig']; 92 | } 93 | console.log('switching to', sourceSelectEl.value, currentSource); 94 | if (currentSource !== null) { 95 | currentSource.connect(userGainNode); 96 | } 97 | } 98 | wireSource(); 99 | sourceSelectEl.addEventListener('change', wireSource, false); 100 | 101 | userGainNode.connect(fftnode); 102 | 103 | var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; 104 | if (!getUserMedia) { 105 | reportFailure('This browser does not support getUserMedia. No signal will be shown.'); 106 | // don't abort, we can at least show the slides 107 | } else { 108 | getUserMedia.call(navigator, {audio: true}, function getUserMediaSuccess(stream) { 109 | var source = ctx.createMediaStreamSource(stream); 110 | sources['user'] = source; 111 | wireSource(); 112 | 113 | // https://bugzilla.mozilla.org/show_bug.cgi?id=934512 114 | // https://stackoverflow.com/q/22860468/99692 115 | // Firefox destroys the media stream source even though it is in use by the audio graph. As a workaround, make a powerless global reference to it. 116 | // TODO: moot now 117 | window[Math.random()] = function() { console.log(source); } 118 | }, function (error) { 119 | console.error('error response from getUserMedia:', error); 120 | var option = sourceSelectEl.querySelector('option[value="user"]'); 121 | option.textContent += ' (unavailable)'; 122 | option.disabled = true; 123 | if (sourceSelectEl.value === 'user') sourceSelectEl.value = 'sig'; 124 | }); 125 | } 126 | 127 | var filterOuterCoeff = 1/3; 128 | var filterInnerCoeff = 2/3; 129 | 130 | // firdes.low_pass(1, 44100, 1000, 2000) 131 | var audio_lowpass = [-0.00114539940841496, -0.0007444394868798554, 2.997766569023952e-05, 0.0019656415097415447, 0.005893126595765352, 0.01247603353112936, 0.02201135642826557, 0.034287191927433014, 0.04853496327996254, 0.06349427998065948, 0.07758451253175735, 0.0891534835100174, 0.09675595909357071, 0.09940661489963531, 0.09675595909357071, 0.0891534835100174, 0.07758451253175735, 0.06349427998065948, 0.04853496327996254, 0.034287191927433014, 0.02201135642826557, 0.01247603353112936, 0.005893126595765352, 0.0019656415097415447, 2.997766569023952e-05, -0.0007444394868798554, -0.00114539940841496]; 132 | var audio_highpass = [0.0010463938815519214, 0.0006800920236855745, -2.738647162914276e-05, -0.0017957363743335009, -0.005383739247918129, -0.011397636495530605, -0.020108748227357864, -0.03132349252700806, -0.04433972015976906, -0.05800599604845047, -0.07087830454111099, -0.08144728094339371, -0.08839261531829834, 0.9104118347167969, -0.08839261531829834, -0.08144728094339371, -0.07087830454111099, -0.05800599604845047, -0.04433972015976906, -0.03132349252700806, -0.020108748227357864, -0.011397636495530605, -0.005383739247918129, -0.0017957363743335009, -2.738647162914276e-05, 0.0006800920236855745, 0.0010463938815519214]; 133 | 134 | var interpolation = 5; 135 | var chfreq = 0.30; 136 | 137 | var audioin = DSP.blocks.ArraySource(audioarray); 138 | var modulatingam = DSP.blocks.AMModulator(audioin); 139 | var modulatingfm = DSP.blocks.FMModulator(audioin, 0.75); 140 | var dsbbuf = DSP.blocks.ToComplex(audioin); 141 | var hfambuf = DSP.blocks.LinearInterpolator(modulatingam, interpolation); 142 | var hffmbuf = DSP.blocks.LinearInterpolator(modulatingfm, interpolation); 143 | var amout = DSP.blocks.Rotator(hfambuf, chfreq); 144 | var fmout = DSP.blocks.Rotator(hffmbuf, chfreq); 145 | var demodrot = DSP.blocks.Siggen(interpolation * sampleCount, function() { return (mbdirector && mbdirector.step == demodStep ? Math.min(mbdirector.clock(demodStep) * 0.08, 1) : 0) * -chfreq; }); 146 | var product = DSP.blocks.Multiply(fmout, demodrot); 147 | var audioh = DSP.blocks.FIRFilter(dsbbuf, 2, -Math.floor(audio_lowpass.length / 2), audio_highpass); 148 | var audiol = DSP.blocks.FIRFilter(dsbbuf, 2, -Math.floor(audio_lowpass.length / 2), audio_lowpass); 149 | 150 | var g = DSP.Graph([ 151 | modulatingam, 152 | modulatingfm, 153 | dsbbuf, 154 | amout, 155 | fmout, 156 | product, 157 | audioh, 158 | audiol, 159 | ]); 160 | 161 | //var digdata = new Float32Array([0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1]); 162 | var digdata = new Float32Array(32); 163 | var diginterp = 40; 164 | var digsamples = digdata.length * diginterp; 165 | var digchfreq = 0.30; 166 | var digUpdateInterval = 1000; 167 | 168 | // firdes.root_raised_cosine(gain=40.0, sampling_freq=40, symbol_rate=1, alpha=0.25, ntaps=200) 169 | var dig_rrc_taps = [0.06413525342941284, 0.06912656128406525, 0.07376259565353394, 0.07799314707517624, 0.08176935464143753, 0.08504420518875122, 0.08777286857366562, 0.08991317451000214, 0.09142597764730453, 0.09227557480335236, 0.09243007004261017, 0.09186173975467682, 0.09054736793041229, 0.08846860378980637, 0.0856122076511383, 0.08197031915187836, 0.07754074782133102, 0.07232708483934402, 0.06633895635604858, 0.059592101722955704, 0.05210845172405243, 0.043916214257478714, 0.03504985198378563, 0.025550054386258125, 0.015463657677173615, 0.004843507893383503, -0.006251695565879345, -0.01775762066245079, -0.029604556038975716, -0.04171771556138992, -0.054017573595047, -0.06642024964094162, -0.07883792370557785, -0.09117928892374039, -0.10335005819797516, -0.11525343358516693, -0.12679073214530945, -0.13786186277866364, -0.1483660340309143, -0.15820223093032837, -0.16726994514465332, -0.1754697859287262, -0.18270409107208252, -0.18887759745121002, -0.19389811158180237, -0.19767707586288452, -0.20013023912906647, -0.20117828249931335, -0.20074740052223206, -0.19876980781555176, -0.19518440961837769, -0.18993720412254333, -0.18298178911209106, -0.1742798388004303, -0.16380146145820618, -0.1515255719423294, -0.13744015991687775, -0.12154260277748108, -0.10383985191583633, -0.08434852957725525, -0.0630950927734375, -0.0401158332824707, -0.015456845983862877, 0.010826030746102333, 0.03866736963391304, 0.06799235194921494, 0.09871701151132584, 0.13074859976768494, 0.16398584842681885, 0.19831956923007965, 0.2336329221725464, 0.26980212330818176, 0.30669692158699036, 0.34418120980262756, 0.3821137547492981, 0.4203488230705261, 0.45873698592185974, 0.4971257448196411, 0.5353603959083557, 0.573284924030304, 0.6107425689697266, 0.6475769877433777, 0.6836327314376831, 0.7187562584877014, 0.7527968287467957, 0.7856071591377258, 0.8170443177223206, 0.8469705581665039, 0.8752537369728088, 0.9017686247825623, 0.926396906375885, 0.949028730392456, 0.9695621728897095, 0.987904965877533, 1.0039739608764648, 1.017696499824524, 1.02901029586792, 1.037863850593567, 1.0442167520523071, 1.0480402708053589, 1.049316644668579, 1.0480402708053589, 1.0442167520523071, 1.037863850593567, 1.02901029586792, 1.017696499824524, 1.0039739608764648, 0.987904965877533, 0.9695621728897095, 0.949028730392456, 0.926396906375885, 0.9017686247825623, 0.8752537369728088, 0.8469705581665039, 0.8170443177223206, 0.7856071591377258, 0.7527968287467957, 0.7187562584877014, 0.6836327314376831, 0.6475769877433777, 0.6107425689697266, 0.573284924030304, 0.5353603959083557, 0.4971257448196411, 0.45873698592185974, 0.4203488230705261, 0.3821137547492981, 0.34418120980262756, 0.30669692158699036, 0.26980212330818176, 0.2336329221725464, 0.19831956923007965, 0.16398584842681885, 0.13074859976768494, 0.09871701151132584, 0.06799235194921494, 0.03866736963391304, 0.010826030746102333, -0.015456845983862877, -0.0401158332824707, -0.0630950927734375, -0.08434852957725525, -0.10383985191583633, -0.12154260277748108, -0.13744015991687775, -0.1515255719423294, -0.16380146145820618, -0.1742798388004303, -0.18298178911209106, -0.18993720412254333, -0.19518440961837769, -0.19876980781555176, -0.20074740052223206, -0.20117828249931335, -0.20013023912906647, -0.19767707586288452, -0.19389811158180237, -0.18887759745121002, -0.18270409107208252, -0.1754697859287262, -0.16726994514465332, -0.15820223093032837, -0.1483660340309143, -0.13786186277866364, -0.12679073214530945, -0.11525343358516693, -0.10335005819797516, -0.09117928892374039, -0.07883792370557785, -0.06642024964094162, -0.054017573595047, -0.04171771556138992, -0.029604556038975716, -0.01775762066245079, -0.006251695565879345, 0.004843507893383503, 0.015463657677173615, 0.025550054386258125, 0.03504985198378563, 0.043916214257478714, 0.05210845172405243, 0.059592101722955704, 0.06633895635604858, 0.07232708483934402, 0.07754074782133102, 0.08197031915187836, 0.0856122076511383, 0.08846860378980637, 0.09054736793041229, 0.09186173975467682, 0.09243007004261017, 0.09227557480335236, 0.09142597764730453, 0.08991317451000214, 0.08777286857366562, 0.08504420518875122, 0.08176935464143753, 0.07799314707517624, 0.07376259565353394, 0.06912656128406525, 0.06413525342941284]; 170 | function pshap(input) { 171 | return DSP.blocks.FIRFilter(DSP.blocks.ImpulseInterpolator(input, diginterp), 2, -Math.floor(dig_rrc_taps.length / 2) - 10 /* fudge factor */, dig_rrc_taps); 172 | } 173 | 174 | var digin = DSP.blocks.ArraySource(digdata); 175 | var digbaseband = DSP.blocks.ToComplex(digin); 176 | var dighold = DSP.blocks.RepeatInterpolator(digbaseband, diginterp); 177 | var digshap = pshap(digbaseband); 178 | var digshapook = DSP.blocks.Rotator(digshap, digchfreq); 179 | var digook = DSP.blocks.Rotator(dighold, digchfreq); 180 | var digpn = DSP.blocks.Mapper(digin, [-1, 1]); 181 | var digpnbase = DSP.blocks.ToComplex(digpn); 182 | var digpnhold = DSP.blocks.RepeatInterpolator(digpnbase, diginterp); 183 | var digpnshap = pshap(digpnbase); 184 | var digpnkey = DSP.blocks.Rotator(digpnshap, digchfreq); 185 | var qpskconst = [[-1, -1], [-1, 1], [1, 1], [1, -1]]; 186 | var digqpskbase = DSP.blocks.SymbolModulator(digin, 1/Math.sqrt(2), qpskconst); 187 | var qamconst = (function() { 188 | var out = []; 189 | for (var i = 0; i < 4; i++) { 190 | for (var q = 0; q < 4; q++) { 191 | out.push([i - 1.5, q - 1.5]); 192 | } 193 | } 194 | console.log(JSON.stringify(out)); 195 | return out; 196 | })(); 197 | var digqambase = DSP.blocks.SymbolModulator(digin, 1/Math.sqrt(2), qamconst); 198 | var digqamshap = pshap(digqambase); 199 | var digqamkey = DSP.blocks.Rotator(digqamshap, digchfreq); 200 | 201 | var diggraph = DSP.Graph([ 202 | dighold, 203 | digook, 204 | digshap, 205 | digshapook, 206 | digpnhold, 207 | digpnshap, 208 | digpnkey, 209 | digpnbase, 210 | digqpskbase, 211 | digqambase, 212 | digqamshap, 213 | digqamkey 214 | ]); 215 | diggraph(); 216 | 217 | var twosig1 = DSP.blocks.Siggen(sampleCount, function() { return 0.3; }); 218 | var twosig2 = DSP.blocks.Siggen(sampleCount, function() { return 10; }); 219 | var twosig = DSP.blocks.Add(twosig1, twosig2); 220 | var twosigl = DSP.blocks.FIRFilter(twosig, 2, -1, [filterOuterCoeff, filterInnerCoeff, filterOuterCoeff]); // 2 for complex 221 | var twosigh = DSP.blocks.FIRFilter(twosig, 2, -1, [-filterOuterCoeff, filterInnerCoeff, -filterOuterCoeff]); // 2 for complex 222 | DSP.Graph([ 223 | twosig, 224 | twosigl, 225 | twosigh, 226 | ])(); 227 | 228 | var mbdirector, demodStep = 6; 229 | ThreeBox.preload(['deps/MathBox.glsl.html'], goMathbox); 230 | function goMathbox() { 231 | var element = document.getElementById('mb'); 232 | var mathbox = mathBox(element, { 233 | stats: false, // (disable) FPS meter in upper left corner 234 | cameraControls: true, 235 | controlClass: ThreeBox.OrbitControls, 236 | camera: new THREE.OrthographicCamera(-1, 1, 1, -1, 0.01, 10000), 237 | scale: 1 238 | }).start(); 239 | 240 | var timeRangeScale = 6; 241 | 242 | var vs = 0.4; 243 | var almost_zero_theta = 0.05; 244 | mathbox.viewport({ 245 | type: 'cartesian', 246 | range: [[-2, 2], [-2, 2], [-timeRangeScale, timeRangeScale]], 247 | scale: [-1*vs, 1*vs, timeRangeScale*vs] 248 | }); 249 | mathbox.camera({ 250 | orbit: 6, 251 | phi: PI, 252 | theta: almost_zero_theta 253 | }); 254 | mathbox.transition(500); 255 | 256 | function axisi(v) { return v == 2 ? 'I' : ''; } 257 | function axisq(v) { return v == 2 ? 'Q' : ''; } 258 | var iaxisdef = { 259 | id: 'iaxis', 260 | axis: 1, 261 | color: 0x777777, 262 | ticks: 3, 263 | lineWidth: 2, 264 | size: .05, 265 | arrow: false, 266 | labels: false, 267 | formatter: axisi 268 | }; 269 | var qaxisdef = { 270 | id: 'qaxis', 271 | axis: 0, 272 | color: 0x777777, 273 | ticks: 3, 274 | lineWidth: 2, 275 | size: .05, 276 | arrow: false, 277 | labels: false, 278 | formatter: axisq 279 | }; 280 | mathbox.axis(iaxisdef); 281 | mathbox.axis(qaxisdef); 282 | mathbox.axis({ 283 | id: 'taxis', 284 | axis: 2, 285 | color: 0x777777, 286 | ticks: 3, 287 | lineWidth: 2, 288 | size: .05, 289 | arrow: true 290 | }); 291 | 292 | // wave 293 | function docurve(id, color, block1, block2) { 294 | var array1 = block1.output; 295 | var array2 = block2 ? block2.output : undefined; 296 | var outbuf = [0, 0, 0]; 297 | return { 298 | id: id, 299 | color: color, 300 | n: array1.length / 2, 301 | live: true, 302 | domain: [-timeRangeScale, timeRangeScale], 303 | expression: function (x, i) { 304 | var k = this.get('ksiginterp'); 305 | if (k > 0) { 306 | outbuf[0] = array1[i * 2 + 1] * (1-k) + array2[i * 2 + 1] * k 307 | outbuf[1] = array1[i * 2] * (1-k) + array2[i * 2] * k 308 | outbuf[2] = x; 309 | } else { 310 | outbuf[0] = array1[i * 2 + 1] 311 | outbuf[1] = array1[i * 2] 312 | outbuf[2] = x; 313 | } 314 | return outbuf; 315 | }, 316 | lineWidth: 2, 317 | ksiginterp: 0, 318 | } 319 | } 320 | function dopoints(id, color, block) { 321 | var record = docurve(id, color, block); 322 | record.points = true; 323 | record.line = false; 324 | record.pointSize = 8; 325 | return record; 326 | } 327 | function dountwist(id, color, radiansPerSample, block) { 328 | var array = block.output; 329 | var outbuf = [0, 0, 0]; 330 | return { 331 | id: id, 332 | color: color, 333 | n: array.length / 2, 334 | live: true, 335 | domain: [-timeRangeScale, timeRangeScale], 336 | expression: function (x, i) { 337 | var vi = array[i * 2]; 338 | var vq = array[i * 2 + 1]; 339 | var phase = this.get('kphase') + i * this.get('kfreq'); 340 | var s = sin(phase); 341 | var c = cos(phase); 342 | var scale = 1; 343 | outbuf[0] = scale * (s * vi + c * vq); 344 | outbuf[1] = scale * (c * vi - s * vq); 345 | outbuf[2] = x; 346 | return outbuf; 347 | }, 348 | lineWidth: 2, 349 | kfreq: radiansPerSample, 350 | kphase: 0, 351 | } 352 | } 353 | function dountwistsum(id, radiansPerSample, block) { 354 | var array = block.output; 355 | var zero = [0, 0, 0]; 356 | var outbuf = [0, 0, 0]; 357 | return { 358 | id: id, 359 | color: 0xFF0000, 360 | n: 2, 361 | live: true, 362 | domain: [-timeRangeScale, timeRangeScale], 363 | expression: function (x, i) { 364 | if (i == 0) { 365 | return zero; 366 | } 367 | var zerophase = this.get('kphase'); 368 | var freq = this.get('kfreq'); 369 | var sumi = 0; 370 | var sumq = 0; 371 | var limit = array.length / 2; 372 | for (var i = 0; i < limit; i++) { 373 | var vi = array[i * 2]; 374 | var vq = array[i * 2 + 1]; 375 | var phase = zerophase + i * freq; 376 | var s = sin(phase); 377 | var c = cos(phase); 378 | sumq += (s * vi + c * vq); 379 | sumi += (c * vi - s * vq); 380 | } 381 | var scale = 40 / limit; 382 | outbuf[0] = scale * sumq; 383 | outbuf[1] = scale * sumi; 384 | outbuf[2] = 0; 385 | return outbuf; 386 | }, 387 | lineWidth: 2, 388 | kfreq: radiansPerSample, 389 | kphase: 0, 390 | } 391 | } 392 | 393 | function doconstellation(id, gain, constellation) { 394 | var n = constellation.length; 395 | var outbuf = [0, 0, 0]; 396 | return { 397 | id: id, 398 | color: 0xFF5555, 399 | n: n, 400 | live: false, 401 | domain: [0, 1], // unused 402 | line: true, 403 | arrow: true, 404 | expression: function (i, end) { 405 | if (end) { 406 | outbuf[2] = timeRangeScale; 407 | } else { 408 | outbuf[2] = -timeRangeScale; 409 | } 410 | var symbol = constellation[i]; 411 | outbuf[0] = gain * symbol[1]; 412 | outbuf[1] = gain * symbol[0]; 413 | return outbuf; 414 | }, 415 | lineWidth: 1, 416 | size: .02 417 | } 418 | } 419 | 420 | var counthalf = 10; 421 | var freqscale = Math.PI / counthalf * 0.1; 422 | function forfourier(f) { 423 | var out = []; 424 | for (var i = -counthalf; i <= counthalf; i++) { 425 | out.push(f(i, 'fourier' + i)); 426 | } 427 | return out; 428 | } 429 | 430 | mathbox.curve(docurve('audio', 0x0000FF, dsbbuf)); 431 | 432 | var step0 = [ 433 | 'A Visual Introduction to DSP for SDR', 434 | 'This presentation is intended to give a tour of DSP topics relevant to implementation of software-defined radios. This is not a complete introduction; if you want to do these things yourself you\'ll probably want a book, or somebody else\'s tutorial. The topics I have selected are those which are either particularly fundamental, or which would benefit from the style of animated graphics used here.' 435 | ]; 436 | var script = [ 437 | [ 438 | 'Amplitude modulation (AM)', 439 | 'This is a depiction of amplitude modulation as usually understood — you\'ve probably seen this sort of picture before. The modulating audio signal, in black, is offset above zero and then used to control the amplitude of the carrier signal — that is, they are multiplied — and the result is the signal shown in blue.', 440 | ['remove', '#audio'], 441 | ['add', 'curve', docurve('modulatingam', 0x000000, modulatingam)], 442 | ['add', 'curve', docurve('amout', 0x0077FF, amout)], 443 | ], 444 | [ 445 | 'Amplitude modulation (AM)', 446 | 'The problem with this picture, for our purposes, is that the math is messy. For example, if you were trying to demodulate this, every time the signal crosses zero, you have no data because the audio was multiplied by zero. Of course, in reality the carrier frequency is immensely higher than the audio frequency, so it\'s easy to average over that. It\'s not that it\'s infeasible to work this way — you can, in exact analogy to analog RF electronics, but rather that there\'s something else you can do which is much more elegant all around. It doesn\'t matter as much for AM, but I\'m using AM in this picture because it makes good pictures, not because it\'s a good example.', 447 | ], 448 | [ 449 | 'Complex-valued signals', 450 | 'Here we have a signal which has values which are complex numbers rather than real numbers. The carrier wave is a complex sinusoid — the real part is a sine and the imaginary part is a cosine. On this plot the real and imaginary parts are labeled I and Q — these are the conventional names in signal processing, which stand for "in-phase" and "quadrature". This can also be called an analytic signal, which means roughly that it has this helical structure as opposed to being, say, the real signal we usually think of but rotated into the complex plane. The modulation works exactly the same way as you\'ve already seen — multiplying the complex carrier by the real audio varies the magnitude of the signal. (We will see later how this picture corresponds to physical radio signals.) ', 451 | ['set', '#iaxis', {labels: true}], 452 | ['set', '#qaxis', {labels: true}], 453 | ['animate', 'camera', { 454 | phi: Math.PI * 0.7, 455 | theta: 0.05 456 | }, { 457 | delay: 0, 458 | duration: 6000 459 | }], 460 | ], 461 | [ 462 | 'Frequency modulation (FM)', 463 | 'This is what frequency modulation, FM, looks like in the same setting. The blue curve looks like the conventional picture of frequency modulation; you can see the cycles being closer together and farther apart here. The black line is again the signal without the carrier wave, but this time instead of moving radially, varying amplitude, it is moving around the circle — varying the frequency, the speed of rotation. When it\'s moving in the same direction as the carrier, the frequency is higher, and when it\'s moving in the opposite direction, the frequency is lower.', 464 | ['remove', '#amout, #modulatingam'], 465 | ['add', 'curve', docurve('modulatingfm', 0x000000, modulatingfm)], 466 | ['add', 'curve', docurve('product', 0x0077FF, product)], 467 | ], 468 | [ 469 | 'Frequency shifting', 470 | 'If we want to receive and demodulate this signal, we\'d like to get rid of that high-frequency carrier wave. In a real radio rather than this picture built for readability, the carrier frequency is immensely higher than the bandwidth of the actual signal, and we\'d like to not deal with the processing requirements of that high frequency — and also to be able to tune anywhere on the radio spectrum and treat the signals the same way.', 471 | ['remove', '#modulatingfm'] 472 | ], 473 | [ 474 | 'Frequency shifting', 475 | 'We do this by multiplying the signal by another complex sinusoid, shown in red, of equal and opposite frequency. This is a negative frequency — the helix is wound the other way. You can also call it the complex conjugate of the carrier, the number with the imaginary component negated. This cancels out the original carrier wave. In general, this technique allows you to change the frequency of an arbitrary signal, adding or subtracting an offset. When the signal is moved to be centered at zero — zero hertz — it is known as a _baseband_ signal.', 476 | ['add', 'curve', docurve('demodrot', 0xFF0000, demodrot)], 477 | //['animate', '#demodrot', { /* dummy */ }, { 478 | // duration: 1000 479 | //}] 480 | ], 481 | [ 482 | 'Sampling and the Nyquist frequency', 483 | 'Up until now, the pictures I\'ve been showing you have had solid lines. This is an accurate depiction of analog signals, but in DSP we are working with only a finite amount of data — the signal is sampled at fixed time intervals, producing a sequence of numbers. This graph is the exact same signal showing only the sample points. Generally, you want the sampling rate to be as slow as possible, to minimize the computation needed. However, there is a fundamental limit known as the Nyquist frequency.', 484 | ['remove', '#demodrot'], 485 | ['set', '#product', { 486 | points: true, 487 | line: false, 488 | }], 489 | ['animate', 'camera', { 490 | phi: Math.PI, 491 | theta: 0.05 492 | }, { 493 | delay: 0, 494 | duration: 1000 495 | }], 496 | ], 497 | [ 498 | 'Sampling and the Nyquist frequency', 499 | 'What you are seeing here is the instantaneous value of a sampled signal. The signal is a sinusoid with a frequency which is continuously increasing. As it increases, it appears to reverse, because the frequency is so high that it completes more than half of a complete cycle between every two samples. This is the Nyquist frequency — one-half of the sampling rate. A signal of some frequency f, when sampled, is exactly the same as a signal of frequency f plus the sampling rate.', 500 | ['remove', '#product'], 501 | ['animate', 'camera', { 502 | phi: Math.PI / 2, 503 | theta: 0.00 504 | }, { 505 | delay: 0, 506 | duration: 1000 507 | }], 508 | ['add', 'curve', { 509 | id: 'clockface', 510 | color: 0x000000, 511 | n: 2, 512 | live: true, 513 | points: true, 514 | line: true, 515 | domain: [-timeRangeScale, timeRangeScale], 516 | expression: (function() { 517 | var frame = 0, phase = 0; 518 | var zero = [0, 0, 0]; 519 | var outbuf = [0, 0, 0]; 520 | return function (x, n) { 521 | if (n == 0) { 522 | return zero; 523 | } else { 524 | frame++; 525 | var t = frame / 2000; 526 | var rate = 0.5 * (1 + sin(PI * (t % 1 - 0.5))) + Math.floor(t); 527 | phase += (rate * 1.016) * TWOPI; 528 | outbuf[0] = sin(phase) * 3; 529 | outbuf[1] = cos(phase) * 3; 530 | return outbuf; 531 | } 532 | }; 533 | })(), 534 | lineWidth: 2, 535 | }] 536 | ], 537 | [ 538 | 'Sampling and the Nyquist frequency', 539 | 'Frequencies in digital signal processing are points on a circle — they are modulo the sampling frequency. We usually think of them as having a range of plus or minus the Nyquist frequency, because the symmetry is useful. But since nothing in the math and physics respects that limit, we have to do it ourselves, by _filtering_. In a software-defined receiver, we filter using analog circuits to remove frequencies above the Nyquist frequency before sampling the signal. This removes the ambiguity and allows us to treat the frequencies in our digital signal as if they were not circular.', 540 | ], 541 | [ 542 | 'Digital filtering', 543 | 'Filtering is also useful for sampled digital signals, to allow us to reduce or increase the sample rate, or to separate a particular signal from nearby irrelevant signals and noise. Digital filters can be much more precise than analog filters, and they can be adjusted by changing data rather than changing circuits. To illustrate filtering, here is an example signal which is the combination — the sum — of two components of different frequencies. It looks very far from the nice helixes we\'ve seen so far, but you can easily see the two components. Practically, good digital filters can extract signals that you just can\'t see at all from a plot like this.', 544 | ['remove', '#clockface'], 545 | ['animate', 'camera', { 546 | phi: Math.PI, 547 | theta: almost_zero_theta 548 | }, { 549 | delay: 500, 550 | duration: 1000 551 | }], 552 | ['add', 'curve', docurve('twosig', 0x000000, twosig, twosigl)], 553 | ], 554 | [ 555 | 'Finite impulse response (FIR) filters', 556 | 'A simple and widely useful class of digital filters is finite impulse response filters. FIR filters operate by taking delayed copies of the input signal, scaling them, and adding them together — a sort of carefully designed moving average.', 557 | ], 558 | [ 559 | 'Finite impulse response (FIR) filters', 560 | 'In this picture, the copies have amplitudes of one-third, two-thirds, and one-third.', 561 | ['add', 'curve', docurve('twosigp', 0xFF2222, twosig)], 562 | ['add', 'curve', docurve('twosign', 0x00EE00, twosig)], 563 | ['animate', '#twosigp', { 564 | mathPosition: [0, 0, timeRangeScale / sampleCount * 2], 565 | }, { 566 | duration: 1000, 567 | }], 568 | ['animate', '#twosign', { 569 | mathPosition: [0, 0, -timeRangeScale / sampleCount * 2], 570 | }, { 571 | duration: 1000, 572 | }], 573 | ['animate', '#twosig', { 574 | mathScale: [filterInnerCoeff, filterInnerCoeff, 1], 575 | }, { 576 | delay: 1000, 577 | duration: 1000, 578 | }], 579 | ['animate', '#twosigp', { 580 | mathScale: [filterOuterCoeff, filterOuterCoeff, 1], 581 | }, { 582 | delay: 1000, 583 | duration: 1000, 584 | }], 585 | ['animate', '#twosign', { 586 | mathScale: [filterOuterCoeff, filterOuterCoeff, 1], 587 | }, { 588 | delay: 1000, 589 | duration: 1000, 590 | }], 591 | ], 592 | [ 593 | 'Low-pass filter', 594 | 'When those three are added together, the result contains mostly the low-frequency component of the input signal and not the high-frequency component. This kind of filter is called a low-pass filter. It\'s not a very good one — good filters have systematically chosen coefficients for the scaling, and have many more of them. These coefficients are also called taps, they are the same as the impulse response of the filter, and the count of them is called the filter\'s order.', 595 | ['animate', '#twosig', { 596 | ksiginterp: 1, 597 | mathScale: [1, 1, 1], 598 | }, { 599 | duration: 1000, 600 | }], 601 | ['animate', '#twosigp', { 602 | mathScale: [0, 0, 1], 603 | opacity: 0, 604 | }, { 605 | duration: 1000, 606 | }], 607 | ['animate', '#twosign', { 608 | mathScale: [0, 0, 1], 609 | opacity: 0, 610 | }, { 611 | duration: 1000, 612 | }], 613 | ], 614 | [ 615 | 'High-pass filter', 616 | 'If instead of adding the three copies we subtract the outer ones from the middle one, the filter becomes a high-pass filter, keeping the high-frequency component instead of the low-frequency one.', 617 | ['remove', '#twosig'], 618 | ['add', 'curve', docurve('twosigh', 0x000000, twosig, twosigh)], 619 | ['animate', '#twosigp', { 620 | mathScale: [filterOuterCoeff, filterOuterCoeff, 1], 621 | opacity: 1, 622 | }, { 623 | duration: 10, 624 | }], 625 | ['animate', '#twosign', { 626 | mathScale: [filterOuterCoeff, filterOuterCoeff, 1], 627 | opacity: 1, 628 | }, { 629 | duration: 10, 630 | }], 631 | ['animate', '#twosigp', { 632 | mathScale: [-filterOuterCoeff, -filterOuterCoeff, 1], 633 | }, { 634 | delay: 10, 635 | duration: 900, 636 | }], 637 | ['animate', '#twosign', { 638 | mathScale: [-filterOuterCoeff, -filterOuterCoeff, 1], 639 | }, { 640 | delay: 10, 641 | duration: 900, 642 | }], 643 | ['animate', '#twosigh', { 644 | ksiginterp: 1 645 | }, { 646 | delay: 1000, 647 | duration: 1000, 648 | }], 649 | ['animate', '#twosigp', { 650 | mathScale: [0, 0, 1], 651 | opacity: 0, 652 | }, { 653 | delay: 1000, 654 | duration: 1000, 655 | }], 656 | ['animate', '#twosign', { 657 | mathScale: [0, 0, 1], 658 | opacity: 0, 659 | }, { 660 | delay: 1000, 661 | duration: 1000, 662 | }], 663 | ], 664 | [ 665 | 'Infinite impulse response (IIR) filters', 666 | 'A so-called infinite impulse response filter works exactly the same way as a finite impulse response filter, except that in addition to summing input samples, it has feedback from its own previous outputs. IIR filters can be more efficient than FIR filters by having a lower order (fewer taps) but have additional hazards such as instability — runaway feedback. As a side note, you may well have used an IIR filter yourself — if you\'ve ever implemented an average like input times 0.1 plus previous output times 0.9, then that\'s an IIR filter of order one.', 667 | ], 668 | //['TODO: Discuss filtering for sample rate conversion', ''], 669 | [ 670 | 'Live Filter', 671 | 'Here\'s some filters applied to the live audio; high-pass in green, low-pass in red.', 672 | ['remove', '#twosign'], 673 | ['remove', '#twosigp'], 674 | ['remove', '#twosigh'], 675 | ['add', 'curve', docurve('audio', 0x0000FF, dsbbuf)], 676 | ['add', 'curve', docurve('audioh', 0x00DD00, audioh)], 677 | ['add', 'curve', docurve('audiol', 0xFF0000, audiol)], 678 | ], 679 | [ 680 | 'The discrete Fourier transform', 681 | 'The discrete Fourier transform, commonly referred to as the fast Fourier transform (which is actually the name of an algorithm for computing it), converts a signal in the form of an array of samples over time — which is what we\'ve been working with so far — into an array of samples over _frequency_. This enables visualization and analysis of an unknown signal, and can also be used to implement filters.', 682 | ['remove', '#audioh'], 683 | ['remove', '#audiol'], 684 | ['animate', 'camera', { 685 | phi: Math.PI * 1.0, 686 | theta: almost_zero_theta 687 | }, { 688 | delay: 0, 689 | duration: 1000 690 | }], 691 | ], 692 | (function () { 693 | return [ 694 | 'The discrete Fourier transform', 695 | 'First, let\'s have a large number of copies of the input signal. In reality, we would have a number equal to the length of the input array, but for this illustration ' + (counthalf * 2 + 1) + ' will do.', 696 | ['remove', '#iaxis'], 697 | ['remove', '#qaxis'], 698 | ['remove', '#audio'], 699 | ['animate', 'camera', { 700 | phi: Math.PI * 0.7, 701 | theta: 0.2 702 | }, { 703 | delay: 0, 704 | duration: 2000 705 | }] 706 | ].concat(forfourier(function (i, id) { 707 | return ['add', 'curve', dountwist(id, 0x0000FF, 0, dsbbuf)]; 708 | })).concat(forfourier(function (i, id) { 709 | return ['add', 'axis', { 710 | id: id + 'axis', 711 | axis: 2, 712 | color: 0x777777, 713 | ticks: 3, 714 | lineWidth: 2, 715 | size: .05, 716 | arrow: true, 717 | }]; 718 | })).concat(forfourier(function (i, id) { 719 | return ['animate', '#' + id, { 720 | mathPosition: [i, 0, 0] 721 | }, { 722 | delay: 3000, 723 | duration: 3000, 724 | }]; 725 | })).concat(forfourier(function (i, id) { 726 | return ['animate', '#' + id + 'axis', { 727 | mathPosition: [i, 0, 0] 728 | }, { 729 | delay: 3000, 730 | duration: 3000, 731 | }]; 732 | })); 733 | }()), 734 | (function () { 735 | return [ 736 | 'The discrete Fourier transform', 737 | 'Then we multiply the signals by complex sinusoids with equally spaced frequencies. The copy remaining at the center has frequency zero, so it is unchanged. As we saw earlier, the effect of this is that a signal with the equal and opposite frequency will be “untwisted”, becoming a signal with constant phase — that is, it does not rotate around the axis.', 738 | ].concat(forfourier(function (i, id) { 739 | return ['animate', '#' + id, { 740 | kfreq: i * freqscale 741 | }, { 742 | delay: 0, 743 | duration: 7000, 744 | }]; 745 | })); 746 | }()), 747 | (function () { 748 | return [ 749 | 'The discrete Fourier transform', 750 | 'Now if we look at these signals end-on, discarding the time information, we can see which ones are least twisted. These are the closest matches to the frequency components in the original signal!', 751 | ['animate', 'camera', { 752 | phi: Math.PI * 0.5, 753 | theta: 0.0 754 | }, { 755 | delay: 0, 756 | duration: 2000 757 | }] 758 | ]; 759 | }()), 760 | (function () { 761 | return [ 762 | 'The discrete Fourier transform', 763 | 'The final step is to sum these signals over time. Where the frequency doesn\'t match, the samples cancel each other out, so the output values are close to zero. Where the frequency does match, the samples combine and produce a large output value. At this point we have a complete DFT. If we took the red lines above, oriented them in the same direction (discarding the phase), and made the lengths logarithmic, you would then have a spectrogram, exactly as a spectrum analyzer or SDR receiver application displays.', 764 | ].concat(forfourier(function (i, id) { 765 | return ['add', 'curve', dountwistsum(id + 'sum', i * freqscale, dsbbuf)]; 766 | })).concat(forfourier(function (i, id) { 767 | return ['set', '#' + id + 'sum', { 768 | mathPosition: [i, 0, 0] 769 | }]; 770 | })); 771 | }()), 772 | (function () { 773 | return [ 774 | 'The fast Fourier transform (FFT)', 775 | 'The fast Fourier transform is an algorithm for implementing the DFT efficiently — the naïve implementation I have described here is quadratic in the length of the input. ', 776 | ].concat(forfourier(function (i, id) { 777 | return ['add', 'curve', dountwistsum(id + 'sum', i * freqscale, dsbbuf)]; 778 | })).concat(forfourier(function (i, id) { 779 | return ['set', '#' + id + 'sum', { 780 | mathPosition: [i, 0, 0] 781 | }]; 782 | })); 783 | }()), 784 | [ 785 | 'Real signals', 786 | 'This graphic also shows the relationship of complex-valued signals to real signals. The spectrum of a real signal, which this is, is always symmetric about zero. In other words, a real signal cannot distinguish negative frequencies from positive frequencies, where a complex signal can. A real sinusoid is equivalent to the sum of two complex sinusoids of opposite frequency — the imaginary components cancel out leaving the real component.' 787 | ], 788 | [ 789 | 'Real signals', 790 | 'That\'s all I have to say about the Fourier transform.' 791 | ], 792 | (function () { 793 | return [ 794 | 'Digital modulation', 795 | 'Up to this point, I\'ve been talking about digital signal processing — that is, signal processing as performed by a digital computer. Now, I\'m going to talk about digital _modulation_ — that is, signals which carry digital data. Here we see a digital signal as you might have it in an introduction to digital logic — two levels, representing binary digits one and zero, and sharp transitions between the two.', 796 | ['animate', 'camera', { 797 | phi: PI, 798 | theta: almost_zero_theta 799 | }, { 800 | delay: 0, 801 | duration: 1000 802 | }], 803 | ['add', 'axis', Object.create(iaxisdef, {labels:{value:true}})], 804 | ['add', 'axis', Object.create(qaxisdef, {labels:{value:true}})], 805 | ['add', 'curve', docurve('dighold', 0x000000, dighold), { 806 | delay: 700, 807 | }], 808 | //['set', '#dighold', { 809 | // points: true, 810 | // line: false, 811 | //}] 812 | ].concat(forfourier(function (i, id) { 813 | return ['remove', '#' + id + 'sum']; 814 | })).concat(forfourier(function (i, id) { 815 | return ['remove', '#' + id]; 816 | })).concat(forfourier(function (i, id) { 817 | return ['remove', '#' + id + 'axis']; 818 | })); 819 | }()), 820 | [ 821 | 'On-off keying', 822 | 'Here\'s the simplest digital modulation, known as on-off keying. For amateur radio operators, this is the CW mode, though the bit sequence here is not Morse code. This is actually identical to the amplitude modulation I showed you at the beginning, except that instead of the modulating signal being audio, centered about +1, it\'s digital data and it takes on only the values one and zero. This is a very simple modulation to transmit, and very power-efficient, because you just switch your transmitter on and off. However, note that because of the sharp transitions in amplitude, this signal as shown has a very wide bandwidth at those transitions (in amateur radio terms, “key clicks”).', 823 | // TODO write non-hams version of this slide 824 | ['add', 'curve', docurve('digook', 0x0077FF, digook)], 825 | ], 826 | [ 827 | 'On-off keying with pulse shaping', 828 | 'In order to fix the transitions, we use a pulse shaping filter on the modulating signal. This looks like a mess, and in fact if someone\'s CW transmitter had a keying waveform like this it would be horrible to listen to because it lacks crisp transitions, but it does minimize the bandwidth used, and it\'s actually easy for demodulators to handle, as we will see later. Incidentally, the little notches you can see in the signal are imperfections in the filter. We use a truncated filter, one with fewer taps, to save computation at the price of not being quite ideal; the ideal filter would be infinitely long and therefore imposible to implement.', 829 | // TODO write non-hams version of this slide 830 | ['remove', '#digook'], 831 | ['set', '#dighold', {color: 0xAAAAAA}], 832 | ['add', 'curve', docurve('digshap', 0x000000, digshap)], 833 | ['add', 'curve', docurve('digshapook', 0x0077FF, digshapook)], 834 | ], 835 | [ 836 | 'On-off keying with pulse shaping', 837 | 'Now let\'s look at another type of modulation.', 838 | ], 839 | [ 840 | 'Phase-shift keying', 841 | 'Here we\'ve taken the modulating signal and replaced the zeroes with minus ones. That is, we\'re multiplying the carrier wave by minus one. This has the effect of producing a _phase shift_ by 180 degrees, while leaving the amplitude the same. However, we\'ve introduced an ambiguity — when the receiver starts receiving the signal, it doesn\'t have any reference phase, and so a phase transition could be from zero to one or one to zero. This isn\'t a serious problem, because more synchronization information is needed to make sense of the bits anyway — a known sequence at the beginning of the transmission packet can resolve the ambiguity. Or you can use a differential encoding, where a phase transition stands for one and no transition stands for zero, or vice versa.', 842 | ['remove', '#dighold'], 843 | ['remove', '#digshap'], 844 | ['remove', '#digshapook'], 845 | ['add', 'curve', docurve('digpnhold', 0xAAAAAA, digpnhold)], 846 | ['add', 'curve', docurve('digpnshap', 0x000000, digpnshap)], 847 | ['add', 'curve', dountwist('digpnkey', 0x0077FF, 0, digpnkey)], 848 | ], 849 | [ 850 | 'Digital demodulation', 851 | 'Before I discuss more complex modulations, let\'s look at what it takes to demodulate this signal, if nothing else so this fairly messy picture gets cleaner. What we have in this case is a carrier wave with occasional phase shifts; we need to recover the original bits.', 852 | ['remove', '#digpnhold'], 853 | ['remove', '#digpnshap'], 854 | ['animate', 'camera', { 855 | phi: Math.PI * 0.8, 856 | theta: 0.05 857 | }, { 858 | delay: 500, 859 | duration: 1000 860 | }] 861 | ], 862 | [ 863 | 'Digital demodulation', 864 | 'First, as we did with the previous analog signals, we perform a frequency shift to return the signal to baseband, using the frequency the receiver is set to receive. However, because no two independently running oscillators are going to be at exactly the same frequency, this won\'t give perfect results; we need to perform a final correction.', 865 | ['animate', '#digpnkey', { 866 | kfreq: -digchfreq * 1.02, 867 | kphase: 0.4 // arbitrary 868 | }, { 869 | delay: 0, 870 | duration: 7000, 871 | }] 872 | ], 873 | [ 874 | 'Digital demodulation', 875 | 'There are a number of algorithms which can be used for this purpose depending on properties of the modulation in use. In any case we now have a true baseband signal with no twist to it. The next problem is that we need to recover the bits — to slice this signal up along the time axis.', 876 | ['animate', '#digpnkey', { 877 | kfreq: -digchfreq, 878 | }, { 879 | delay: 0, 880 | duration: 1000, 881 | }], 882 | ['animate', '#digpnkey', { 883 | kphase: 0, 884 | }, { 885 | delay: 1000, 886 | duration: 400, 887 | }] 888 | ], 889 | [ 890 | 'Digital demodulation', 891 | 'Remember, this is a digital signal, so we already have it sliced up in a sense, but we have far too many samples; we want instead to have exactly one sample per bit. To do this, we need to take those samples in a way which is synchronized with the digital clock which generated the bits in the first place at the transmitter; this is necessary to ensure we don\'t take samples halfway between two bits and read nonsense. Again, there are various algorithms for this synchronization, and I\'m going to skip the details of that.', 892 | ['set', '#digpnkey', { 893 | points: true, 894 | line: false, 895 | }] 896 | ], 897 | [ 898 | 'Digital demodulation', 899 | 'So, here we have the original bits — still represented as plus and minus one. I\'ve also conveniently assumed we\'ve recovered the exact original phase so they\'re lined up on the I axis.', 900 | ['remove', '#digpnkey'], 901 | ['add', 'curve', dopoints('digpnbase', 0x000000, digpnbase)], 902 | ], 903 | [ 904 | 'Digital demodulation', 905 | 'Now let\'s take the end-on view. Here, we stop being able to see the actual data, and we instead see just all the _possible_ positions in the signal. This is known as a constellation diagram, and it is a very useful representation of digital signals.', 906 | ['animate', 'camera', { 907 | phi: Math.PI / 2, 908 | theta: 0.00 909 | }, { 910 | delay: 1000, 911 | duration: 2000 912 | }], 913 | ['add', 'vector', doconstellation('pskconst', 1, [[1, 0], [-1, 0]])], 914 | ], 915 | [ 916 | 'Symbols', 917 | 'Notice that there\'s lots of empty space in this diagram — we\'re only using one dimension, but there are two available. Let\'s do that.', 918 | ], 919 | [ 920 | 'Quadrature phase-shift keying (QPSK)', 921 | 'Now there are four dots rather than two; the digital signal must have four possible values instead of two. Since there are more than two, each dot represents more than a single bit; we call these _symbols_. It takes two bits to identify one of four things, so there are two bits per symbol. Angles on this diagram are phase, so there is 90 degrees of phase separation between them. This case with four symbols is called quadrature phase-shift keying, or QPSK.', 922 | ['remove', '#digpnbase'], 923 | ['remove', '#pskconst'], 924 | ['add', 'curve', dopoints('digqpskbase', 0x000000, digqpskbase)], 925 | ['add', 'vector', doconstellation('qpskconst', 1/Math.sqrt(2), qpskconst)], 926 | ], 927 | [ 928 | 'Quadrature phase-shift keying (QPSK)', 929 | 'In general, you can have PSK with any number of symbols, but as the number of symbols increases the decreasing separation means that a better signal-to-noise ratio is required to receive the symbols without error. If there were noise in this signal, then you would see it on the diagram as these single points becoming fuzzy clouds of samples; a decoding error occurs when the noise pushes one of the samples into being closer to a different symbol than the correct symbol.', 930 | ['remove', '#digpnbase'], 931 | ['add', 'curve', dopoints('digqpskbase', 0x000000, digqpskbase)], 932 | ], 933 | [ 934 | 'Quadrature amplitude modulation (QAM)', 935 | 'Suppose we put in an entire grid of points. Ignoring what it means for the moment, we can see that the diagram implies this ought to work just as well; it\'s got 16 symbols, each therefore carrying 4 bits. This is called quadrature amplitude modulation (QAM), because unlike the PSK we\'ve seen before, the points don\'t lie on a circle — that is, the amplitude as well as the phase is being changed. QAM has very good spectral efficiency — data rate per bandwidth used — because it\'s using all the degrees of freedom available in the signal. However, it does require good linearity in both the transmitter and receiver, since amplitude distinctions are critical. QAM is commonly used in high bandwidth applications; for example, in digital cable TV and internet service, where constellations with up to 256 symbols are used.', 936 | ['remove', '#digqpskbase'], 937 | ['remove', '#qpskconst'], 938 | ['add', 'curve', dopoints('digqambase', 0x000000, digqambase)], 939 | ['add', 'vector', doconstellation('qamconst', 1/Math.sqrt(2), qamconst)], 940 | ], 941 | // TODO: FSK in more detail 942 | [ 943 | 'Digital modulations: wrap', 944 | 'There\'s lots more that can be said about digital modulations, but that\'s all I have for the moment. One thing I notably haven\'t covered is frequency-shift keying (FSK) modulation. Very briefly, FSK is FM with discrete levels rather than an audio signal.', 945 | ], 946 | [ 947 | 'End', 948 | 'This presentation copyright © 2014, 2015, 2018 Kevin Reid. Implemented using the MathBox.js framework. https://visual-dsp.switchb.org/', 949 | //['remove', '#digqambase'], 950 | ['add', 'curve', docurve('digqamshap', 0x000000, digqamshap)], 951 | ['add', 'curve', dountwist('digqamkey', 0x0077FF, 0, digqamkey)], 952 | ['animate', 'camera', { 953 | phi: Math.PI * 0.7, 954 | theta: Math.PI * 0.1 955 | }, { 956 | delay: 0, 957 | duration: 2000 958 | }], 959 | ], 960 | ]; 961 | var mbscript = script.map(function(step) { return step.slice(2); }); 962 | mbdirector = new MathBox.Director(mathbox, mbscript); 963 | 964 | var baseTitle = document.title; 965 | document.body.addEventListener('keydown', function (event) { 966 | if (event.keyCode == 39) { 967 | mbdirector.forward(); 968 | } else if (event.keyCode == 37) { 969 | mbdirector.back(); 970 | } else { 971 | return; 972 | } 973 | writeFragment(); 974 | g(); 975 | //console.log('Now at slide', mbdirector.step); 976 | }, false); 977 | //setTimeout(function() { 978 | // mbdirector.forward(); 979 | //}, 1000); 980 | //mbdirector.go(script.length - 1); 981 | 982 | function readFragment() { 983 | var fragment = window.location.hash; 984 | if (fragment[0] !== "#") return; 985 | mbdirector.go(parseInt(fragment.substr(1))); 986 | writeFragment(); 987 | } 988 | function writeFragment() { 989 | document.title = '(' + mbdirector.step + ') ' + baseTitle; 990 | window.history.replaceState(undefined, document.title, '#' + mbdirector.step); 991 | } 992 | window.addEventListener("popstate", function (event) { 993 | readFragment(); 994 | }); 995 | readFragment(); 996 | 997 | setInterval(function() { 998 | var step = mbdirector.step; 999 | document.getElementById('slidetitle').textContent = (step ? script[step - 1] : step0)[0]; 1000 | document.getElementById('slidecaption').textContent = (step ? script[step - 1] : step0)[1]; 1001 | }, 20); 1002 | } 1003 | 1004 | var paused = false; 1005 | var leveltrigger = false; 1006 | var nextDigUpdate = 0; 1007 | document.body.addEventListener('keydown', function (event) { 1008 | if (event.keyCode == 0x20) { 1009 | paused = !paused; 1010 | } 1011 | if (event.keyCode == 'T'.charCodeAt(0)) { 1012 | leveltrigger = true; 1013 | paused = false; 1014 | console.log('leveltrigger'); 1015 | } 1016 | }, false); 1017 | 1018 | var audioTriggerArray = new Float32Array(fftnode.fftSize); 1019 | function updateFFT() { 1020 | if (!paused) { 1021 | 1022 | fftnode.getFloatFrequencyData(fftarray); 1023 | fftnode.getFloatTimeDomainData(audioTriggerArray); 1024 | var outLengthHalf = Math.floor(audioarray.length / 2); 1025 | var limit = fftnode.fftSize - outLengthHalf - 1; 1026 | // rising edge trigger 1027 | for (var i = outLengthHalf; i < limit; i++) { 1028 | if (audioTriggerArray[i] <= 0 && audioTriggerArray[i + 1] > 0) { 1029 | break; 1030 | } 1031 | } 1032 | audioarray.set(audioTriggerArray.subarray(i - outLengthHalf, i + outLengthHalf)); 1033 | 1034 | } 1035 | 1036 | if (!paused || (mbdirector && mbdirector.step == demodStep)) { 1037 | g(); 1038 | 1039 | if (nextDigUpdate <= Date.now()) { 1040 | nextDigUpdate = Date.now() + digUpdateInterval; 1041 | for (var i = 0; i < digdata.length; i++) { 1042 | digdata[i] = Math.random() > 0.5; 1043 | } 1044 | //console.log(number, Array.prototype.slice.call(digdata)); 1045 | diggraph(); 1046 | } 1047 | } 1048 | 1049 | if (leveltrigger) { 1050 | for (var i = audioarray.length - 1; i >= 0; i--) { 1051 | if (audioarray[i] > 0.5) { 1052 | leveltrigger = false; 1053 | paused = true; 1054 | console.log('triggered'); 1055 | break; 1056 | } 1057 | } 1058 | } 1059 | } 1060 | 1061 | function loop() { 1062 | updateFFT(); 1063 | requestAnimationFrame(loop); 1064 | } 1065 | requestAnimationFrame(loop); 1066 | })(); -------------------------------------------------------------------------------- /presentation.html: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | A Visual Introduction to DSP for SDR 14 | 36 | 37 | 38 | 39 | 40 | (Not loaded) 45 | 49 | 50 | 53 | 54 | Microphone Input 55 | Signal generator 56 | 57 | Gain 58 | 59 | 60 | 61 | 62 | 63 | --------------------------------------------------------------------------------