├── .gitignore ├── .eslintrc.json ├── package.json ├── readme.md ├── core.js ├── gl.js ├── qa.md ├── test.js └── 2d.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | ./bundle.js 5 | demo -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "commonjs": true, 6 | "es6": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "rules": { 10 | "strict": 2, 11 | "indent": 0, 12 | "linebreak-style": 0, 13 | "quotes": 0, 14 | "semi": 0, 15 | "no-cond-assign": 1, 16 | "no-constant-condition": 1, 17 | "no-duplicate-case": 1, 18 | "no-empty": 1, 19 | "no-ex-assign": 1, 20 | "no-extra-boolean-cast": 1, 21 | "no-extra-semi": 1, 22 | "no-fallthrough": 1, 23 | "no-func-assign": 1, 24 | "no-global-assign": 1, 25 | "no-implicit-globals": 2, 26 | "no-inner-declarations": ["error", "functions"], 27 | "no-irregular-whitespace": 2, 28 | "no-loop-func": 1, 29 | "no-multi-str": 1, 30 | "no-mixed-spaces-and-tabs": 1, 31 | "no-proto": 1, 32 | "no-sequences": 1, 33 | "no-throw-literal": 1, 34 | "no-unmodified-loop-condition": 1, 35 | "no-useless-call": 1, 36 | "no-void": 1, 37 | "no-with": 2, 38 | "wrap-iife": 1, 39 | "no-redeclare": 1, 40 | "no-unused-vars": ["error", { "vars": "all", "args": "none" }], 41 | "no-sparse-arrays": 1 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gl-spectrum", 3 | "version": "3.2.6", 4 | "description": "Display spectrum data with webgl", 5 | "main": "gl.js", 6 | "dependencies": { 7 | "a-weighting": "^1.1.0", 8 | "canvas-loop": "^1.0.7", 9 | "clamp": "^1.0.1", 10 | "color-interpolate": "^1.0.2", 11 | "color-parse": "^1.2.0", 12 | "color-rgba": "^1.1.0", 13 | "gl-util": "^1.1.6", 14 | "inherits": "^2.0.1", 15 | "is-browser": "^2.0.1", 16 | "is-plain-obj": "^1.1.0", 17 | "mumath": "^3.3.4", 18 | "object-assign": "^4.1.0", 19 | "pan-zoom": "^2.0.0", 20 | "plot-grid": "^2.4.2", 21 | "pretty-number": "^1.0.7" 22 | }, 23 | "devDependencies": { 24 | "app-audio": "^1.2.1", 25 | "audio-context": "^1.0.1", 26 | "bubleify": "^0.6.0", 27 | "color-alpha": "^1.0.2", 28 | "colormap": "^2.2.0", 29 | "decibels": "^1.0.1", 30 | "enable-mobile": "^1.0.7", 31 | "fourier-transform": "^1.1.2", 32 | "fps-indicator": "^1.1.0", 33 | "get-float-time-domain-data": "^0.1.0", 34 | "indexhtmlify": "^1.3.1", 35 | "insert-styles": "^1.2.1", 36 | "metadataify": "^1.0.2", 37 | "nice-color-palettes": "^1.0.1", 38 | "scijs-window-functions": "^2.0.2", 39 | "settings-panel": "^1.8.17", 40 | "spiral-2d": "^1.1.0", 41 | "tinycolor2": "^1.4.1" 42 | }, 43 | "scripts": { 44 | "test": "budo test", 45 | "test:browser": "budo test -- -g bubleify", 46 | "test:iphone": "budo test.js -- -g bubleify", 47 | "build": "browserify test.js -g bubleify | indexhtmlify | metadataify > demo/index.html" 48 | }, 49 | "repository": { 50 | "type": "git", 51 | "url": "git+https://github.com/dfcreative/gl-spectrum.git" 52 | }, 53 | "keywords": [ 54 | "gl", 55 | "spectrum", 56 | "audio", 57 | "a-weighting", 58 | "psychoacoustics", 59 | "acoustics", 60 | "audio-vis" 61 | ], 62 | "author": "ΔY ", 63 | "license": "MIT", 64 | "bugs": { 65 | "url": "https://github.com/dfcreative/gl-spectrum/issues" 66 | }, 67 | "homepage": "https://github.com/dfcreative/gl-spectrum#readme" 68 | } 69 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # gl-spectrum [![unstable](http://badges.github.io/stability-badges/dist/unstable.svg)](http://github.com/badges/stability-badges) 2 | 3 | Spectrum rendering component with webgl or context2d. 4 | 5 | [![Spectrum](https://raw.githubusercontent.com/dy/gl-spectrum/gh-pages/preview.png "Spectrum")](http://dy.github.io/gl-spectrum/) 6 | 7 | 8 | ## Usage 9 | 10 | [![npm install gl-spectrum](https://nodei.co/npm/gl-spectrum.png?mini=true)](https://npmjs.org/package/gl-spectrum/) 11 | 12 | ```js 13 | var Spectrum = require('gl-spectrum'); 14 | 15 | var spectrum = new Spectrum({ 16 | container: document.body, 17 | 18 | //if undefined, new canvas will be created 19 | canvas: null, 20 | 21 | //existing webgl-context and some context options 22 | context: null, 23 | alpha: false, 24 | 25 | //enable render on every frame, disable for manual rendering 26 | autostart: true, 27 | 28 | //visible range 29 | maxDb: 0, 30 | minDb: -100, 31 | maxFrequency: 20000, 32 | minFrequency: 20, 33 | sampleRate: 44100, 34 | 35 | //perceptual loudness weighting, 'a', 'b', 'c', 'd', 'itu' or 'z' (see a-weighting) 36 | weighting: 'itu', 37 | 38 | //display grid, can be an object with plot-grid settings 39 | grid: true, 40 | 41 | //place frequencies logarithmically 42 | log: true, 43 | 44 | //smooth series of data 45 | smoothing: 0.75, 46 | 47 | //0 - bottom, .5 - symmetrically, 1. - top 48 | align: 0, 49 | 50 | //peak highlight balance 51 | balance: .5, 52 | 53 | //display max value trail 54 | trail: true, 55 | 56 | //style of rendering: line, bar or fill 57 | type: 'line', 58 | 59 | //width of the bar, applicable only in bar mode 60 | barWidth: 2, 61 | 62 | //colormap for the levels of magnitude. Can be a single color for flat fill. 63 | palette: ['black', 'white'], 64 | 65 | //by default transparent, to draw waveform 66 | background: null, 67 | 68 | //pan and zoom to show detailed view 69 | interactions: false 70 | }); 71 | 72 | //pass values in decibels (-100...0 range) 73 | spectrum.set(magnitudes); 74 | 75 | //update style/options 76 | spectrum.update(options); 77 | 78 | //hook up every data set 79 | spectrum.on('data', (magnitudes, trail) => {}); 80 | 81 | //for manual mode of rendering you may want to call this whenever you feel right 82 | spectrum.render(); 83 | spectrum.draw(); 84 | ``` 85 | 86 | 87 | ## Related 88 | 89 | * [gl-waveform](https://github.com/dy/gl-waveform) 90 | * [gl-spectrogram](https://github.com/dy/gl-spectrogram) 91 | * [a-weighting](https://github.com/audio-lab/a-weighting) — perception loudness weighting for audio. 92 | * [colormap](https://github.com/bpostlethwaite/colormap) — list of js color maps. 93 | * [cli-visualizer](https://github.com/dpayne/cli-visualizer) — C++ spectrum visualizer. 94 | * [spectrum](https://github.com/mattdesl/spectrum) by mattdesl 95 | * [audioMotion](https://github.com/hvianna/audioMotion.js/) 96 | -------------------------------------------------------------------------------- /core.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module gl-spectrum/code 3 | */ 4 | 'use strict' 5 | 6 | const extend = require('object-assign') 7 | const inherits = require('inherits') 8 | const createGrid = require('plot-grid') 9 | const clamp = require('clamp') 10 | const Emitter = require('events') 11 | const weighting = require('a-weighting') 12 | const interpolate = require('color-interpolate') 13 | const pretty = require('pretty-number') 14 | const isPlainObj = require('is-plain-obj') 15 | const createLoop = require('canvas-loop') 16 | const db = require('decibels') 17 | const getContext = require('gl-util/context') 18 | const lg = Math.log10 19 | 20 | 21 | module.exports = Spectrum; 22 | 23 | 24 | inherits(Spectrum, Emitter); 25 | 26 | 27 | /** 28 | * @contructor 29 | */ 30 | function Spectrum (options) { 31 | if (!(this instanceof Spectrum)) return new Spectrum(options); 32 | 33 | Emitter.call(); 34 | 35 | extend(this, options); 36 | 37 | //create canvas/container 38 | //FIXME: this is not very good for 2d case though 39 | if (!this.context) this.context = getContext(this); 40 | if (!this.canvas) this.canvas = this.context.canvas; 41 | if (!this.container) this.container = document.body || document.documentElement; 42 | if (!this.canvas.parentNode) this.container.appendChild(this.canvas); 43 | 44 | //create loop 45 | this.loop = createLoop(this.canvas, {parent: this.container, scale: this.pixelRatio}); 46 | this.loop.on('tick', () => { 47 | this.render(); 48 | }); 49 | this.loop.on('resize', () => { 50 | this.update() 51 | }); 52 | 53 | this.autostart && this.loop.start(); 54 | 55 | //create data holder 56 | this.magnitudes = []; 57 | } 58 | 59 | 60 | Spectrum.prototype.pixelRatio = window.devicePixelRatio; 61 | Spectrum.prototype.autostart = true; 62 | Spectrum.prototype.className = 'gl-spectrum'; 63 | Spectrum.prototype.align = .5; 64 | Spectrum.prototype.trail = true; 65 | Spectrum.prototype.type = 'fill'; 66 | Spectrum.prototype.barWidth = 2; 67 | Spectrum.prototype.grid = true; 68 | Spectrum.prototype.maxDb = 0; 69 | Spectrum.prototype.minDb = -100; 70 | Spectrum.prototype.maxFrequency = 20000; 71 | Spectrum.prototype.minFrequency = 20; 72 | Spectrum.prototype.smoothing = 0.82; 73 | Spectrum.prototype.log = true; 74 | Spectrum.prototype.weighting = 'm'; 75 | Spectrum.prototype.sampleRate = 44100; 76 | Spectrum.prototype.palette = 'black'; 77 | Spectrum.prototype.levels = 32; 78 | Spectrum.prototype.balance = .5; 79 | Spectrum.prototype.trailAlpha = .33; 80 | Spectrum.prototype.interactions = false; 81 | Spectrum.prototype.background = null; 82 | 83 | 84 | /** 85 | * Set data 86 | */ 87 | Spectrum.prototype.set = function (data) { 88 | let halfRate = this.sampleRate * 0.5; 89 | let nf = halfRate / data.length; 90 | let weight = typeof this.weighting === 'string' ? weighting[this.weighting] : this.weighting; 91 | let smoothing = this.smoothing; 92 | let magnitudes = this.magnitudes; 93 | 94 | magnitudes.length = data.length; 95 | 96 | //apply weighting and clamping, bring db to 0..1 range 97 | let peak = 0; 98 | for (let i = 0; i < data.length; i++) { 99 | let v = data[i] 100 | if (weight) v += 20 * Math.log(weight(i * nf)) / Math.log(10); 101 | v = clamp(v, -100, 0) 102 | v = .01 * (v + 100); 103 | if (v > peak) peak = v; 104 | magnitudes[i] = v * (1 - smoothing) + (magnitudes[i] || 0) * smoothing; 105 | } 106 | 107 | this.peak = peak; 108 | 109 | if (this.trail) { 110 | if (!Array.isArray(this.trail)) { 111 | this.trail = magnitudes; 112 | this.trailPeak = this.peak; 113 | } 114 | else { 115 | this.trail.length = magnitudes.length; 116 | let trailPeak = 0; 117 | this.trail = magnitudes.map((v, i) => { 118 | v = Math.max(v, this.trail[i]); 119 | if (v > trailPeak) trailPeak = v; 120 | return v; 121 | }); 122 | this.trailPeak = trailPeak; 123 | } 124 | } 125 | 126 | this.emit('data', magnitudes, this.trail); 127 | 128 | !this.autostart && this.render(); 129 | 130 | return this; 131 | }; 132 | 133 | 134 | /** 135 | * Update options 136 | */ 137 | Spectrum.prototype.update = function (options) { 138 | if (!options) options = {}; 139 | extend(this, options); 140 | 141 | //create colormap from palette 142 | if (!Array.isArray(this.palette)) this.palette = [this.palette]; 143 | this.getColor = interpolate(this.palette); 144 | this.infoColor = this.getColor(.5); 145 | 146 | //limit base 147 | this.minFrequency = Math.max(20, this.minFrequency); 148 | this.maxFrequency = Math.min(this.sampleRate/2, this.maxFrequency); 149 | 150 | //set bg in case of slave draw mode 151 | if (this.background && this.alpha) { 152 | this.canvas.style.background = this.background; 153 | } 154 | 155 | //create grid if true/options passed 156 | if (this.grid === true || isPlainObj(this.grid)) { 157 | if (!this._grid) { 158 | this._grid = createGrid({ 159 | autostart: false, 160 | context: this.context, 161 | interactions: this.interactions, 162 | x: extend({ 163 | type: this.log ? 'logarithmic' : 'linear', 164 | minScale: 1e-10, 165 | origin: 0, 166 | axisOrigin: -Infinity, 167 | format: (v) => { 168 | // v = this.log ? Math.pow(10, v) : v; 169 | return pretty(v); 170 | } 171 | }, this.grid), 172 | // y: 'linear' 173 | }); 174 | 175 | this._grid.on('interact', (grid) => { 176 | if (!this.interactions) return; 177 | let x = grid.x; 178 | let leftF = x.offset; 179 | let rightF = x.offset + x.scale*this.canvas.width; 180 | if (this.log) { 181 | leftF = Math.pow(10, leftF); 182 | rightF = Math.pow(10, rightF); 183 | } 184 | 185 | this.update({minFrequency: leftF, maxFrequency: rightF}); 186 | }); 187 | } 188 | 189 | this.grid = this._grid; 190 | this._grid.update({x: {disabled: false}}); 191 | } 192 | else if (!this.grid && this._grid) { 193 | this._grid.update({x: {disabled: true}}); 194 | this._grid.draw(); 195 | } 196 | 197 | //update grid 198 | if (this.grid) { 199 | let xOpts = { 200 | color: this.getColor(.75), 201 | // lineColor: .05 202 | }; 203 | 204 | if (options.log != null) { 205 | xOpts.type = this.log ? 'logarithmic' : 'linear'; 206 | xOpts.min = this.log ? lg(20) : 0; 207 | xOpts.max = this.log ? lg(this.sampleRate/2) : this.sampleRate/2; 208 | } 209 | 210 | xOpts.offset = this.log ? lg(this.minFrequency) : this.minFrequency; 211 | xOpts.scale = (this.log ? lg(this.maxFrequency/this.minFrequency) : (this.maxFrequency - this.minFrequency)) / this.canvas.width; 212 | if (options.interactions) { 213 | xOpts.pan = xOpts.zoom = options.interactions; 214 | } 215 | 216 | //FIXME: add better decibels rendering 217 | // let yOpts = { 218 | // axisOrigin: Infinity, 219 | // origin: 0, 220 | // offset: -this.align*200, 221 | // scale: 200/height, 222 | // lineColor: false, 223 | // distance: 10, 224 | // color: this.getColor(.75), 225 | // format: v => { 226 | // return pretty(-100 + Math.abs(v)); 227 | // } 228 | // } 229 | 230 | this.grid.update({ 231 | x: xOpts, 232 | //y: yOpts 233 | }); 234 | } 235 | 236 | //reset trail if weight changed 237 | if (options.weighting != null) this.trail = !!this.trail; 238 | 239 | //emit update 240 | this.emit('update'); 241 | 242 | return this; 243 | }; 244 | -------------------------------------------------------------------------------- /gl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module gl-spectrum/gl 3 | */ 4 | 'use strict' 5 | 6 | const Spectrum = require('./core') 7 | const inherit = require('inherits') 8 | const rgba = require('color-rgba') 9 | const uniform = require('gl-util/uniform') 10 | const attribute = require('gl-util/attribute') 11 | const texture = require('gl-util/texture') 12 | const setProgram = require('gl-util/program') 13 | 14 | module.exports = GlSpectrum; 15 | 16 | 17 | inherit(GlSpectrum, Spectrum); 18 | 19 | function GlSpectrum (opts) { 20 | if (!(this instanceof GlSpectrum)) return new GlSpectrum(opts); 21 | 22 | Spectrum.call(this, opts); 23 | 24 | var gl = this.gl = this.context; 25 | 26 | this.program = setProgram(gl, this.vert, this.frag); 27 | 28 | texture(this.gl, 'position', {usage: gl.STREAM_DRAW, size: 2}) 29 | texture(this.gl, 'colormap', { 30 | type: gl.UNSIGNED_BYTE, 31 | format: gl.RGBA, 32 | filter: gl.LINEAR, 33 | wrap: gl.CLAMP_TO_EDGE, 34 | height: this.levels, 35 | width: 1 36 | }); 37 | 38 | this.on('data', (magnitudes, trail) => { 39 | this.positions = this.calcPositions(this.type, magnitudes); 40 | this.trailPositions = this.calcPositions('line', trail); 41 | }); 42 | 43 | this.on('update', () => { 44 | let gl = this.gl; 45 | 46 | setProgram(gl, this.program); 47 | 48 | //update uniforms 49 | uniform(gl, 'align', this.align, this.program); 50 | uniform(gl, 'balance', this.balance, this.program); 51 | uniform(gl, 'minFrequency', this.minFrequency, this.program); 52 | uniform(gl, 'maxFrequency', this.maxFrequency, this.program); 53 | uniform(gl, 'minDb', this.minDb, this.program); 54 | uniform(gl, 'maxDb', this.maxDb, this.program); 55 | uniform(gl, 'logarithmic', this.log ? 1 : 0, this.program); 56 | uniform(gl, 'sampleRate', this.sampleRate, this.program); 57 | uniform(gl, 'shape', [this.canvas.width, this.canvas.height], this.program); 58 | 59 | this.infoColorArr = rgba(this.infoColor); 60 | this.infoColorArr[3] *= this.trailAlpha; 61 | 62 | this.bgArr = rgba(this.background || 'white'); 63 | 64 | this.isFlat = this.palette.length === 1; 65 | 66 | if (this.isFlat) { 67 | this.colorArr = rgba(this.palette[0]); 68 | } 69 | 70 | let colormap = []; 71 | for (let i = 0; i < this.levels; i++) { 72 | let channels = rgba(this.getColor((i + .5)/this.levels), false); 73 | colormap.push(channels[0]) 74 | colormap.push(channels[1]) 75 | colormap.push(channels[2]) 76 | colormap.push(channels[3]*255) 77 | } 78 | texture(this.gl, 'colormap', colormap); 79 | }) 80 | 81 | this.update(); 82 | } 83 | 84 | 85 | 86 | GlSpectrum.prototype.antialias = true; 87 | GlSpectrum.prototype.alpha = false; 88 | GlSpectrum.prototype.premultipliedAlpha = true; 89 | GlSpectrum.prototype.preserveDrawingBuffer = false; 90 | GlSpectrum.prototype.depth = false; 91 | GlSpectrum.prototype.stencil = false; 92 | 93 | 94 | /** 95 | * Recalculate number of verteces 96 | */ 97 | GlSpectrum.prototype.calcPositions = function (type, magnitudes) { 98 | if (!magnitudes) magnitudes = this.magnitudes; 99 | 100 | var positions = [], l = magnitudes.length; 101 | 102 | //creating vertices every time is not much slower than 103 | if (type === 'line') { 104 | for (let i = 0; i < l; i++) { 105 | positions[i*2] = i/l; 106 | positions[i*2 + 1] = magnitudes[i]; 107 | } 108 | for (let i = l-1, j = 0; j < l; i--, j++) { 109 | positions[l*2 + j*2] = i/l; 110 | positions[l*2 + j*2 + 1] = -magnitudes[i]; 111 | } 112 | } 113 | else if (type === 'fill') { 114 | for (let i = 0; i < l; i++) { 115 | positions.push(i/l); 116 | positions.push(magnitudes[i]); 117 | positions.push(i/l); 118 | positions.push(-magnitudes[i]); 119 | } 120 | } 121 | else { 122 | let w = this.barWidth / this.canvas.width; 123 | 124 | for (let i = 0; i < l; i++) { 125 | let x = i/l; 126 | 127 | //left break 128 | positions.push(x); 129 | positions.push(magnitudes[i]); 130 | positions.push(x); 131 | positions.push(magnitudes[i]); 132 | 133 | //bar square 134 | positions.push(x); 135 | positions.push(magnitudes[i]); 136 | positions.push(x + w); 137 | positions.push(magnitudes[i]); 138 | positions.push(x); 139 | positions.push(-magnitudes[i]); 140 | positions.push(x + w); 141 | positions.push(-magnitudes[i]); 142 | 143 | //right break 144 | positions.push(x + w); 145 | positions.push(-magnitudes[i]); 146 | positions.push(x + w); 147 | positions.push(-magnitudes[i]); 148 | } 149 | } 150 | 151 | return positions; 152 | }; 153 | 154 | 155 | GlSpectrum.prototype.render = function () { 156 | this.gl.viewport(0, 0, this.canvas.width, this.canvas.height); 157 | if (!this.alpha) { 158 | let bg = this.bgArr; 159 | this.gl.clearColor(...bg); 160 | this.gl.clear(this.gl.COLOR_BUFFER_BIT); 161 | } 162 | this.emit('render') 163 | this.draw(); 164 | } 165 | 166 | 167 | /** 168 | * Render main loop 169 | */ 170 | GlSpectrum.prototype.draw = function () { 171 | setProgram(this.gl, this.program); 172 | 173 | let gl = this.gl; 174 | 175 | if (this.positions) { 176 | uniform(this.gl, 'alpha', 1, this.program); 177 | uniform(this.gl, 'peak', this.peak, this.program); 178 | uniform(this.gl, 'flatFill', this.isFlat ? 1 : 0, this.program); 179 | attribute(this.gl, 'position', this.positions); 180 | if (this.isFlat) uniform(this.gl, 'color', this.colorArr, this.program); 181 | 182 | //draw fill 183 | if (this.type === 'line') { 184 | gl.drawArrays(gl.LINE_STRIP, 0, this.positions.length/2); 185 | } 186 | else { 187 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, this.positions.length/2); 188 | } 189 | 190 | //draw trail 191 | if (this.trail) { 192 | uniform(this.gl, 'alpha', this.trailAlpha, this.program); 193 | uniform(this.gl, 'flatFill', this.isFlat ? 1 : 0, this.program); 194 | uniform(this.gl, 'balance', this.balance*.5, this.program); 195 | if (this.isFlat) uniform(this.gl, 'color', this.infoColorArr, this.program); 196 | uniform(this.gl, 'peak', this.trailPeak, this.program); 197 | attribute(this.gl, 'position', this.trailPositions); 198 | gl.drawArrays(gl.LINE_STRIP, 0, this.trailPositions.length/2); 199 | uniform(this.gl, 'balance', this.balance, this.program); 200 | } 201 | } 202 | 203 | //draw grid 204 | if (this.grid) { 205 | this.grid.draw(); 206 | } 207 | 208 | return this; 209 | }; 210 | 211 | 212 | 213 | 214 | //vertex shader applies alignment mapping 215 | GlSpectrum.prototype.vert = ` 216 | precision highp float; 217 | 218 | attribute vec2 position; 219 | uniform float align; 220 | uniform float peak; 221 | uniform float minFrequency; 222 | uniform float maxFrequency; 223 | uniform float minDb; 224 | uniform float maxDb; 225 | uniform float logarithmic; 226 | uniform float sampleRate; 227 | 228 | varying float vIntensity; 229 | 230 | float decide (float a, float b, float w) { 231 | return step(0.5, w) * b + step(w, 0.5) * a; 232 | } 233 | 234 | float f (float ratio) { 235 | float halfRate = sampleRate * .5; 236 | float minF = max(minFrequency, 1e-5); 237 | float leftF = minF / halfRate; 238 | float rightF = maxFrequency / halfRate; 239 | 240 | ratio = (ratio - leftF) / (rightF - leftF); 241 | 242 | float logRatio = ratio * (maxFrequency - minF) + minF; 243 | 244 | logRatio = log(logRatio/minF) / log(maxFrequency/minF); 245 | 246 | ratio = decide(ratio, logRatio, logarithmic); 247 | 248 | return clamp(ratio, 0., 1.); 249 | } 250 | 251 | void main () { 252 | vIntensity = abs(position.y)/peak; 253 | 254 | gl_Position = vec4( 255 | f(position.x) * 2. - 1., 256 | (align * 2. - 1.) + step(0., position.y) * position.y * (1. - align) + step(position.y, 0.) * position.y * align, 257 | 0, 1); 258 | } 259 | `; 260 | 261 | 262 | 263 | GlSpectrum.prototype.frag = ` 264 | precision highp float; 265 | 266 | uniform sampler2D colormap; 267 | uniform vec2 shape; 268 | uniform float align; 269 | uniform float peak; 270 | uniform float balance; 271 | uniform float flatFill; 272 | uniform vec4 color; 273 | uniform float alpha; 274 | 275 | varying float vIntensity; 276 | 277 | void main () { 278 | if (flatFill > 0.) { 279 | gl_FragColor = color; 280 | } 281 | 282 | else { 283 | vec2 coord = gl_FragCoord.xy / shape; 284 | 285 | float dist = abs(coord.y - align); 286 | float amt = dist/peak; 287 | 288 | float intensity = pow((amt + dist)*.5 + .5, .8888) * balance + pow(vIntensity, 2.) * (1. - balance); 289 | intensity = clamp(intensity, 0., 1.); 290 | 291 | vec4 color = texture2D(colormap, vec2(coord.x, intensity)); 292 | color.a *= alpha; 293 | 294 | gl_FragColor = color; 295 | } 296 | } 297 | `; 298 | -------------------------------------------------------------------------------- /qa.md: -------------------------------------------------------------------------------- 1 | ## Q: what is good API for trail/type/channels in webgl? 2 | 1. Render by passing color, style and data to render method. 3 | - need to introduce `dashes` type to render trail for bars 4 | - forces user to pick combination for dash/bar, because type does not reflect complex style 5 | + possible to easily do shift param 6 | + easy to render additional channel 7 | - churns frequenciesData more often than required - each renderframe 8 | 2. Track freq (possibly chanelled) and trails. 9 | + complex and easy to get-going solution (no need to care about style match) 10 | + less frequent updates of data 11 | - some possible difficulty with channels 12 | + best way seems to be hooking up second spectrum on the same canvas 13 | 14 | ## Q: how to make single-line spectrum? 15 | 1. We could provide specific colormap with only >0.9 black. 16 | - too worrisome 17 | 2. We could provide separate program. 18 | + We could set a separate optional gradient param, that would work for single-color bars too. 19 | 20 | ## Q: how to make bars? 21 | 1. We could set grouping parameter. 22 | 2. We could delegate to a separate program. 23 | - Yet requires the size of the bar 24 | + That is solvable with mask 25 | 26 | ## Q: what is designation of mask in line-mode? 27 | * Ideally - map line value, instead of smoothstep. So we need thin mask. 28 | + That would replace kernel and make it actual. 29 | 30 | ## Q: how do we make bar’s gradient? 31 | * If we separate it to a program 32 | + We could provide optional enabled gradient via param. Related with line-only style. 33 | 34 | ## Q: how to make dots? 35 | * If we do separate program 36 | + Then it is just a way of treating mask - via cycle. 37 | 38 | ## Q: how do we define mask size? 39 | * Via user input mask size. That is also weight for grouping/averaging. 40 | 41 | ## Q: how to make glow? 42 | * Via mask. Define custom kernel. 43 | 44 | ## Q: how to make symmetrical view? 45 | * Via program mode: symmetrical:true 46 | 47 | ## Q: what should be the gradient of symmetrical view? 48 | * I guess like in line mode, but from the center. It is ok. 49 | 50 | ## Q: how do we interpolate inter-frequencies? Should we? 51 | + That would allow for natural nearest/average/linear/smooth views. 52 | * That might be somehow related with bar grouping. And as far bar grouping might be related with mask, we can... interpolate by mask averaging? 53 | 54 | ## Q: how do we do fill-only style? 55 | * Optinal `fill` or `gradient` param I guess. Related with line-only style. 56 | 57 | ## Q: how do we define style of filling gradient? 58 | * We could do gradient-mask, or gradient kernel - a set of values representing gradient. 59 | - It reserves texture spot for really unimportant task. At most - use uniform array. Like, 3 values or 5 values. 60 | + That is natural for dots/bar style. 61 | - We could provide linear grad and let user scale it to log or in some custom way himself. Like reserve values from 0.1 to 0.9 for fill gradient. 62 | 63 | ## Q: how do we make max/shadow frequencies? 64 | * I guess each program style has it’s own shadow frequency view. 65 | * For bars it is single mask size 66 | * For dots it is single mask as well 67 | * For lines it is ...shadow line? 68 | 69 | ## Q: what is the API of shadow frequencies? 70 | 1. We can set a delay size, related with smoothing param. 71 | - it definitely should be settable to false, so we need `shadow` or `trail` param. 72 | 2. We could pass a separate set of frequencies 73 | - a texture again... 74 | + To avoid texture squatting we could place all technical textures into a sprite. 75 | + And delegate it to gl-component. 76 | - We decided to avoid sprites for now - they require size uniforms and uneasy handling in shaders - there are no API for them. It is easier just to create another webgl instance. 77 | 3. We sould switch mode to line and draw freqs 78 | - that would require extra-step of sending textures to GPU. 79 | 80 | ## Q: how can we avoid sending trail frequencies to GPU? 81 | 1. we could render to a separate texture 82 | + anyways that texture is reserved 83 | ? is it better that calculating in CPU? 84 | + it is parallel 85 | - it switches program 86 | + it does not read texture back, and API is simple 87 | * trail: false, 1, 2, 5, 10, etc. 88 | - we don’t smooth trail 89 | + due to smoothing main frequencies we don’t need to smooth trail 90 | - we have not to only calc trail, but to paint it as well. Therefore, it anyways should be a separate program with it’s own buffer corresponding to line frequencies. 91 | 2. we could use multibuffers 92 | - worrisome 93 | - badly supported 94 | 95 | ## Q: how trail painting program should calculate average? 96 | 1. Passing averaged uniform. 97 | 2. Keeping buffer only. Actually we don’t use frequs uniform, only to scale verteces buffer. We could just easily precalc buffer in CPU (not parallel but simplier and allows for averaging at the same time, also use bufferSubData instead of uniformi, saves texture spot. 98 | + bufferSubData is ~5 times faster than texImage2D = setting two buffers is faster than one texture 99 | + CPU processing allows for log-normalize branching 100 | + CPU allows for dynamic details control - no need to create extra verteces 101 | * therefore place frequencies and trail in 2 buffers and that is it 102 | + or even better - place frequencies and trail in element buffers and call referenced drawElements 103 | - calc log mapping in CPU is non-parallel, and one idle mapping 4 times slower than texImage2D, therefore use textures and for lines just create a separate buffer. 104 | * We have to do separate bars style, which just sets the verteces layout 105 | + that would allow for combining continuous line view and discrete 106 | + that has continuous regulation scale 107 | 108 | ## Q: how do we make x-colormap? 109 | * Well setting gradient mask a texture would allow for that... Rarely one needs to colorize line, usually just gradient - like phase etc. Basically - mapped colorspace. 110 | 111 | ## Q: how do we do background image not-plain color? 112 | * Do colormap with 0=transparent. 113 | * Place proper gradientmap? 114 | 115 | ## Q: should we keep colormap when there are gradient? 116 | * we could just pick extreme gradient values and that’s it. 117 | * better leave colormap but make it 2d-able. 118 | 119 | ## Q: how do we organize bg rendering? 120 | 1. That would be nice to have a simple way to set bg to 0-level of colormap. 121 | * Therefore, bg should be rendered along by a single component. 122 | 2. We could combine it in a single component 123 | * then we would have to update the gl-component API to include multiple programs, buffers etc. 124 | - that is lazy for me. 125 | - reserving gl-component for a single program is quite nice and simple practice. 126 | + that would allow for avoiding managing reserved texture spots. 127 | 128 | ## Q: should we render bg as a separate component, but include it in gl-spectrum? 129 | + That would allow for reusability in gl-spectrum, gl-waveform etc. 130 | + That would remove need in combining multiple buffers in single component, we in that case keep things discreet. 131 | 132 | ## Q: how can we organize a single component combining any type of audio-info: spectrum, waveform or spectrogram? 133 | * call it audio-stats and render anything by a chosen type. 134 | + That would allow for persistent style across selected components. 135 | 136 | ## Q: will there be a problem as a result of re-including gl-background in gl-spectrum, gl-waveform etc? 137 | * should not, it should be a single component. 138 | 139 | ## Q: what is the strategy of mapping log frequencies through vertex shader? 140 | * we should provide freq’s texture as is (linear), and take current vertex coord.x as normal frequency value. 141 | * Therefore, vertex values should be properly scaled - log and subview 142 | 143 | ## Q: should our freq texture contain all the freq’s or only subrange? 144 | * all freq’s is clearer, but requires subview recalc within the shader in case 145 | * ? how do we do this case, how should we map verteces? 146 | * 0-vertex should be mapped to 0.2 texture for one. 1-vertex to 0.9. How? 147 | * ideally we should do vertex.x*.5 + .5 and obtain proper texture value. 148 | - but in this case we force picking interpolated frequency, because freq array should be subviewed for that case. 149 | + though the freq texture contains 0..1 subview values. 150 | * if we subview frequency in vert, 151 | + that is less calcs, and also parallel, which is faster than CPU 152 | - that forces conditioning in shader - decision whether log or real subview 153 | * subview is mapped to 0..1 range, but the fill x-colors are shifted with changing subview. 154 | * ✔ So we map in vertex shaders, conditionless. Pass whole texture as is. 155 | 156 | ## Q: how is it possible - to avoid subviewing in CPU with falsy interpolation - and at the same time avoid log condition in shader? 157 | * we have to equalize lg(minF) + ratio * (lg(maxF) - lg(minF)) and minF + ratio * (maxF - minF). How to make lg(f) function which with some argument returns f? 158 | * simple. y = step(0., isLog) * log(x) + x * step(isLog, x) / log(10) ... 159 | 160 | ## Q: what is the best customization for fill? 161 | * Basically we need min level, the range, taking into account align, also magnitude is pretty useful visually. 162 | * Forcing fillRange to start with 0, which is required for image-fill, is not very cool and creates all the troubles. Sticking to the cool fillRange regardless of image as a fill is 163 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | require('enable-mobile') 2 | const Spectrum = require('./gl'); 3 | const isBrowser = require('is-browser'); 4 | const db = require('decibels'); 5 | const colormap = require('colormap'); 6 | const colorScales = require('colormap/colorScales'); 7 | const appAudio = require('app-audio'); 8 | const ctx = require('audio-context')(); 9 | const insertCss = require('insert-styles'); 10 | const isMobile = require('is-mobile')(); 11 | const Color = require('tinycolor2'); 12 | const createFps = require('fps-indicator'); 13 | const createSettings = require('settings-panel') 14 | const theme = require('settings-panel/theme/typer') 15 | const fft = require('fourier-transform'); 16 | const alpha = require('color-alpha'); 17 | const blackman = require('scijs-window-functions/blackman-harris'); 18 | let palettes = require('nice-color-palettes'); 19 | 20 | 21 | let colormaps = {}; 22 | 23 | for (var name in colorScales) { 24 | if (name === 'alpha') continue; 25 | if (name === 'hsv') continue; 26 | if (name === 'rainbow') continue; 27 | if (name === 'rainbow-soft') continue; 28 | if (name === 'phase') continue; 29 | 30 | colormaps[name] = colormap({ 31 | colormap: colorScales[name], 32 | nshades: 16, 33 | format: 'rgbaString' 34 | }); 35 | palettes.push(colormaps[name]); 36 | } 37 | 38 | palettes = palettes 39 | //filter not readable palettes 40 | .filter((palette) => { 41 | return Color.isReadable(palette[0], palette.slice(-1)[0], { 42 | level:"AA", size:"large" 43 | }); 44 | }); 45 | 46 | 47 | insertCss(` 48 | select option { 49 | -webkit-appearance: none; 50 | appearance: none; 51 | display: block; 52 | background: white; 53 | position: absolute; 54 | } 55 | `); 56 | 57 | 58 | //show framerate 59 | let fps = createFps(); 60 | fps.element.style.color = theme.palette[0]; 61 | fps.element.style.fontFamily = theme.fontFamily; 62 | fps.element.style.fontWeight = 500; 63 | fps.element.style.fontSize = '12px'; 64 | fps.element.style.marginTop = '1rem'; 65 | fps.element.style.marginRight = '1rem'; 66 | 67 | 68 | 69 | var analyser; 70 | var audio = appAudio({ 71 | context: ctx, 72 | token: '6b7ae5b9df6a0eb3fcca34cc3bb0ef14', 73 | // source: './Liwei.mp3', 74 | source: 'https://soundcloud.com/egroove/premiere-jazzuelle-music-of-the-spheres-get-physical', 75 | // source: 'https://soundcloud.com/wooded-events/wooded-podcast-cinthie', 76 | // source: 'https://soundcloud.com/compost/cbls-362-compost-black-label-sessions-tom-burclay', 77 | // source: isMobile ? './sample.mp3' : 'https://soundcloud.com/vertvrecords/trailer-mad-rey-hotel-la-chapelle-mp3-128kbit-s', 78 | // source: isMobile ? './sample.mp3' : 'https://soundcloud.com/robbabicz/rbabicz-lavander-and-the-firefly', 79 | // source: 'https://soundcloud.com/einmusik/einmusik-live-watergate-4th-may-2016', 80 | // source: 'https://soundcloud.com/when-we-dip/atish-mark-slee-manjumasi-mix-when-we-dip-062', 81 | // source: 'https://soundcloud.com/dark-textures/dt-darkambients-4', 82 | // source: 'https://soundcloud.com/deep-house-amsterdam/diynamic-festival-podcast-by-kollektiv-turmstrasse', 83 | }).on('load', (node) => { 84 | analyser = audio.context.createAnalyser(); 85 | analyser.smoothingTimeConstant = 0; 86 | analyser.fftSize = 4096; 87 | analyser.minDecibels = -100; 88 | analyser.maxDecibels = 0; 89 | 90 | node.disconnect(); 91 | node.connect(analyser); 92 | analyser.connect(audio.context.destination); 93 | 94 | // setTimeout(upd, 100); 95 | // setTimeout(upd, 200); 96 | // setTimeout(upd, 300); 97 | // setTimeout(upd, 1000); 98 | }); 99 | 100 | audio.element.style.fontFamily = theme.fontFamily; 101 | audio.element.style.fontSize = theme.fontSize; 102 | audio.update(); 103 | 104 | 105 | 106 | 107 | var spectrum = new Spectrum({ 108 | // autostart: false, 109 | interactions: true, 110 | // log: false, 111 | // align: .5, 112 | // fill: colormap, 113 | // grid: true, 114 | // minFrequency: 20, 115 | // maxFrequency: 20000, 116 | // logarithmic: true, 117 | smoothing: 0.5, 118 | // maxDecibels: 0, 119 | // align: .5, 120 | // trail: 38, 121 | // autostart: false, 122 | // balance: .5, 123 | // antialias: true, 124 | // fill: [1,1,1,0], 125 | // fill: './images/stretch.png', 126 | // type: 'bar', 127 | // barWidth: 1, 128 | // weighting: 'z', 129 | // background: [27/255,0/255,37/255, 1], 130 | //background: [1,0,0,1]//'./images/bg-small.jpg' 131 | // viewport: function (w, h) { 132 | // return [50,20,w-70,h-60]; 133 | // } 134 | }).on('render', upd) 135 | 136 | spectrum.grid.update({x: {fontFamily: theme.fontFamily, fontSize: '10px'}}); 137 | // setInterval(upd, 100) 138 | 139 | 140 | function upd () { 141 | if (!analyser) return; 142 | 143 | // var waveform = new Float32Array(analyser.fftSize); 144 | // analyser.getFloatTimeDomainData(waveform); 145 | 146 | // dbMagnitudes = fft(waveform.map((v, i) => v*blackman(i, waveform.length))); 147 | // dbMagnitudes = dbMagnitudes.map((f, i) => db.fromGain(f)); 148 | 149 | var dbMagnitudes = new Float32Array(analyser.frequencyBinCount); 150 | analyser.getFloatFrequencyData(dbMagnitudes); 151 | 152 | spectrum.set(dbMagnitudes); 153 | } 154 | 155 | // spectrum.render(); 156 | 157 | // createColormapSelector(spectrum); 158 | 159 | // test('line webgl'); 160 | // test('bars 2d'); 161 | // test('node'); 162 | // test('viewport'); 163 | // test('clannels'); 164 | // test('classic'); 165 | // test('bars'); 166 | // test('bars line'); 167 | // test('dots'); 168 | // test('dots line'); 169 | // test('colormap (heatmap)'); 170 | // test('multilayered (max values)'); 171 | // test('line'); 172 | // test('oscilloscope'); 173 | 174 | let settings = createSettings([ 175 | {id: 'type', type: 'select', options: ['line', 'bar', 'fill'], value: spectrum.type, change: v => spectrum.update({type: v})}, 176 | // {id: 'align', label: '↕', title: 'align', type: 'range', min: 0, max: 1, value: spectrum.align, change: v => spectrum.update({align: v})}, 177 | // {id: 'smoothing', label: '~', title: 'smoothing', type: 'range', min: 0, max: 1, value: spectrum.smoothing, change: v => spectrum.update({smoothing: v})}, 178 | {type: 'raw', label: 'palette', id: 'palette', style: ``, content: function (data) { 179 | let el = document.createElement('div'); 180 | el.className = 'random-palette'; 181 | el.style.cssText = ` 182 | width: 1.5em; 183 | height: 1.5em; 184 | background-color: rgba(120,120,120,.2); 185 | margin-left: 0em; 186 | display: inline-block; 187 | vertical-align: middle; 188 | cursor: pointer; 189 | `; 190 | el.title = 'Randomize palette'; 191 | let settings = this.panel; 192 | setColors(el, spectrum.palette, settings.theme.active); 193 | 194 | el.onclick = () => { 195 | // settings.set('colors', 'custom'); 196 | let palette = palettes[Math.floor((palettes.length - 1) * Math.random())]; 197 | 198 | if (Math.random() > .5) palette = palette.reverse(); 199 | 200 | setColors(el, palette); 201 | } 202 | 203 | //create colors in the element 204 | function setColors(el, palette, active) { 205 | spectrum.update({ 206 | background: palette.length > 1 ? palette[palette.length - 1] : 'white', 207 | palette: palette.slice().reverse() 208 | }); 209 | 210 | let bg = palette.length > 1 ? spectrum.getColor(0.) : 'white'; 211 | 212 | settings.update({ 213 | style: `background-color: ${alpha(bg, .5)}; 214 | box-shadow: 0 0 0 2px ${alpha(spectrum.getColor(0.5), .1)};` 215 | }); 216 | if (palette.length > 1) { 217 | settings.update({ 218 | palette: palette, 219 | }); 220 | } 221 | 222 | audio.update({color: palette[0]}); 223 | fps.element.style.color = spectrum.palette[palette.length-1]; 224 | 225 | el.innerHTML = ''; 226 | if (active) { 227 | palette = palette.slice(); 228 | palette.unshift(active); 229 | } 230 | for (var i = 0; i < 3; i++) { 231 | let colorEl = document.createElement('div'); 232 | el.appendChild(colorEl); 233 | colorEl.className = 'random-palette-color'; 234 | colorEl.style.cssText = ` 235 | width: 50%; 236 | height: 50%; 237 | float: left; 238 | background-color: ${palette[i] || 'transparent'} 239 | `; 240 | } 241 | } 242 | return el; 243 | }}, 244 | {id: 'log', type: 'checkbox', value: spectrum.log, change: v => spectrum.update({log: v}) 245 | }, 246 | {id: 'grid', type: 'checkbox', value: spectrum.grid, change: v => spectrum.update({grid: v}) 247 | }, 248 | {id: 'weighting', label: 'weighting', title: 'Weighting', type: 'select', options: ['a', 'b', 'c', 'd', 'm', 'z'], 249 | value: spectrum.weighting, 250 | change: (value) => { 251 | spectrum.update({weighting: value, trail: true}) 252 | } 253 | }, 254 | {id: 'trail', label: 'trail', type: 'checkbox', value: !!spectrum.trail, change: v => spectrum.update({trail: v}) 255 | } 256 | ],{ 257 | title: 'gl-spectrum', 258 | theme: theme, 259 | fontSize: 12, 260 | palette: ['black', 'white'], 261 | css: ` 262 | :host { 263 | z-index: 1; 264 | position: fixed; 265 | left: 1rem; 266 | max-width: 100vw; 267 | white-space: nowrap; 268 | bottom: 2.5rem; 269 | width: auto; 270 | background-color: transparent; 271 | padding: .5rem 1rem; 272 | border-radius: 0; 273 | } 274 | .settings-panel-title { 275 | width: auto; 276 | display: inline-block; 277 | line-height: 1; 278 | padding: .5rem 0; 279 | vertical-align: baseline; 280 | } 281 | .settings-panel-field { 282 | width: auto; 283 | vertical-align: top; 284 | display: inline-block; 285 | margin-right: 0; 286 | margin-left: 1rem; 287 | } 288 | .settings-panel-label { 289 | width: auto!important; 290 | } 291 | ` 292 | }); 293 | -------------------------------------------------------------------------------- /2d.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simplified 2d version of spectrum 3 | */ 4 | 5 | var Spectrum = require('./lib/core'); 6 | var clamp = require('mumath/clamp'); 7 | var mix = require('mumath/mix'); 8 | var spiral = require('spiral-2d'); 9 | 10 | 11 | module.exports = Spectrum; 12 | 13 | Spectrum.prototype.context = '2d'; 14 | 15 | 16 | //return color based on current palette 17 | Spectrum.prototype.getColor = function (ratio) { 18 | var cm = this.fillData; 19 | ratio = clamp(ratio, 0, 1); 20 | var idx = ratio*(cm.length*.25 - 1); 21 | var amt = idx % 1; 22 | var left = cm.slice(Math.floor(idx)*4, Math.floor(idx)*4 + 4); 23 | var right = cm.slice(Math.ceil(idx)*4, Math.ceil(idx)*4 + 4); 24 | var values = left.map((v,i) => (v * (1 - amt) + right[i] * amt)|0 ); 25 | return values; 26 | } 27 | 28 | 29 | //generic draw 30 | Spectrum.prototype.draw = function () { 31 | var ctx = this.context; 32 | 33 | var type = ''+this.type; 34 | var isLine = /line/.test(type); 35 | var isFill = /fill/.test(type); 36 | var isBar = /bar/.test(type); 37 | 38 | ctx.clearRect.apply(ctx,this.viewport); 39 | 40 | this.drawSpiral(this.magnitudes); 41 | 42 | // if (isLine) return this.drawLine(this.magnitudes); 43 | // else if (isBar) return this.drawBar(this.magnitudes); 44 | // return this.drawFill(); 45 | }; 46 | 47 | 48 | Spectrum.prototype.drawSpiral = function (data) { 49 | var ctx = this.context; 50 | var width = this.viewport[2], 51 | height = this.viewport[3]; 52 | var center = [width*.5, height*.5]; 53 | var balance = .5; 54 | 55 | var type = ''+this.type; 56 | var isLine = /line/.test(type); 57 | var isFill = /fill/.test(type); 58 | var isBar = /bar/.test(type); 59 | 60 | 61 | //log spiral 62 | if (this.logarithmic) { 63 | var startAngle = Math.PI * Math.log10(this.minFrequency) * 2; 64 | var endAngle = Math.PI * Math.log10(this.maxFrequency) * 2; 65 | 66 | if (width > height) { 67 | var b = spiral.logarithmic.b(height*.4, endAngle + Math.PI, 1); 68 | } 69 | else { 70 | var b = spiral.logarithmic.b(width*.4, endAngle + Math.PI, 1); 71 | } 72 | 73 | 74 | //create spiral shape to fill 75 | var ratio = 0, outerAmp = 0, innerAmp = 0, amp = 0, nf, f, x, offset, pi2 = Math.PI * 2; 76 | // var gradient = ctx.createLinearGradient(this.viewport[0],0,width,0); 77 | // gradient.addColorStop(0, `rgba(${this.getColor(0.5)})`) 78 | 79 | var innerPoints = []; 80 | var outerPoints = []; 81 | 82 | var coords = spiral.logarithmic(center, startAngle, 1, b); 83 | 84 | for (var i = 0; i < data.length; i++) { 85 | nf = (i + .5) / data.length; 86 | f = this.unf(nf); 87 | 88 | angle = f * (endAngle - startAngle) + startAngle; 89 | 90 | amp = data[i]; 91 | relativeAmp = (amp + 100) / (this.peak + 100); 92 | amp = clamp((amp - this.minDecibels) / (this.maxDecibels - this.minDecibels), 0, 1); 93 | 94 | ratio = (angle - startAngle) / (endAngle - startAngle); 95 | innerAmp = spiral.radius(angle, 1, b) - spiral.radius(angle - pi2, 1, b); 96 | outerAmp = spiral.radius(angle + pi2, 1, b) - spiral.radius(angle, 1, b); 97 | 98 | var outerRadius = amp * (outerAmp) * this.align; 99 | var innerRadius = amp * (innerAmp) * (1 - this.align); 100 | var coord = spiral.logarithmic(center, angle, 1, b); 101 | var from = [ 102 | coord[0] - innerRadius * Math.cos(angle), 103 | coord[1] - innerRadius * Math.sin(angle) 104 | ]; 105 | var to = [ 106 | coord[0] + outerRadius * Math.cos(angle), 107 | coord[1] + outerRadius * Math.sin(angle) 108 | ]; 109 | 110 | // ctx.beginPath(); 111 | // ctx.moveTo(from[0], from[1]); 112 | // ctx.lineTo(to[0], to[1]); 113 | // ctx.strokeStyle = `rgba(${this.getColor( amp*balance + relativeAmp*(1 - balance) )})`; 114 | // ctx.stroke(); 115 | 116 | innerPoints.push(from); 117 | outerPoints.push(to); 118 | } 119 | 120 | //fill shape 121 | ctx.beginPath(); 122 | var points = innerPoints.concat(outerPoints.reverse()); 123 | ctx.moveTo(points[0][0], points[0][1]); 124 | points.forEach((coord) => { 125 | ctx.lineTo(coord[0], coord[1]); 126 | }); 127 | ctx.closePath(); 128 | ctx.fillStyle = `rgba(${this.getColor(1)})`; 129 | ctx.strokeStyle = `rgba(${this.getColor(1)})`; 130 | ctx.fill(); 131 | // ctx.stroke(); 132 | 133 | 134 | //paint spiral curve in canvas 135 | // ctx.beginPath(); 136 | // var coords = spiral.logarithmic(center, startAngle, 1, b); 137 | // ctx.moveTo(coords[0], coords[1]); 138 | 139 | // //draw spiral 140 | // for (var angle = startAngle; angle <= endAngle; angle+=0.1) { 141 | // coords = spiral.logarithmic(center, angle, 1, b); 142 | // ctx.lineTo(coords[0], coords[1]); 143 | // } 144 | // ctx.lineWidth = this.width; 145 | // ctx.strokeStyle = `rgba(${this.getColor(1)})`; 146 | // ctx.stroke(); 147 | 148 | } 149 | 150 | //archimedean spiral 151 | else { 152 | if (width > height) { 153 | var b = spiral.archimedean.b(width*.5, Math.PI * 4, 0); 154 | } 155 | else { 156 | var b = spiral.archimedean.b(height*.5, Math.PI * 4, 0); 157 | } 158 | 159 | //paint spiral curve in canvas 160 | ctx.beginPath(); 161 | ctx.moveTo(center[0], center[1]); 162 | for (var angle = 0; angle <= Math.PI * 4; angle+=0.01) { 163 | var coords = spiral.archimedean(center, angle, 0, b); 164 | ctx.lineTo(coords[0], coords[1]); 165 | } 166 | 167 | ctx.lineWidth = this.width; 168 | ctx.strokeStyle = `rgba(${this.getColor(.5)})`; 169 | ctx.stroke(); 170 | } 171 | 172 | return this; 173 | }; 174 | 175 | 176 | //render line-style 177 | Spectrum.prototype.drawLine = function () { 178 | var ctx = this.context; 179 | var width = this.viewport[2], 180 | height = this.viewport[3]; 181 | 182 | ctx.lineWidth = this.trail ? this.width * 2 : this.width; 183 | 184 | //draw trail 185 | var gradient = ctx.createLinearGradient(this.viewport[0],0,width,0); 186 | this.createShape(this.trailMagnitudes, gradient); 187 | ctx.fillStyle = gradient; 188 | ctx.strokeStyle = `rgba(${this.getColor(1)})`; 189 | ctx.stroke(); 190 | if (this.trail) { 191 | ctx.fill(); 192 | } 193 | 194 | //draw main magnitudes 195 | this.createShape(this.magnitudes); 196 | ctx.strokeStyle = gradient; 197 | ctx.fillStyle = gradient; 198 | 199 | if (this.trail) { 200 | ctx.save(); 201 | ctx.globalCompositeOperation = 'xor'; 202 | ctx.fillStyle = 'rgba(0,0,0,1)'; 203 | ctx.fill(); 204 | ctx.restore(); 205 | } 206 | 207 | return this; 208 | }; 209 | 210 | 211 | //render fill-style 212 | Spectrum.prototype.drawFill = function () { 213 | var ctx = this.context; 214 | var width = this.viewport[2], 215 | height = this.viewport[3]; 216 | 217 | 218 | ctx.lineWidth = this.width; 219 | 220 | //draw trail 221 | var gradient = ctx.createLinearGradient(this.viewport[0],0,width,0); 222 | this.createShape(this.trailMagnitudes, gradient); 223 | ctx.fillStyle = gradient; 224 | ctx.strokeStyle = `rgba(${this.getColor(1)})`; 225 | if (this.trail) { 226 | ctx.stroke(); 227 | } 228 | 229 | //draw main magnitudes 230 | this.createShape(this.magnitudes); 231 | ctx.strokeStyle = gradient; 232 | ctx.fillStyle = gradient; 233 | 234 | ctx.fill(); 235 | 236 | return this; 237 | }; 238 | 239 | 240 | //render bar-style 241 | //FIXME: ponder on making even bars for log fn 242 | Spectrum.prototype.drawBar = function () { 243 | var ctx = this.context; 244 | var prevX = -1, prevOffset = -1, nf, f, x, offset, amp; 245 | 246 | var width = this.viewport[2], 247 | height = this.viewport[3]; 248 | 249 | var magnitudes = this.magnitudes; 250 | var trail = this.trailMagnitudes; 251 | var barWidth; 252 | 253 | for (var i = .5; i < magnitudes.length; i++) { 254 | nf = i / magnitudes.length; 255 | f = this.unf(nf); 256 | 257 | x = f * width; 258 | offset = nf * (magnitudes.length - 1); 259 | 260 | barWidth = Math.min(this.width, Math.abs(x - prevX)); 261 | if (x === prevX) continue; 262 | prevX = x|0; 263 | if (offset === prevOffset) continue; 264 | prevOffset = offset|0; 265 | 266 | amp = magnitudes[offset|0]; 267 | amp = clamp((amp - this.minDecibels) / (this.maxDecibels - this.minDecibels), 0, 1); 268 | 269 | ctx.fillRect(x - barWidth, (height*(1 - this.align) - amp*height*(1 - this.align) ), barWidth, (amp*height)); 270 | } 271 | 272 | if (this.trail) { 273 | ctx.fillStyle = `rgba(${this.getColor(1)})`; 274 | prevX = 0; 275 | for (var i = .5; i < trail.length; i++) { 276 | nf = i / trail.length; 277 | f = this.unf(nf); 278 | 279 | x = f * width; 280 | offset = nf * (trail.length - 1); 281 | 282 | barWidth = Math.min(this.width, x - prevX); 283 | 284 | if (x === prevX) continue; 285 | prevX = x|0; 286 | if (offset === prevOffset) continue; 287 | prevOffset = offset|0; 288 | 289 | amp = trail[offset|0]; 290 | amp = clamp((amp - this.minDecibels) / (this.maxDecibels - this.minDecibels), 0, 1); 291 | 292 | 293 | ctx.fillRect(x - barWidth, (height*(1 - this.align) - amp*height*(1 - this.align) ), barWidth, 1); 294 | ctx.fillRect(x - barWidth, (height*(1 - this.align) - amp*height*(1 - this.align) + amp*height ) - 1, barWidth, 1); 295 | } 296 | } 297 | 298 | return this; 299 | }; 300 | 301 | 302 | //create shape for a data in rect view 303 | Spectrum.prototype.createShape = function (data, gradient) { 304 | var ctx = this.context; 305 | var prevX = -1, prevOffset = -1, nf, f, x, offset, amp, relativeAmp; 306 | var padding = 40; 307 | var balance = .5; 308 | 309 | var width = this.viewport[2], 310 | height = this.viewport[3]; 311 | 312 | ctx.beginPath(); 313 | ctx.moveTo(-padding, height * (1 - this.align)); 314 | gradient && gradient.addColorStop(0, `rgba(${this.getColor(0.5)})`); 315 | 316 | for (var i = 0; i < data.length; i++) { 317 | nf = (i + .5) / data.length; 318 | f = this.unf(nf); 319 | 320 | x = f * width; 321 | offset = nf * (data.length - 1); 322 | 323 | amp = mix(data[offset|0], data[(offset+1)|0], offset%1); 324 | relativeAmp = (amp + 100) / (this.peak + 100); 325 | amp = clamp((amp - this.minDecibels) / (this.maxDecibels - this.minDecibels), 0, 1); 326 | gradient && gradient.addColorStop(f, `rgba(${this.getColor( amp*balance + relativeAmp*(1 - balance) )})`); 327 | ctx.lineTo(x, (height*(1 - this.align) - amp*height*(1 - this.align) )); 328 | } 329 | 330 | prevOffset = -1; 331 | prevX = -1; 332 | ctx.lineTo(width+padding, height * (1 - this.align)); 333 | for (var i = data.length - 1; i>=0; i--) { 334 | nf = (i + .5) / data.length; 335 | f = this.unf(nf); 336 | 337 | x = f * width; 338 | offset = nf * (data.length - 1); 339 | 340 | amp = mix(data[offset|0], data[(offset+1)|0], offset%1); 341 | amp = clamp((amp - this.minDecibels) / (this.maxDecibels - this.minDecibels), 0, 1); 342 | 343 | ctx.lineTo(x, (height*(1 - this.align) + amp*height*(this.align) )); 344 | } 345 | ctx.lineTo(-padding, height * (1 - this.align)); 346 | ctx.closePath(); 347 | 348 | return this; 349 | }; 350 | 351 | 352 | //get linear f from logarithmic f 353 | Spectrum.prototype.f = function (ratio, log) { 354 | log = log == null ? this.logarithmic : log; 355 | 356 | var halfRate = this.sampleRate * .5; 357 | var leftF = this.minFrequency / halfRate; 358 | var rightF = this.maxFrequency / halfRate; 359 | 360 | //forward action 361 | if (log) { 362 | var logF = Math.pow(10., 363 | Math.log10(this.minFrequency) + ratio * (Math.log10(this.maxFrequency) - Math.log10(this.minFrequency)) 364 | ); 365 | ratio = (logF - this.minFrequency) / (this.maxFrequency - this.minFrequency); 366 | } 367 | 368 | 369 | ratio = leftF + ratio * (rightF - leftF); 370 | 371 | return ratio; 372 | }; 373 | 374 | //get log-shifted f from linear f 375 | Spectrum.prototype.unf = function (ratio, log) { 376 | log = log == null ? this.logarithmic : log; 377 | 378 | var halfRate = this.sampleRate * .5; 379 | var leftF = this.minFrequency / halfRate; 380 | var rightF = this.maxFrequency / halfRate; 381 | 382 | //back action 383 | ratio = (ratio - leftF) / (rightF - leftF); 384 | 385 | if (log) { 386 | var logRatio = ratio * (this.maxFrequency - this.minFrequency) + this.minFrequency; 387 | 388 | ratio = (Math.log10(logRatio) - Math.log10(this.minFrequency)) / (Math.log10(this.maxFrequency) - Math.log10(this.minFrequency)); 389 | } 390 | 391 | return clamp(ratio, 0, 1); 392 | }; 393 | --------------------------------------------------------------------------------