/*============================================================= Filename: Spectrogram-1v00.js JavaScript graphics functions to draw Spectrograms. Date Description By -------|-------------------------------------------------|--- 12Nov18 First beta ARC 17Nov18 Added offset into data buffer ARC 08May19 this.imageURL URL added bugfix: fixed isNaN test Changed sgStart, sgStop to start, stop Added options object to constructors ARC 10May19 Enabled Left to Right as well as Top to Bottom ARC 11May19 Added RasterscanSVG ARC 12May19 Added blnkline for horizontal ratser scans ARC 13May19 Eliminated unneccessary putImageData ARC 14May19 Removed toDataURL, not used drawImage is better bugfix: SVG RHC names swapped ARC 02Jun19 bugfix: startOfs not honored in horizontalNewLine ARC 03Jun19 Flipped the SVG and RHC names for waterfalls ARC 04Jun19 Unflip SVG and RHC for horizontal mode ARC Swap "SVG" & "RHC" strings to match fn names ARC 05Jun19 bugfix: WaterfallSVG scrolling wrong way ARC 10Jun19 bugfix: support lineRate=0 for static display bugfix: ipBufPtr must be a ptr to a ptr ARC 11Jun19 Make ipBuffers an Array of Arrays, if lineRate=0 use all buffers else use only ipBuffer[0] ARC 13Jun19 Use Waterfall and Rasterscan plus direction Use Boolean rater than string compare ARC 16Jun19 Use const and let ARC 20Jun19 Change order of parameters ARC 21Jun19 Add setLineRate method ARC 06Jul19 Released as Rev 1v00 ARC ==============================================================*/ var Waterfall, Rasterscan; (function () { Waterfall = function (ipBufAry, w, h, dir, options) { var direction = typeof dir === "string" ? dir.toLowerCase() : "down"; switch (direction) { case "up": return new Spectrogram(ipBufAry, w, h, "WF", false, true, options); case "down": default: return new Spectrogram(ipBufAry, w, h, "WF", true, true, options); case "left": return new Spectrogram(ipBufAry, w, h, "WF", false, false, options); case "right": return new Spectrogram(ipBufAry, w, h, "WF", true, false, options); } }; Rasterscan = function (ipBufAry, w, h, dir, options) { const direction = typeof dir === "string" ? dir.toLowerCase() : "down"; switch (direction) { case "up": return new Spectrogram(ipBufAry, w, h, "RS", true, true, options); case "down": default: return new Spectrogram(ipBufAry, w, h, "RS", false, true, options); case "left": return new Spectrogram(ipBufAry, w, h, "RS", false, false, options); case "right": return new Spectrogram(ipBufAry, w, h, "RS", true, false, options); } }; function Spectrogram(ipBufAry, w, h, sgMode, rhc, vert, options) { const opt = typeof options === "object" ? options : {}; // avoid undeclared object errors let offScreenCtx; // offscreen canvas drawing context const pxPerLine = w || 200; const lines = h || 200; let lineRate = 30; // requested line rate for dynamic waterfalls let interval = 0; // msec let startOfs = 0; const lineBuf = new ArrayBuffer(pxPerLine * 4); // 1 line const lineBuf8 = new Uint8ClampedArray(lineBuf); const lineImgData = new ImageData(lineBuf8, pxPerLine, 1); // 1 line of canvas pixels let pageImgData; // lines * pxPerLine of canvas pixels let ipBuf8; // map input data to 0..255 unsigned bytes const blankBuf = new ArrayBuffer(pxPerLine * 4); // 1 line const blankBuf8 = new Uint8ClampedArray(blankBuf); const blankImgData = new ImageData(blankBuf8, pxPerLine, 1); // 1 line of canvas pixels const clearBuf = new ArrayBuffer(pxPerLine * lines * 4); // fills with 0s ie. rgba 0,0,0,0 = transparent const clearBuf8 = new Uint8ClampedArray(clearBuf); let clearImgData; let nextLine = 0; let timerID = null; let running = false; let sgTime = 0; let sgStartTime = 0; // Matlab Jet ref: stackoverflow.com grayscale-to-red-green-blue-matlab-jet-color-scale let colMap = [ [0, 0, 128, 255], [0, 0, 131, 255], [0, 0, 135, 255], [0, 0, 139, 255], [0, 0, 143, 255], [0, 0, 147, 255], [0, 0, 151, 255], [0, 0, 155, 255], [0, 0, 159, 255], [0, 0, 163, 255], [0, 0, 167, 255], [0, 0, 171, 255], [0, 0, 175, 255], [0, 0, 179, 255], [0, 0, 183, 255], [0, 0, 187, 255], [0, 0, 191, 255], [0, 0, 195, 255], [0, 0, 199, 255], [0, 0, 203, 255], [0, 0, 207, 255], [0, 0, 211, 255], [0, 0, 215, 255], [0, 0, 219, 255], [0, 0, 223, 255], [0, 0, 227, 255], [0, 0, 231, 255], [0, 0, 235, 255], [0, 0, 239, 255], [0, 0, 243, 255], [0, 0, 247, 255], [0, 0, 251, 255], [0, 0, 255, 255], [0, 4, 255, 255], [0, 8, 255, 255], [0, 12, 255, 255], [0, 16, 255, 255], [0, 20, 255, 255], [0, 24, 255, 255], [0, 28, 255, 255], [0, 32, 255, 255], [0, 36, 255, 255], [0, 40, 255, 255], [0, 44, 255, 255], [0, 48, 255, 255], [0, 52, 255, 255], [0, 56, 255, 255], [0, 60, 255, 255], [0, 64, 255, 255], [0, 68, 255, 255], [0, 72, 255, 255], [0, 76, 255, 255], [0, 80, 255, 255], [0, 84, 255, 255], [0, 88, 255, 255], [0, 92, 255, 255], [0, 96, 255, 255], [0, 100, 255, 255], [0, 104, 255, 255], [0, 108, 255, 255], [0, 112, 255, 255], [0, 116, 255, 255], [0, 120, 255, 255], [0, 124, 255, 255], [0, 128, 255, 255], [0, 131, 255, 255], [0, 135, 255, 255], [0, 139, 255, 255], [0, 143, 255, 255], [0, 147, 255, 255], [0, 151, 255, 255], [0, 155, 255, 255], [0, 159, 255, 255], [0, 163, 255, 255], [0, 167, 255, 255], [0, 171, 255, 255], [0, 175, 255, 255], [0, 179, 255, 255], [0, 183, 255, 255], [0, 187, 255, 255], [0, 191, 255, 255], [0, 195, 255, 255], [0, 199, 255, 255], [0, 203, 255, 255], [0, 207, 255, 255], [0, 211, 255, 255], [0, 215, 255, 255], [0, 219, 255, 255], [0, 223, 255, 255], [0, 227, 255, 255], [0, 231, 255, 255], [0, 235, 255, 255], [0, 239, 255, 255], [0, 243, 255, 255], [0, 247, 255, 255], [0, 251, 255, 255], [0, 255, 255, 255], [4, 255, 251, 255], [8, 255, 247, 255], [12, 255, 243, 255], [16, 255, 239, 255], [20, 255, 235, 255], [24, 255, 231, 255], [28, 255, 227, 255], [32, 255, 223, 255], [36, 255, 219, 255], [40, 255, 215, 255], [44, 255, 211, 255], [48, 255, 207, 255], [52, 255, 203, 255], [56, 255, 199, 255], [60, 255, 195, 255], [64, 255, 191, 255], [68, 255, 187, 255], [72, 255, 183, 255], [76, 255, 179, 255], [80, 255, 175, 255], [84, 255, 171, 255], [88, 255, 167, 255], [92, 255, 163, 255], [96, 255, 159, 255], [100, 255, 155, 255], [104, 255, 151, 255], [108, 255, 147, 255], [112, 255, 143, 255], [116, 255, 139, 255], [120, 255, 135, 255], [124, 255, 131, 255], [128, 255, 128, 255], [131, 255, 124, 255], [135, 255, 120, 255], [139, 255, 116, 255], [143, 255, 112, 255], [147, 255, 108, 255], [151, 255, 104, 255], [155, 255, 100, 255], [159, 255, 96, 255], [163, 255, 92, 255], [167, 255, 88, 255], [171, 255, 84, 255], [175, 255, 80, 255], [179, 255, 76, 255], [183, 255, 72, 255], [187, 255, 68, 255], [191, 255, 64, 255], [195, 255, 60, 255], [199, 255, 56, 255], [203, 255, 52, 255], [207, 255, 48, 255], [211, 255, 44, 255], [215, 255, 40, 255], [219, 255, 36, 255], [223, 255, 32, 255], [227, 255, 28, 255], [231, 255, 24, 255], [235, 255, 20, 255], [239, 255, 16, 255], [243, 255, 12, 255], [247, 255, 8, 255], [251, 255, 4, 255], [255, 255, 0, 255], [255, 251, 0, 255], [255, 247, 0, 255], [255, 243, 0, 255], [255, 239, 0, 255], [255, 235, 0, 255], [255, 231, 0, 255], [255, 227, 0, 255], [255, 223, 0, 255], [255, 219, 0, 255], [255, 215, 0, 255], [255, 211, 0, 255], [255, 207, 0, 255], [255, 203, 0, 255], [255, 199, 0, 255], [255, 195, 0, 255], [255, 191, 0, 255], [255, 187, 0, 255], [255, 183, 0, 255], [255, 179, 0, 255], [255, 175, 0, 255], [255, 171, 0, 255], [255, 167, 0, 255], [255, 163, 0, 255], [255, 159, 0, 255], [255, 155, 0, 255], [255, 151, 0, 255], [255, 147, 0, 255], [255, 143, 0, 255], [255, 139, 0, 255], [255, 135, 0, 255], [255, 131, 0, 255], [255, 128, 0, 255], [255, 124, 0, 255], [255, 120, 0, 255], [255, 116, 0, 255], [255, 112, 0, 255], [255, 108, 0, 255], [255, 104, 0, 255], [255, 100, 0, 255], [255, 96, 0, 255], [255, 92, 0, 255], [255, 88, 0, 255], [255, 84, 0, 255], [255, 80, 0, 255], [255, 76, 0, 255], [255, 72, 0, 255], [255, 68, 0, 255], [255, 64, 0, 255], [255, 60, 0, 255], [255, 56, 0, 255], [255, 52, 0, 255], [255, 48, 0, 255], [255, 44, 0, 255], [255, 40, 0, 255], [255, 36, 0, 255], [255, 32, 0, 255], [255, 28, 0, 255], [255, 24, 0, 255], [255, 20, 0, 255], [255, 16, 0, 255], [255, 12, 0, 255], [255, 8, 0, 255], [255, 4, 0, 255], [255, 0, 0, 255], [251, 0, 0, 255], [247, 0, 0, 255], [243, 0, 0, 255], [239, 0, 0, 255], [235, 0, 0, 255], [231, 0, 0, 255], [227, 0, 0, 255], [223, 0, 0, 255], [219, 0, 0, 255], [215, 0, 0, 255], [211, 0, 0, 255], [207, 0, 0, 255], [203, 0, 0, 255], [199, 0, 0, 255], [195, 0, 0, 255], [191, 0, 0, 255], [187, 0, 0, 255], [183, 0, 0, 255], [179, 0, 0, 255], [175, 0, 0, 255], [171, 0, 0, 255], [167, 0, 0, 255], [163, 0, 0, 255], [159, 0, 0, 255], [155, 0, 0, 255], [151, 0, 0, 255], [147, 0, 0, 255], [143, 0, 0, 255], [139, 0, 0, 255], [135, 0, 0, 255], [131, 0, 0, 255], [0, 0, 0, 0], ]; function incrLine() { if ((vert && !rhc) || (!vert && rhc)) { nextLine++; if (nextLine >= lines) { nextLine = 0; } } else { nextLine--; if (nextLine < 0) { nextLine = lines - 1; } } } function updateWaterfall() { // update dynamic waterfalls at a fixed rate let sgDiff; // grab latest line of data, write it to off screen buffer, inc 'nextLine' sgNewLine(); // loop to write data data at the desired rate, data is being updated asynchronously // ref for accurate timeout: http://www.sitepoint.com/creating-accurate-timers-in-javascript sgTime += interval; sgDiff = Date.now() - sgStartTime - sgTime; if (running) { timerID = setTimeout(updateWaterfall, interval - sgDiff); } } function sgSetLineRate(newRate) { if (isNaN(newRate) || newRate > 50 || newRate < 0) { console.error("invalid line rate [0 <= lineRate < 50 lines/sec]"); // don't change the lineRate; } else if (newRate === 0) { // static (one pass) raster lineRate = 0; } else { lineRate = newRate; interval = 1000 / lineRate; // msec } } this.setLineRate = sgSetLineRate; function setProperty(propertyName, value) { if (typeof propertyName !== "string" || value === undefined) { // null is OK, forces default return; } switch (propertyName.toLowerCase()) { case "linerate": sgSetLineRate(value); // setLine does checks for number etc break; case "startbin": if (!isNaN(value) && value > 0) { startOfs = value; } break; case "onscreenparentid": if (typeof value === "string" && document.getElementById(value)) { demoCvsId = value; } break; case "colormap": if ( Array.isArray(value) && Array.isArray(value[0]) && value[0].length == 4 ) { colMap = value; // value must be an array of 4 element arrays to get here if (colMap.length < 256) { // fill out the remaining colors with last color for (let i = colMap.length; i < 256; i++) { colMap[i] = colMap[colMap.length - 1]; } } } break; default: break; } } function verticalNewLine() { let tmpImgData, ipBuf8; if (sgMode == "WF") { if (rhc) { // shift the current display down 1 line, oldest line drops off tmpImgData = offScreenCtx.getImageData(0, 0, pxPerLine, lines - 1); offScreenCtx.putImageData(tmpImgData, 0, 1); } else { // shift the current display up 1 line, oldest line drops off tmpImgData = offScreenCtx.getImageData(0, 1, pxPerLine, lines - 1); offScreenCtx.putImageData(tmpImgData, 0, 0); } } ipBuf8 = Uint8ClampedArray.from(ipBufAry[0]); for ( let sigVal, rgba, opIdx = 0, ipIdx = startOfs; ipIdx < pxPerLine + startOfs; opIdx += 4, ipIdx++ ) { sigVal = ipBuf8[ipIdx] || 0; // if input line too short add zeros rgba = colMap[sigVal]; // array of rgba values // byte reverse so number aa bb gg rr lineBuf8[opIdx] = rgba[0]; // red lineBuf8[opIdx + 1] = rgba[1]; // green lineBuf8[opIdx + 2] = rgba[2]; // blue lineBuf8[opIdx + 3] = rgba[3]; // alpha } offScreenCtx.putImageData(lineImgData, 0, nextLine); if (sgMode === "RS") { incrLine(); // if not static draw a white line in front of the current line to indicate new data point if (lineRate) { offScreenCtx.putImageData(blankImgData, 0, nextLine); } } } function horizontalNewLine() { let tmpImgData, ipBuf8; if (sgMode == "WF") { if (rhc) { // shift the current display right 1 line, oldest line drops off tmpImgData = offScreenCtx.getImageData(0, 0, lines - 1, pxPerLine); offScreenCtx.putImageData(tmpImgData, 1, 0); } else { // shift the current display left 1 line, oldest line drops off tmpImgData = offScreenCtx.getImageData(1, 0, lines - 1, pxPerLine); offScreenCtx.putImageData(tmpImgData, 0, 0); } } // refresh the page image (it was just shifted) pageImgData = offScreenCtx.getImageData(0, 0, lines, pxPerLine); if (ipBufAry[0].constructor !== Uint8Array) { ipBuf8 = Uint8ClampedArray.from(ipBufAry[0]); // clamp input values to 0..255 range } else { ipBuf8 = ipBufAry[0]; // conversion already done } for (let sigVal, rgba, opIdx, ipIdx = 0; ipIdx < pxPerLine; ipIdx++) { sigVal = ipBuf8[ipIdx + startOfs] || 0; // if input line too short add zeros rgba = colMap[sigVal]; // array of rgba values opIdx = 4 * ((pxPerLine - ipIdx - 1) * lines + nextLine); // byte reverse so number aa bb gg rr pageImgData.data[opIdx] = rgba[0]; // red pageImgData.data[opIdx + 1] = rgba[1]; // green pageImgData.data[opIdx + 2] = rgba[2]; // blue pageImgData.data[opIdx + 3] = rgba[3]; // alpha } if (sgMode === "RS") { incrLine(); // if not draw a white line in front of the current line to indicate new data point if (lineRate) { for (let j = 0; j < pxPerLine; j++) { if (rhc) { opIdx = 4 * (j * lines + nextLine); } else { opIdx = 4 * ((pxPerLine - j - 1) * lines + nextLine); } // byte reverse so number aa bb gg rr pageImgData.data[opIdx] = 255; // red pageImgData.data[opIdx + 1] = 255; // green pageImgData.data[opIdx + 2] = 255; // blue pageImgData.data[opIdx + 3] = 255; // alpha } } } offScreenCtx.putImageData(pageImgData, 0, 0); } const sgNewLine = vert ? verticalNewLine : horizontalNewLine; // function pointers //===== set all the options ================ for (let prop in opt) { // check that this is opt's own property, not inherited from prototype if (opt.hasOwnProperty(prop)) { setProperty(prop, opt[prop]); } } // ===== now make the exposed properties and methods =============== this.newLine = sgNewLine; this.offScreenCvs = document.createElement("canvas"); if (vert) { this.offScreenCvs.setAttribute("width", pxPerLine); // reset canvas pixels width this.offScreenCvs.setAttribute("height", lines); // don't use style for this clearImgData = new ImageData(clearBuf8, pxPerLine, lines); } // data written in columns else { this.offScreenCvs.setAttribute("width", lines); // reset canvas pixels width this.offScreenCvs.setAttribute("height", pxPerLine); // don't use style for this clearImgData = new ImageData(clearBuf8, lines, pxPerLine); } offScreenCtx = this.offScreenCvs.getContext("2d"); this.clear = function () { offScreenCtx.putImageData(clearImgData, 0, 0); }; this.start = function () { sgStartTime = Date.now(); sgTime = 0; running = true; updateWaterfall(); // start the update loop }; this.stop = function () { running = false; if (timerID) { clearTimeout(timerID); } // reset where the next line is to be written if (sgMode === "RS") { if (vert) { nextLine = rhc ? lines - 1 : 0; } else { nextLine = rhc ? 0 : lines - 1; } } // WF else { nextLine = rhc ? 0 : lines - 1; } }; // make a white line, it will show the input line for RS displays blankBuf8.fill(255); // make a full canvas of the color map 0 values for (let i = 0; i < pxPerLine * lines * 4; i += 4) { // byte reverse so number aa bb gg rr clearBuf8[i] = colMap[0][0]; // red clearBuf8[i + 1] = colMap[0][1]; // green clearBuf8[i + 2] = colMap[0][2]; // blue clearBuf8[i + 3] = colMap[0][3]; // alpha } // for diagnostics only if (typeof demoCvsId == "string") { document.getElementById(demoCvsId).appendChild(this.offScreenCvs); } // initialize the direction and first line position this.stop(); // everything is set // if dynamic, wait for the start or newLine methods to be called } })();