├── .gitignore
├── Makefile
├── README.md
├── communication.js
├── example.wav
├── main.html
├── packages-osx.txt
├── packages.txt
├── requirements.txt
├── screenshot.png
├── server.py
├── specsize.js
└── spectrogram.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.swp
3 |
4 | # Byte-compiled / optimized / DLL files
5 | __pycache__/
6 | *.py[cod]
7 |
8 | # Distribution / packaging
9 | .Python
10 | env/
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | lib/
17 | lib64/
18 | parts/
19 | sdist/
20 | var/
21 | *.egg-info/
22 | .installed.cfg
23 | *.egg
24 |
25 | # Installer logs
26 | pip-log.txt
27 | pip-delete-this-directory.txt
28 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: clean run installdeps lint
2 |
3 | OS := $(shell uname)
4 |
5 | ifeq ($(RPM),1)
6 | PKG_INSTALLER = yum
7 | else
8 | PKG_INSTALLER = apt-get
9 | endif
10 |
11 | run:
12 | python server.py
13 |
14 | clean:
15 | find . -type f -name '*.py[cod]' -delete
16 | find . -type f -name '*.*~' -delete
17 |
18 | installdeps:
19 | ifeq ('$(OS)','Darwin')
20 | # Run MacOS commands
21 | cat packages-osx.txt | xargs brew install
22 | export PKG_CONFIG_PATH=/usr/local/Cellar/libffi/3.0.13/lib/pkgconfig/
23 | else
24 | # Run Linux commands
25 | cat packages.txt | xargs sudo $(PKG_INSTALLER) -y install
26 | endif
27 | pip install -r requirements.txt
28 |
29 | lint: clean
30 | flake8 .
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | WebGL-Spectrogram
2 | =================
3 |
4 | ## Get running in 5 minutes
5 |
6 | First, check out WebGL-Spectrogram and launch it locally:
7 |
8 | ```bash
9 | git clone https://github.com/bastibe/WebGL-Spectrogram.git
10 | cd WebGL-Spectrogram
11 | ```
12 |
13 | We have some installation commands that utilize `brew`, `yum`, and `apt-get`
14 | based on your system and package manager preference.
15 |
16 | All Python packages are install using `pip` from the `requirements.txt` file.
17 | If you wish to install via another method i.e. `conda`, all the packages and
18 | versions can be found in `requirements.txt`.
19 |
20 | The commands below install common packages for OSX or Linux and run the server.
21 |
22 | Note: You need to use `sudo` if you are not working in a
23 | [virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/).
24 |
25 | If you are not using OSX, the `apt-get` manger is used by default.
26 |
27 | To use `yum` to install packages instead, run `make installdeps RPM=1`.
28 |
29 | ```bash
30 | make installdeps
31 | make run
32 | ```
33 |
34 | A webpage should now open and you can select the file `example.wav` to generate
35 | a sample spectrogram.
36 |
37 | ## About
38 |
39 | This is a small local web app that displays a spectrogram of an audio signal in
40 | the browser using WebGL. It is known to work with Firefox and Chrome, though
41 | performance is best in Firefox.
42 |
43 | The spectrogram display can be zoomed and panned smoothly and has a
44 | configurable FFT length. The amplitude range can be adjusted on the fly as
45 | well. It can open any local `.wav` or `.flac` file.
46 |
47 | 
48 |
49 | `server.py` contains a small web server written in Python and Tornado that
50 | responds to messages on `ws://localhost:XXXX/spectrogram`, where `XXXX` is a
51 | random port in the local range. Currently, it supports two kinds of messages:
52 | One that requests a spectrogram from a file name, and another that requests a
53 | spectrogram from a file content attached to the message. It responds to these
54 | messages with a message containing a full spectrogram for the given audio file.
55 |
56 | `communications.js` contains the Javascript implementation of the messaging
57 | protocol.
58 |
59 | `specsize.js` contains a helper class for storing the extent of a
60 | spectro-temporal display.
61 |
62 | `spectrogram.js` contains Javascript code that can load audio files and request
63 | spectrograms from the server, and draw those spectrograms using WebGL. It also
64 | contains a time and frequency scale and a small indicator that shows the cursor
65 | position in time/frequency coordinates.
66 |
67 | `main.html` contains the website used to display the spectrogram.
68 |
69 | The messaging protocol is JSON-based, easily extensible and supports
70 | transmission of textual or binary data.
71 |
--------------------------------------------------------------------------------
/communication.js:
--------------------------------------------------------------------------------
1 | var ws = new WebSocket("ws://localhost:" + getURLParameters()['port'] + "/spectrogram");
2 | ws.binaryType = 'arraybuffer';
3 |
4 | /* return an object containing the URL parameters */
5 | function getURLParameters() {
6 | var params = {};
7 | if (location.search) {
8 | var parts = location.search.substring(1).split('&');
9 | for (var i = 0; i < parts.length; i++) {
10 | var pair = parts[i].split('=');
11 | if (!pair[0]) continue;
12 | params[pair[0]] = pair[1] || "";
13 | }
14 | }
15 | return params
16 | }
17 |
18 | /* Send a message.
19 |
20 | Arguments:
21 | type the message type as string.
22 | content the message content as json-serializable data.
23 | payload binary data as ArrayBuffer.
24 | */
25 | function sendMessage(type, content, payload) {
26 | if (payload === undefined) {
27 | ws.send(JSON.stringify({
28 | type: type,
29 | content: content
30 | }));
31 | } else {
32 | var headerString = JSON.stringify({
33 | type: type,
34 | content: content
35 | });
36 | // append enough spaces so that the payload starts at an 8-byte
37 | // aligned position. The first four bytes will be the length of
38 | // the header, encoded as a 32 bit signed integer:
39 | var alignmentBytes = 8 - ((headerString.length + 4) % 8);
40 | for (var i = 0; i < alignmentBytes; i++) {
41 | headerString += " ";
42 | }
43 |
44 | var message = new ArrayBuffer(4 + headerString.length + payload.byteLength);
45 |
46 | // write the length of the header as a binary 32 bit signed integer:
47 | var prefixData = new Int32Array(message, 0, 4);
48 | prefixData[0] = headerString.length;
49 |
50 | // write the header data
51 | var headerData = new Uint8Array(message, 4, headerString.length)
52 | for (var i = 0; i < headerString.length; i++) {
53 | headerData[i] = headerString.charCodeAt(i);
54 | }
55 |
56 | // write the payload data
57 | payloadData = new Uint8Array(message, 4 + headerString.length, payload.byteLength);
58 | payloadData.set(new Uint8Array(payload));
59 | ws.send(message);
60 | }
61 | }
62 |
63 | /* Request the spectrogram for a file.
64 |
65 | Arguments:
66 | filename the file name from which to load audio data.
67 | nfft the FFT length used for calculating the spectrogram.
68 | overlap the amount of overlap between consecutive spectra.
69 | */
70 | function requestFileSpectrogram(filename, nfft, overlap) {
71 | sendMessage("request_file_spectrogram", {
72 | filename: filename,
73 | nfft: nfft,
74 | overlap: overlap
75 | });
76 | }
77 |
78 | /* Request the spectrogram for a file.
79 |
80 | Arguments:
81 | data the content of a file from which to load audio data.
82 | nfft the FFT length used for calculating the spectrogram.
83 | overlap the amount of overlap between consecutive spectra.
84 | */
85 | function requestDataSpectrogram(data, nfft, overlap) {
86 | sendMessage("request_data_spectrogram", {
87 | nfft: nfft,
88 | overlap: overlap
89 | }, data);
90 | }
91 |
92 | /* Parses a message
93 |
94 | Each message must contain the message type, the message content,
95 | and an optional binary payload. The decoded message will be
96 | forwarded to different functions based on the message type.
97 |
98 | Arguments:
99 | event the message, either as string or ArrayBuffer.
100 | */
101 | ws.onmessage = function(event) {
102 | if (event.data.constructor.name === "ArrayBuffer") {
103 | var headerLen = new Int32Array(event.data, 0, 1)[0];
104 | var header = String.fromCharCode.apply(null, new Uint8Array(event.data, 4, headerLen));
105 | } else {
106 | var header = event.data;
107 | }
108 |
109 | try {
110 | msg = JSON.parse(header);
111 | } catch (e) {
112 | console.error("Message", e.message, "is not a valid JSON object");
113 | return
114 | }
115 |
116 | var type = msg.type
117 | var content = msg.content
118 |
119 | if (type === "spectrogram") {
120 | loadSpectrogram(new Float32Array(event.data, headerLen + 4),
121 | content.extent[0], content.extent[1],
122 | content.fs, content.length);
123 | } else if (type === "loading_progress") {
124 | updateProgressBar(content.progress);
125 | } else {
126 | console.log(type, content);
127 | }
128 | }
129 |
130 | /* Sets the progress bar
131 |
132 | If progress is 0 or 1, the progress bar will be turned invisible.
133 | */
134 | function updateProgressBar(progress) {
135 | var progressBar = document.getElementById('progressBar');
136 | if (progress === 0 || progress === 1) {
137 | progressBar.hidden = true;
138 | } else {
139 | progressBar.hidden = false;
140 | progressBar.value = progress;
141 | }
142 | }
143 |
144 | /* log some info about the GL, then display spectrogram */
145 | ws.onopen = function() {
146 | logGLInfo();
147 | reloadSpectrogram();
148 | }
149 |
150 | /* Test the keyup event for a submission and then reload the spectrogram.
151 | */
152 | function submitSpectrogram(e) {
153 | e.which = e.which || e.keyCode;
154 | if (e.which == 13) {
155 | reloadSpectrogram();
156 | }
157 | }
158 |
159 | /* Loads the spectrogram for the currently seleced file/FFT-length.
160 |
161 | Reads the audioFile input field to get the current file and the
162 | select field to get the current FFT length.
163 |
164 | This only sends the request for a spectrogram. Delivering the
165 | spectrogram is up to the server.
166 | */
167 | function reloadSpectrogram() {
168 | var audioFile = document.getElementById('audioFileByData').files[0];
169 | var fftLen = parseFloat(document.getElementById('fftLen').value);
170 | // first we try to load a file
171 | if (audioFile) {
172 | var reader = new FileReader();
173 | reader.readAsArrayBuffer(audioFile);
174 | reader.onloadend = function() {
175 | requestDataSpectrogram(reader.result, fftLen);
176 | }
177 | } else { // otherwise see if there is a filename
178 | audioFile = document.getElementById('audioFileByName').value;
179 | if (audioFile) {
180 | console.log("Requesting spectrogram for: " + audioFile);
181 | requestFileSpectrogram(audioFile, fftLen);
182 | }
183 | }
184 | if (!audioFile) {
185 | console.log("Could not load spectrogram: No file selected");
186 | return;
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/example.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bastibe/WebGL-Spectrogram/135b43ab20f07a01cc45e25540ca1cc997d493dd/example.wav
--------------------------------------------------------------------------------
/main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Spectrogram
5 |
54 |
55 |
56 |
57 |
83 |
84 |
85 |
86 |
NaN
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/packages-osx.txt:
--------------------------------------------------------------------------------
1 | pkg-config
2 | libffi
3 | libsndfile
4 |
--------------------------------------------------------------------------------
/packages.txt:
--------------------------------------------------------------------------------
1 | pkg-config
2 | libffi-dev
3 | libsndfile-dev
4 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | PySoundFile==0.5.0
2 | backports.ssl-match-hostname==3.4.0.2
3 | certifi==14.05.14
4 | cffi==0.8.6
5 | flake8==2.2.5
6 | numpy==1.9.1
7 | pycparser==2.10
8 | scipy==0.14.0
9 | tornado==4.0.2
10 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bastibe/WebGL-Spectrogram/135b43ab20f07a01cc45e25540ca1cc997d493dd/screenshot.png
--------------------------------------------------------------------------------
/server.py:
--------------------------------------------------------------------------------
1 | from __future__ import division
2 |
3 | import io
4 | import json
5 | import struct
6 | import numpy as np
7 |
8 | from tornado.websocket import WebSocketHandler
9 | from pysoundfile import SoundFile
10 |
11 |
12 | def hann(n):
13 | return 0.5 - 0.5 * np.cos(2.0 * np.pi * np.arange(n) / (n - 1))
14 |
15 |
16 | def from_bytes(b):
17 | return struct.unpack("@i", b)[0]
18 |
19 |
20 | def to_bytes(n):
21 | return struct.pack("@i", n)
22 |
23 |
24 | class JSONWebSocket(WebSocketHandler):
25 |
26 | """A websocket that sends/receives JSON messages.
27 |
28 | Each message has a type, a content and optional binary data.
29 | Message type and message content are stored as a JSON object. Type
30 | must be a string and content must be JSON-serializable.
31 |
32 | If binary data is present, the message will be sent in binary,
33 | with the first four bytes storing a signed integer containing the
34 | length of the JSON data, then the JSON data, then the binary data.
35 | The binary data will be stored 8-byte aligned.
36 |
37 | """
38 |
39 | def check_origin(self, origin):
40 | return True
41 |
42 | def open(self):
43 | print("WebSocket opened")
44 |
45 | def send_message(self, msg_type, content, data=None):
46 | """Send a message.
47 |
48 | Arguments:
49 | msg_type the message type as string.
50 | content the message content as json-serializable data.
51 | data raw bytes that are appended to the message.
52 |
53 | """
54 |
55 | if data is None:
56 | self.write_message(json.dumps({"type": msg_type,
57 | "content": content}).encode())
58 | else:
59 | header = json.dumps({'type': msg_type,
60 | 'content': content}).encode()
61 | # append enough spaces so that the payload starts at an 8-byte
62 | # aligned position. The first four bytes will be the length of
63 | # the header, encoded as a 32 bit signed integer:
64 | header += b' ' * (8 - ((len(header) + 4) % 8))
65 | # the length of the header as a binary 32 bit signed integer:
66 | prefix = to_bytes(len(header))
67 | self.write_message(prefix + header + data, binary=True)
68 |
69 | def on_message(self, msg):
70 | """Parses a message
71 |
72 | Each message must contain the message type, the message
73 | content, and an optional binary payload. The decoded message
74 | will be forwarded to receive_message().
75 |
76 | Arguments:
77 | msg the message, either as str or bytes.
78 |
79 | """
80 |
81 | if isinstance(msg, bytes):
82 | header_len = from_bytes(msg[:4])
83 | header = msg[4:header_len + 4].decode()
84 | data = msg[4 + header_len:]
85 | else:
86 | header = msg
87 | data = None
88 |
89 | try:
90 | header = json.loads(header)
91 | except ValueError:
92 | print('message {} is not a valid JSON object'.format(msg))
93 | return
94 |
95 | if 'type' not in header:
96 | print('message {} does not have a "type" field'.format(header))
97 | elif 'content' not in header:
98 | print('message {} does not have a "content" field'.format(header))
99 | else:
100 | self.receive_message(header['type'], header['content'], data)
101 |
102 | def receive_message(self, msg_type, content, data=None):
103 | """Message dispatcher.
104 |
105 | This is meant to be overwritten by subclasses. By itself, it
106 | does nothing but complain.
107 |
108 | """
109 |
110 | if msg_type == "information":
111 | print(content)
112 | else:
113 | print(("Don't know what to do with message of type {}" +
114 | "and content {}").format(msg_type, content))
115 |
116 | def on_close(self):
117 | print("WebSocket closed")
118 |
119 |
120 | class SpectrogramWebSocket(JSONWebSocket):
121 |
122 | """A websocket that sends spectrogram data.
123 |
124 | It calculates a spectrogram with a given FFT length and overlap
125 | for a requested file. The file can either be supplied as a binary
126 | data blob, or as a file name.
127 |
128 | This implements two message types:
129 | - request_file_spectrogram, which needs a filename, and optionally
130 | `nfft` and `overlap`.
131 | - request_data_spectrogram, which needs the file as a binary data
132 | blob, and optionally `nfft` and `overlap`.
133 |
134 | """
135 |
136 | def receive_message(self, msg_type, content, data=None):
137 | """Message dispatcher.
138 |
139 | Dispatches
140 | - `request_file_spectrogram` to self.on_file_spectrogram
141 | - `request_data_spectrogram` to self.on_data_spectrogram
142 |
143 | Arguments:
144 | msg_type the message type as string.
145 | content the message content as dictionary.
146 | data raw bytes.
147 |
148 | """
149 |
150 | if msg_type == 'request_file_spectrogram':
151 | self.on_file_spectrogram(**content)
152 | elif msg_type == 'request_data_spectrogram':
153 | self.on_data_spectrogram(data, **content)
154 | else:
155 | super(self.__class__, self).receive_message(
156 | msg_type, content, data)
157 |
158 | def on_file_spectrogram(self, filename, nfft=1024, overlap=0.5):
159 | """Loads an audio file and calculates a spectrogram.
160 |
161 | Arguments:
162 | filename the file name from which to load the audio data.
163 | nfft the FFT length used for calculating the spectrogram.
164 | overlap the amount of overlap between consecutive spectra.
165 |
166 | """
167 |
168 | try:
169 | file = SoundFile(filename)
170 | sound = file[:].sum(axis=1)
171 | spec = self.spectrogram(sound, nfft, overlap)
172 |
173 | self.send_message('spectrogram',
174 | {'extent': spec.shape,
175 | 'fs': file.samplerate,
176 | 'length': len(file) / file.samplerate},
177 | spec.tostring())
178 | except RuntimeError as e:
179 | error_msg = 'Filename: {} could not be loaded.\n{}'.format(filename, e)
180 | self.send_message('error', {
181 | 'error_msg': error_msg
182 | })
183 | print(error_msg)
184 |
185 | def on_data_spectrogram(self, data, nfft=1024, overlap=0.5):
186 | """Loads an audio file and calculates a spectrogram.
187 |
188 | Arguments:
189 | data the content of a file from which to load audio data.
190 | nfft the FFT length used for calculating the spectrogram.
191 | overlap the amount of overlap between consecutive spectra.
192 |
193 | """
194 |
195 | file = SoundFile(io.BytesIO(data))
196 | sound = file[:].sum(axis=1)
197 | spec = self.spectrogram(sound, nfft, overlap)
198 |
199 | self.send_message('spectrogram',
200 | {'extent': spec.shape,
201 | 'fs': file.samplerate,
202 | 'length': len(file) / file.samplerate},
203 | spec.tostring())
204 |
205 | def spectrogram(self, data, nfft, overlap):
206 | """Calculate a real spectrogram from audio data
207 |
208 | An audio data will be cut up into overlapping blocks of length
209 | `nfft`. The amount of overlap will be `overlap*nfft`. Then,
210 | calculate a real fourier transform of length `nfft` of every
211 | block and save the absolute spectrum.
212 |
213 | Arguments:
214 | data audio data as a numpy array.
215 | nfft the FFT length used for calculating the spectrogram.
216 | overlap the amount of overlap between consecutive spectra.
217 |
218 | """
219 |
220 | shift = round(nfft * overlap)
221 | num_blocks = int((len(data) - nfft) / shift + 1)
222 | specs = np.zeros((nfft / 2 + 1, num_blocks), dtype=np.float32)
223 | window = hann(nfft)
224 | for idx in range(num_blocks):
225 | specs[:, idx] = np.abs(
226 | np.fft.rfft(
227 | data[idx * shift:idx * shift + nfft] * window,
228 | n=nfft)) / nfft
229 | if idx % 10 == 0:
230 | self.send_message(
231 | "loading_progress", {"progress": idx / num_blocks})
232 | specs[:, -1] = np.abs(
233 | np.fft.rfft(data[num_blocks * shift:], n=nfft)) / nfft
234 | self.send_message("loading_progress", {"progress": 1})
235 | return specs.T
236 |
237 |
238 | if __name__ == "__main__":
239 | import os
240 | import webbrowser
241 | from tornado.web import Application
242 | from tornado.ioloop import IOLoop
243 | import random
244 |
245 | app = Application([("/spectrogram", SpectrogramWebSocket)])
246 |
247 | random.seed()
248 | port = random.randrange(49152, 65535)
249 | app.listen(port)
250 | webbrowser.open('file://{}/main.html?port={}'.format(os.getcwd(), port))
251 | IOLoop.instance().start()
252 |
--------------------------------------------------------------------------------
/specsize.js:
--------------------------------------------------------------------------------
1 | /* the size of a spectrogram in time, frequency and amplitude */
2 | function SpecSize(minT, maxT, minF, maxF, minA, maxA) {
3 | this.minT = minT;
4 | this.maxT = maxT;
5 | this.minF = minF;
6 | this.maxF = maxF;
7 | this.minA = minA;
8 | this.maxA = maxA;
9 | }
10 |
11 | /* the size in time */
12 | SpecSize.prototype.widthT = function() {
13 | return this.maxT - this.minT;
14 | }
15 |
16 | /* the size in frequency */
17 | SpecSize.prototype.widthF = function() {
18 | return this.maxF - this.minF;
19 | }
20 |
21 | /* the size in amplitude */
22 | SpecSize.prototype.widthA = function() {
23 | return this.maxA - this.minA;
24 | }
25 |
26 | /* the center in time */
27 | SpecSize.prototype.centerT = function() {
28 | return (this.minT + this.maxT) / 2;
29 | }
30 |
31 | /* the center in frequency */
32 | SpecSize.prototype.centerF = function() {
33 | return (this.minF + this.maxF) / 2;
34 | }
35 |
36 | /* the center in amplitude */
37 | SpecSize.prototype.centerA = function() {
38 | return (this.minA + this.maxA) / 2;
39 | }
40 |
41 | /* the relative position of a time */
42 | SpecSize.prototype.scaleT = function(t) {
43 | return t * this.widthT() + this.minT;
44 | }
45 |
46 | /* the relative position of a frequency */
47 | SpecSize.prototype.scaleF = function(f) {
48 | return f * this.widthF() + this.minF;
49 | }
50 |
51 | /* the relative position of a amplitude */
52 | SpecSize.prototype.scaleA = function(a) {
53 | return a * this.widthA() + this.minA;
54 | }
55 |
--------------------------------------------------------------------------------
/spectrogram.js:
--------------------------------------------------------------------------------
1 | var specView;
2 | var specTimeScale;
3 | var specFrequencyScale;
4 | var specDataView;
5 |
6 | var gl; // the WebGL instance
7 |
8 | // shader attributes:
9 | var vertexPositionAttribute;
10 | var textureCoordAttribute;
11 |
12 | // shader uniforms:
13 | var samplerUniform;
14 | var ampRangeUniform;
15 | var zoomUniform;
16 | var specSizeUniform;
17 | var specDataSizeUniform;
18 | var specModeUniform;
19 | var specLogarithmicUniform;
20 |
21 | // vertex buffer objects
22 | var vertexPositionBuffers;
23 | var textureCoordBuffer;
24 |
25 | // textures objects
26 | var spectrogramTextures;
27 |
28 | var specSize; // total size of the spectrogram
29 | var specViewSize; // visible size of the spectrogram
30 |
31 | /* initialize all canvases */
32 | function start() {
33 | specView = document.getElementById('spectrogram');
34 | specTimeScale = document.getElementById('specTimeScale');
35 | specFrequencyScale = document.getElementById('specFreqScale');
36 | specDataView = document.getElementById('specDataView');
37 | initSpectrogram();
38 | window.addEventListener("resize", updateCanvasResolutions, false);
39 | updateCanvasResolutions();
40 | eventsInit()
41 | }
42 |
43 | /* set resolution of all canvases to native resolution */
44 | function updateCanvasResolutions() {
45 | specView.width = specView.clientWidth;
46 | specView.height = specView.clientHeight;
47 | gl.viewport(0, 0, specView.width, specView.height);
48 | specTimeScale.width = specTimeScale.clientWidth;
49 | specTimeScale.height = specTimeScale.clientHeight;
50 | specFrequencyScale.width = specFrequencyScale.clientWidth;
51 | specFrequencyScale.height = specFrequencyScale.clientHeight;
52 | window.requestAnimationFrame(drawScene);
53 | }
54 |
55 | /* log version and memory information about WebGL */
56 | function logGLInfo() {
57 | sendMessage('information',
58 | "version: " + gl.getParameter(gl.VERSION) + "\n" +
59 | "shading language version: " + gl.getParameter(gl.SHADING_LANGUAGE_VERSION) + "\n" +
60 | "vendor: " + gl.getParameter(gl.VENDOR) + "\n" +
61 | "renderer: " + gl.getParameter(gl.RENDERER) + "\n" +
62 | "max texture size: " + gl.getParameter(gl.MAX_TEXTURE_SIZE) + "\n" +
63 | "max combined texture image units: " + gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS));
64 | }
65 |
66 | /* get WebGL context and load required extensions */
67 | function initSpectrogram() {
68 | try {
69 | gl = specView.getContext('webgl');
70 | } catch (e) {
71 | alert('Could not initialize WebGL');
72 | gl = null;
73 | }
74 | // needed for floating point textures
75 | gl.getExtension("OES_texture_float");
76 | var error = gl.getError();
77 | if (error != gl.NO_ERROR) {
78 | alert("Could not enable float texture extension");
79 | }
80 | // needed for linear filtering of floating point textures
81 | gl.getExtension("OES_texture_float_linear");
82 | if (error != gl.NO_ERROR) {
83 | alert("Could not enable float texture linear extension");
84 | }
85 | // 2D-drawing only
86 | gl.disable(gl.DEPTH_TEST);
87 |
88 | // get shaders ready
89 | loadSpectrogramShaders();
90 |
91 | // load dummy data
92 | loadSpectrogram(new Float32Array(1), 1, 1, 44100, 1);
93 | }
94 |
95 | /* link shaders and save uniforms and attributes
96 |
97 | saves the following attributes to global scope:
98 | - vertexPositionAttribute: aVertexPosition
99 | - textureCoordAttribute: aTextureCoord
100 |
101 | saves the following uniforms to global scope:
102 | - samplerUniform: uSampler
103 | - zoomUniform: mZoom
104 | - ampRangeUniform: vAmpRange
105 | - specSizeUniform: vSpecSize
106 | - specModeUniform: uSpecMode
107 | */
108 | function loadSpectrogramShaders() {
109 | //
110 | // creates a shader of the given type, uploads the source and
111 | // compiles it.
112 | //
113 | function loadShader(gl, type, source) {
114 | const shader = gl.createShader(type);
115 |
116 | // Send the source to the shader object
117 |
118 | gl.shaderSource(shader, source);
119 |
120 | // Compile the shader program
121 |
122 | gl.compileShader(shader);
123 |
124 | // See if it compiled successfully
125 |
126 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
127 | alert('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));
128 | gl.deleteShader(shader);
129 | return null;
130 | }
131 |
132 | return shader;
133 | }
134 |
135 | // Vertex shader program
136 | var vsSource = `
137 | attribute vec2 aVertexPosition;
138 | attribute vec2 aTextureCoord;
139 | uniform mat3 mZoom;
140 | varying highp vec2 vTextureCoord;
141 | void main() {
142 | vec3 zoomedVertexPosition = vec3(aVertexPosition, 1.0) * mZoom;
143 | gl_Position = vec4(zoomedVertexPosition, 1.0);
144 | vTextureCoord = aTextureCoord;
145 | }
146 | `;
147 |
148 | // Fragment shader program
149 | var fsSource = `
150 | varying highp vec2 vTextureCoord;
151 | uniform sampler2D uSampler;
152 | uniform highp vec2 vAmpRange;
153 | uniform highp vec2 vSpecSize;
154 | uniform highp vec2 vDataSize;
155 | uniform int uSpecMode;
156 | uniform bool bSpecLogarithmic;
157 | highp vec3 physicalColor(highp float amplitude) {
158 | /*
159 | The physical color scaling is partitioning the full [0 ... 1] range into
160 | seven evenly spaced sub-ranges. That's what the modulo and floor are
161 | doing. Then, each range modifies only one color channel while keeping
162 | the other two constant. This is done in such a way that no two
163 | different values map to the same color.
164 | The ranges are:
165 | 1. black (0,0,0) to red (1,0,0)
166 | 2. red (1,0,0) to yellow (1,1,0)
167 | 3. yellow (1,1,0) to green (0,1,0)
168 | 4. green (0,1,0) to cyan (0,1,1)
169 | 5. cyan (0,1,1) to blue (0,0,1)
170 | 6. blue (0,0,1) to magenta (1,0,1)
171 | 7. magenta (1,0,1) to white (1,1,1)
172 | */
173 | highp float fractional = mod(amplitude*8.0, 1.0);
174 | highp float integer = floor(amplitude*8.0);
175 | if (integer == 0.0) {
176 | return vec3(fractional, 0.0, 0.0);
177 | } else if (integer == 1.0) {
178 | return vec3(1.0, fractional, 0.0);
179 | } else if (integer == 2.0) {
180 | return vec3(1.0-fractional, 1.0, 0.0);
181 | } else if (integer == 3.0) {
182 | return vec3(0.0, 1.0, fractional);
183 | } else if (integer == 4.0) {
184 | return vec3(0.0, 1.0-fractional, 1.0);
185 | } else if (integer == 5.0) {
186 | return vec3(fractional, 0.0, 1.0);
187 | } else if (integer == 6.0) {
188 | return vec3(1.0, 1.0-fractional, 1.0);
189 | } else {
190 | return vec3(1.0, 1.0, 1.0);
191 | }
192 | }
193 | // from http://lolengine.net/blog/2013/07/27/rgb-to-hsv-in-glsl
194 | highp vec3 rgb2hsv(highp vec3 c)
195 | {
196 | highp vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
197 | highp vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
198 | highp vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
199 | highp float d = q.x - min(q.w, q.y);
200 | highp float e = 1.0e-10;
201 | return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
202 | }
203 | // from http://lolengine.net/blog/2013/07/27/rgb-to-hsv-in-glsl
204 | highp vec3 hsv2rgb(highp vec3 c)
205 | {
206 | highp vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
207 | highp vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
208 | return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
209 | }
210 | highp float logScale(highp float value) {
211 | value = 20.0*log(value)/log(10.0);
212 | //value = max(value, vAmpRange[0]);
213 | //value = min(value, vAmpRange[1]);
214 | value = (value - vAmpRange[0]) / (vAmpRange[1] - vAmpRange[0]);
215 | return value;
216 | }
217 | highp float getAmplitude(highp vec2 coord) {
218 | if (bSpecLogarithmic) {
219 | coord.y = pow(vSpecSize.y, coord.y)/vSpecSize.y;
220 | coord.y += 1.0/512.0;
221 | }
222 | return texture2D(uSampler, coord).r;
223 | }
224 | highp vec3 physicalMode() {
225 | highp float amplitude = getAmplitude(vTextureCoord);
226 | return physicalColor(logScale(amplitude));
227 | }
228 | highp vec3 normalMode() {
229 | highp float amplitude = getAmplitude(vTextureCoord);
230 | amplitude = logScale(amplitude);
231 | return vec3(amplitude, amplitude, amplitude);
232 | }
233 | highp vec3 direction() {
234 | highp vec2 d = vec2(0.0, 0.0);
235 | for (int x=0; x<=5; x++) {
236 | for (int y=-5; y<=5; y+=1) {
237 | highp vec2 vIter=vec2(x, y);
238 | highp float amplitude = getAmplitude(vTextureCoord+vIter/vDataSize) *
239 | getAmplitude(vTextureCoord-vIter/vDataSize);
240 | d += vIter * amplitude / ((x==0&&y==0) ? 1.0 : sqrt(float(x*x + y*y)));
241 | }
242 | }
243 | highp float direction = atan(d.x, d.y)/3.14;
244 | highp float strength = sqrt(d.x*d.x + d.y*d.y);
245 | strength = logScale(strength*1000.0);
246 | return hsv2rgb(vec3((direction-0.5), abs((direction-0.5))*2.0, strength));
247 | }
248 | highp float amplitudeAt(highp float angle, highp float distance) {
249 | highp vec2 coord = vec2(cos(angle)*distance,
250 | sin(angle)*distance);
251 | highp float amplitude = getAmplitude(vTextureCoord+coord/vDataSize) *
252 | getAmplitude(vTextureCoord-coord/vDataSize);
253 | return amplitude;
254 | }
255 | highp float amplitudesAt(highp float angle) {
256 | highp float amplitude = 0.0;
257 | for (int i=1; i<=5; i++) {
258 | amplitude += amplitudeAt(angle, float(i))/float(i*i);
259 | }
260 | return amplitude;
261 | }
262 | highp vec3 direction2() {
263 | highp vec2 histogram[16];
264 | histogram[0] = vec2(amplitudesAt(-3.14), -3.14);
265 | histogram[1] = vec2(amplitudesAt(-2.36), -2.36);
266 | histogram[2] = vec2(amplitudesAt(-1.57), -1.57);
267 | histogram[3] = vec2(amplitudesAt(-0.78), -0.78);
268 | histogram[4] = vec2(amplitudesAt( 0.0 ), 0.0 );
269 | histogram[5] = vec2(amplitudesAt( 0.78), 0.78);
270 | histogram[6] = vec2(amplitudesAt( 1.57), 1.57);
271 | histogram[7] = vec2(amplitudesAt( 2.36), 2.36);
272 | histogram[9] = vec2(amplitudesAt( 3.14), 3.14);
273 | highp vec2 max_value = vec2(0.0, 0.0);
274 | max_value = histogram[0].x > max_value.x ? histogram[0] : max_value;
275 | max_value = histogram[1].x > max_value.x ? histogram[1] : max_value;
276 | max_value = histogram[2].x > max_value.x ? histogram[2] : max_value;
277 | max_value = histogram[3].x > max_value.x ? histogram[3] : max_value;
278 | max_value = histogram[4].x > max_value.x ? histogram[4] : max_value;
279 | max_value = histogram[5].x > max_value.x ? histogram[5] : max_value;
280 | max_value = histogram[6].x > max_value.x ? histogram[6] : max_value;
281 | max_value = histogram[7].x > max_value.x ? histogram[7] : max_value;
282 | max_value = histogram[8].x > max_value.x ? histogram[8] : max_value;
283 | max_value = histogram[9].x > max_value.x ? histogram[9] : max_value;
284 | // scale to a third color rotation from green to red.
285 | highp float direction = max_value.y/3.14/3.0+0.333;
286 | highp float value = logScale(max_value.x);
287 | return hsv2rgb(vec3(direction, 1.0, value));
288 | }
289 | highp vec3 multiples() {
290 | highp float amplitude = getAmplitude(vTextureCoord);
291 | int iterations = 0;
292 | for (int m=2; m<=5; m++) {
293 | highp float y = vTextureCoord.y*float(m);
294 | if (y<1.0) {
295 | amplitude *= getAmplitude(vec2(vTextureCoord.x, y));
296 | iterations++;
297 | }
298 | }
299 | amplitude = pow(amplitude, 1.0/float(iterations));
300 | amplitude = logScale(amplitude);
301 | return physicalColor(amplitude);
302 | }
303 | void main() {
304 | if (uSpecMode == 0) {
305 | gl_FragColor = vec4(physicalMode(), 1.0);
306 | } else if (uSpecMode == 1) {
307 | gl_FragColor = vec4(normalMode(), 1.0);
308 | } else if (uSpecMode == 2) {
309 | gl_FragColor = vec4(direction2(), 1.0);
310 | } else if (uSpecMode == 3) {
311 | gl_FragColor = vec4(multiples(), 1.0);
312 | }
313 | }
314 | `;
315 |
316 | var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
317 | var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
318 |
319 | var shaderProgram = gl.createProgram();
320 | gl.attachShader(shaderProgram, vertexShader);
321 | gl.attachShader(shaderProgram, fragmentShader);
322 | gl.linkProgram(shaderProgram);
323 |
324 | if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
325 | alert("unable to link shader program.");
326 | }
327 |
328 | gl.useProgram(shaderProgram);
329 |
330 | vertexPositionAttribute = gl.getAttribLocation(shaderProgram, 'aVertexPosition');
331 | gl.enableVertexAttribArray(vertexPositionAttribute);
332 |
333 | textureCoordAttribute = gl.getAttribLocation(shaderProgram, 'aTextureCoord');
334 | gl.enableVertexAttribArray(textureCoordAttribute);
335 |
336 | samplerUniform = gl.getUniformLocation(shaderProgram, 'uSampler');
337 | zoomUniform = gl.getUniformLocation(shaderProgram, 'mZoom');
338 | ampRangeUniform = gl.getUniformLocation(shaderProgram, 'vAmpRange');
339 | specSizeUniform = gl.getUniformLocation(shaderProgram, 'vSpecSize');
340 | specDataSizeUniform = gl.getUniformLocation(shaderProgram, 'vDataSize');
341 | specModeUniform = gl.getUniformLocation(shaderProgram, 'uSpecMode');
342 | specLogarithmicUniform = gl.getUniformLocation(shaderProgram, 'bSpecLogarithmic');
343 | }
344 |
345 | /* load and compile a shader
346 |
347 | Attributes:
348 | id the id of a script element that contains the shader source.
349 |
350 | Returns the compiled shader.
351 | */
352 | function getShader(id) {
353 | var script = document.getElementById(id);
354 |
355 | if (script.type == "x-shader/x-fragment") {
356 | var shader = gl.createShader(gl.FRAGMENT_SHADER);
357 | } else if (script.type == "x-shader/x-vertex") {
358 | var shader = gl.createShader(gl.VERTEX_SHADER);
359 | } else {
360 | return null;
361 | }
362 |
363 | gl.shaderSource(shader, script.innerHTML);
364 | gl.compileShader(shader);
365 |
366 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
367 | alert("An error occurred compiling the shaders: " + gl.getShaderInfoLog(shader));
368 | return null;
369 | }
370 |
371 | return shader;
372 | }
373 |
374 | /* loads a spectrogram into video memory and fills VBOs
375 |
376 | If there is more data than fits into a single texture, several
377 | textures are allocated and the data is written into consecutive
378 | textures. According vertex positions are saved into an equal number
379 | of VBOs.
380 |
381 | - saves textures into a global array `spectrogramTextures`.
382 | - saves vertexes into a global array `vertexPositionBuffers`.
383 | - saves texture coordinates into global `textureCoordBuffer`.
384 |
385 | Attributes:
386 | data a Float32Array containing nblocks x nfreqs values.
387 | nblocks the width of the data, the number of blocks.
388 | nfreqs the height of the data, the number of frequency bins.
389 | fs the sample rate of the audio data.
390 | length the length of the audio data in seconds.
391 | dB an amplitude - {min,max}
392 | */
393 | function loadSpectrogram(data, nblocks, nfreqs, fs, length, db = undefined) {
394 | // calculate the number of textures needed
395 | var maxTexSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
396 | var numTextures = nblocks / maxTexSize;
397 | var currentDB;
398 | if (db)
399 | {
400 | currentDB = db;
401 | }
402 | else
403 | {
404 | currentDB = {'min':-120,'max':0}
405 | }
406 |
407 | // bail if too big for video memory
408 | if (Math.ceil(numTextures) > gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS)) {
409 | alert("Not enough texture units to display spectrogram");
410 | return;
411 | }
412 |
413 | // delete previously allocated textures and VBOs
414 | for (var i in spectrogramTextures) {
415 | gl.deleteBuffer(vertexPositionBuffers[i]);
416 | gl.deleteTexture(spectrogramTextures[i]);
417 | }
418 | gl.deleteBuffer(textureCoordBuffer);
419 |
420 |
421 | vertexPositionBuffers = new Array(Math.ceil(numTextures));
422 | spectrogramTextures = new Array(Math.ceil(numTextures));
423 |
424 | // texture coordinates for all textures are identical
425 | textureCoordBuffer = gl.createBuffer();
426 | gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);
427 | var textureCoordinates = new Float32Array([
428 | 1.0, 1.0,
429 | 1.0, 0.0,
430 | 0.0, 1.0,
431 | 0.0, 0.0
432 | ]);
433 | gl.bufferData(gl.ARRAY_BUFFER, textureCoordinates, gl.STATIC_DRAW);
434 |
435 | // for every texture, calculate vertex indices and texture content
436 | for (var i = 0; i < numTextures; i++) {
437 | // texture position in 0..1:
438 | var minX = i / numTextures;
439 | var maxX = ((i + 1) < numTextures) ? (i + 1) / numTextures : 1;
440 |
441 | // calculate vertex positions, scaled to -1..1
442 | vertexPositionBuffers[i] = gl.createBuffer();
443 | gl.bindBuffer(gl.ARRAY_BUFFER, vertexPositionBuffers[i]);
444 | var vertices = new Float32Array([
445 | maxX * 2 - 1, 1.0,
446 | maxX * 2 - 1, -1.0,
447 | minX * 2 - 1, 1.0,
448 | minX * 2 - 1, -1.0
449 | ]);
450 | gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
451 |
452 | // fill textures with spectrogram data
453 | var blocks = ((i + 1) < numTextures) ? maxTexSize : (numTextures % 1) * maxTexSize;
454 | var chunk = data.subarray(i * maxTexSize * nfreqs, (i * maxTexSize + blocks) * nfreqs);
455 | var tmp = new Float32Array(chunk.length);
456 | for (var x = 0; x < blocks; x++) {
457 | for (var y = 0; y < nfreqs; y++) {
458 | tmp[x + blocks * y] = chunk[y + nfreqs * x];
459 | }
460 | }
461 | spectrogramTextures[i] = gl.createTexture();
462 | gl.activeTexture(gl.TEXTURE0 + i);
463 | gl.bindTexture(gl.TEXTURE_2D, spectrogramTextures[i]);
464 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
465 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
466 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, blocks, nfreqs, 0, gl.LUMINANCE, gl.FLOAT, tmp);
467 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
468 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
469 | }
470 |
471 | // save spectrogram sizes
472 | specSize = new SpecSize(0, length, 0, fs / 2);
473 | specSize.numT = nblocks;
474 | specSize.numF = nfreqs;
475 | specViewSize = new SpecSize(0, length, 0, fs / 2, currentDB['min'], currentDB['max']);
476 |
477 | window.requestAnimationFrame(drawScene);
478 | }
479 |
480 | /* updates the spectrogram and the scales */
481 | function drawScene() {
482 | drawSpectrogram();
483 | drawSpecTimeScale();
484 | drawSpecFrequencyScale();
485 | }
486 |
487 | /* draw the zoomed spectrogram, one texture at a time */
488 | function drawSpectrogram() {
489 | // load the texture coordinates VBO
490 | gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);
491 | gl.vertexAttribPointer(textureCoordAttribute, 2, gl.FLOAT, false, 0, 0);
492 |
493 | // set the current model view matrix
494 | var panX = (specViewSize.centerT() - specSize.centerT()) / specSize.widthT();
495 | var panY = (specViewSize.centerF() - specSize.centerF()) / specSize.widthF();
496 | var zoomX = specSize.widthT() / specViewSize.widthT();
497 | var zoomY = specSize.widthF() / specViewSize.widthF();
498 | var zoomMatrix = [
499 | zoomX, 0.0, -2 * panX * zoomX,
500 | 0.0, zoomY, -2 * panY * zoomY,
501 | 0.0, 0.0, 1.0
502 | ];
503 | gl.uniformMatrix3fv(zoomUniform, gl.FALSE, zoomMatrix);
504 |
505 | // set the current amplitude range to display
506 | gl.uniform2f(ampRangeUniform, specViewSize.minA, specViewSize.maxA);
507 | // set the size of the spectrogram
508 | gl.uniform2f(specSizeUniform, specSize.widthT(), specSize.widthF());
509 | gl.uniform2f(specDataSizeUniform, specSize.numT, specSize.numF);
510 | // set the spectrogram display mode
511 | var specMode = document.getElementById('specMode').value;
512 | gl.uniform1i(specModeUniform, specMode);
513 | var specLogarithmic = document.getElementById('specLogarithmic').checked;
514 | gl.uniform1i(specLogarithmicUniform, specLogarithmic);
515 |
516 | // switch interpolation on or off
517 | var interpolate = document.getElementById('specInterpolation').checked;
518 |
519 | // draw the spectrogram textures
520 | for (var i = 0; i < spectrogramTextures.length; i++) {
521 | gl.activeTexture(gl.TEXTURE0 + i);
522 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, interpolate ? gl.LINEAR : gl.NEAREST);
523 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, interpolate ? gl.LINEAR : gl.NEAREST);
524 | gl.bindTexture(gl.TEXTURE_2D, spectrogramTextures[i]);
525 | gl.uniform1i(samplerUniform, i);
526 |
527 | gl.bindBuffer(gl.ARRAY_BUFFER, vertexPositionBuffers[i]);
528 | gl.vertexAttribPointer(vertexPositionAttribute, 2, gl.FLOAT, false, 0, 0);
529 |
530 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
531 | }
532 | }
533 |
534 | /* format a time in mm:ss.ss
535 |
536 | Attributes:
537 | seconds a time in seconds.
538 |
539 | returns a formatted string containing minutes, seconds, and
540 | hundredths.
541 | */
542 | function formatTime(seconds) {
543 | var minutes = Math.floor(seconds / 60);
544 | var seconds = seconds % 60;
545 | minutes = minutes.toString();
546 | if (minutes.length === 1) {
547 | minutes = "0" + minutes;
548 | }
549 | seconds = seconds.toFixed(2);
550 | if (seconds.length === 4) {
551 | seconds = "0" + seconds;
552 | }
553 | return minutes + ":" + seconds;
554 | }
555 |
556 | /* draw the time scale canvas
557 |
558 | The time scale prints the minimum and maximum currently visible
559 | time and an axis with two ticks. Minimum and maximum time are taken
560 | from specViewSize.(min|max)T.
561 | */
562 | function drawSpecTimeScale() {
563 | var ctx = specTimeScale.getContext('2d');
564 | ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
565 | // draw axis line and two ticks
566 | ctx.fillStyle = "black";
567 | ctx.fillRect(10, 2, ctx.canvas.width - 20, 1);
568 | ctx.fillRect(10, 2, 1, 5);
569 | ctx.fillRect(ctx.canvas.width - 10, 3, 1, 5);
570 | // draw lower time bound
571 | ctx.font = "8px sans-serif";
572 | var text = formatTime(specViewSize.minT);
573 | var textWidth = ctx.measureText(text).width;
574 | ctx.fillText(text, 0, ctx.canvas.height - 2);
575 | // draw upper time bound
576 | var text = formatTime(specViewSize.maxT);
577 | var textWidth = ctx.measureText(text).width;
578 | ctx.fillText(text, ctx.canvas.width - textWidth, ctx.canvas.height - 2);
579 | }
580 |
581 | /* convert a linear frequency coordinate to logarithmic frequency */
582 | function freqLin2Log(f) {
583 | return Math.pow(specSize.widthF(), f / specSize.widthF());
584 | }
585 |
586 | /* format a frequency
587 |
588 | Attributes:
589 | frequency a frequency in Hz.
590 |
591 | returns a formatted string with the frequency in Hz or kHz,
592 | with an appropriate number of decimals. If logarithmic
593 | frequency is enabled, the returned frequency will be
594 | appropriately distorted.
595 | */
596 | function formatFrequency(frequency) {
597 | frequency = document.getElementById('specLogarithmic').checked ? freqLin2Log(frequency) : frequency;
598 |
599 | if (frequency < 10) {
600 | return frequency.toFixed(2) + " Hz";
601 | } else if (frequency < 100) {
602 | return frequency.toFixed(1) + " Hz";
603 | } else if (frequency < 1000) {
604 | return Math.round(frequency).toString() + " Hz";
605 | } else if (frequency < 10000) {
606 | return (frequency / 1000).toFixed(2) + " KHz";
607 | } else if (frequency < 100000) {
608 | return (frequency / 1000).toFixed(1) + " KHz";
609 | } else if (frequency < 1000000) {
610 | return Math.round(frequency / 1000) + " KHz";
611 | } else if (frequency < 10000000) {
612 | return (frequency / 1000000).toFixed(2) + " MHz";
613 | } else if (frequency < 100000000) {
614 | return (frequency / 1000000).toFixed(1) + " MHz";
615 | } else {
616 | return Math.round(frequency / 1000000) + " MHz";
617 | }
618 | }
619 |
620 | /* draw the frequency scale canvas
621 |
622 | The frequency scale prints the minimum and maximum currently
623 | visible frequency and an axis with two ticks. Minimum and maximum
624 | frequency are taken from specViewSize.(min|max)F.
625 | */
626 | function drawSpecFrequencyScale() {
627 | var ctx = specFrequencyScale.getContext('2d');
628 | ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
629 | // draw axis line and two ticks
630 | ctx.fillStyle = "black";
631 | ctx.fillRect(2, 10, 1, ctx.canvas.height - 20);
632 | ctx.fillRect(2, 10, 5, 1);
633 | ctx.fillRect(2, ctx.canvas.height - 10, 5, 1);
634 | // draw upper frequency bound
635 | ctx.font = "8px sans-serif";
636 | var text = formatFrequency(specViewSize.maxF);
637 | var textWidth = ctx.measureText(text).width;
638 | ctx.fillText(text, 8, 14);
639 | // draw lower frequency bound
640 | var text = formatFrequency(specViewSize.minF);
641 | var textWidth = ctx.measureText(text).width;
642 | ctx.fillText(text, 8, ctx.canvas.height - 8);
643 | }
644 |
645 | function eventsInit()
646 | {
647 | /* zoom or pan when on scrolling
648 |
649 | If no modifier is pressed, scrolling scrolls the spectrogram.
650 |
651 | If alt is pressed, scrolling changes the displayed amplitude range.
652 | Pressing shift as well switches X/Y scrolling.
653 |
654 | If ctrl is pressed, scrolling zooms in or out. If ctrl and shift is
655 | pressed, scrolling only zooms the time axis.
656 |
657 | At no time will any of this zoom or pan outside of the spectrogram
658 | view area.
659 | */
660 | specView.onwheel = function(wheel) {
661 | var stepF = specViewSize.widthF() / 100;
662 | var stepT = specViewSize.widthT() / 100;
663 | if (wheel.altKey) {
664 | var center = specViewSize.centerA();
665 | var range = specViewSize.widthA();
666 | range += wheel.shiftKey ? wheel.deltaY / 10 : wheel.deltaX / 10;
667 | range = Math.max(range, 1);
668 | center += wheel.shiftKey ? wheel.deltaX / 10 : wheel.deltaY / 10;
669 | specViewSize.minA = center - range / 2;
670 | specViewSize.maxA = center + range / 2;
671 | } else if (wheel.ctrlKey) {
672 | var deltaT = wheel.deltaY * stepT;
673 | if (specViewSize.widthT() + 2 * deltaT > specSize.widthT()) {
674 | deltaT = (specSize.widthT() - specViewSize.widthT()) / 2;
675 | }
676 | var deltaF = wheel.shiftKey ? 0 : wheel.deltaY * stepF;
677 | if (specViewSize.widthF() + 2 * deltaF > specSize.widthF()) {
678 | deltaF = (specSize.widthF() - specViewSize.widthF()) / 2;
679 | }
680 | specViewSize.minF -= deltaF;
681 | specViewSize.maxF += deltaF;
682 | specViewSize.minT -= deltaT;
683 | specViewSize.maxT += deltaT;
684 | if (specViewSize.minT < specSize.minT) {
685 | specViewSize.maxT += specSize.minT - specViewSize.minT;
686 | specViewSize.minT += specSize.minT - specViewSize.minT;
687 | }
688 | if (specViewSize.maxT > specSize.maxT) {
689 | specViewSize.minT += specSize.maxT - specViewSize.maxT;
690 | specViewSize.maxT += specSize.maxT - specViewSize.maxT;
691 | }
692 | if (specViewSize.minF < specSize.minF) {
693 | specViewSize.maxF += specSize.minF - specViewSize.minF;
694 | specViewSize.minF += specSize.minF - specViewSize.minF;
695 | }
696 | if (specViewSize.maxF > specSize.maxF) {
697 | specViewSize.minF += specSize.maxF - specViewSize.maxF;
698 | specViewSize.maxF += specSize.maxF - specViewSize.maxF;
699 | }
700 | } else {
701 | var deltaT = (wheel.shiftKey ? -wheel.deltaY : wheel.deltaX) * stepT / 10;
702 | if (specViewSize.maxT + deltaT > specSize.maxT) {
703 | deltaT = specSize.maxT - specViewSize.maxT;
704 | }
705 | if (specViewSize.minT + deltaT < specSize.minT) {
706 | deltaT = specSize.minT - specViewSize.minT;
707 | }
708 | var deltaF = (wheel.shiftKey ? wheel.deltaX : -wheel.deltaY) * stepF / 10;
709 | if (specViewSize.maxF + deltaF > specSize.maxF) {
710 | deltaF = specSize.maxF - specViewSize.maxF;
711 | }
712 | if (specViewSize.minF + deltaF < specSize.minF) {
713 | deltaF = specSize.minF - specViewSize.minF;
714 | }
715 | specViewSize.minF += deltaF;
716 | specViewSize.maxF += deltaF;
717 | specViewSize.minT += deltaT;
718 | specViewSize.maxT += deltaT;
719 | }
720 | wheel.preventDefault();
721 | specView.onmousemove(wheel);
722 | window.requestAnimationFrame(drawScene);
723 | }
724 |
725 | /* update the specDataView with cursor position.
726 |
727 | The specDataView should contain the current cursor position in
728 | frequency/time coordinates. It is updated every time the mouse is
729 | moved on the spectrogram.
730 | */
731 | specView.onmousemove = function(mouse) {
732 | var t = specViewSize.scaleT(mouse.layerX / specView.clientWidth);
733 | var f = specViewSize.scaleF(1 - mouse.layerY / specView.clientHeight);
734 | specDataView.innerHTML = formatTime(t) + ", " + formatFrequency(f) + "
" +
735 | specViewSize.centerA().toFixed(2) + " dB " +
736 | "± " + (specViewSize.widthA() / 2).toFixed(2) + " dB";
737 | }
738 |
739 | /* update spectrogram display mode on keypress */
740 | window.onkeypress = function(e) {
741 | var specMode = -1;
742 | if (e.key === 'p') {
743 | specMode = 0;
744 | } else if (e.key === 'n') {
745 | specMode = 1;
746 | } else if (e.key === 'd') {
747 | specMode = 2;
748 | } else if (e.key === 'm') {
749 | specMode = 3;
750 | }
751 | // prevent the default action of submitting the GET parameters.
752 | e.which = e.which || e.keyCode;
753 | if (e.which === 13) {
754 | e.preventDefault();
755 | }
756 |
757 | document.getElementById('specMode').value = specMode;
758 | window.requestAnimationFrame(drawScene);
759 | }
760 | }
761 |
--------------------------------------------------------------------------------