From 25fec83f6f7db468721a38adb2c84a0c0a2121f1 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Tue, 6 Mar 2018 13:28:12 +0100 Subject: displaying waveform and spectrogram --- client/draw.js | 197 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 client/draw.js (limited to 'client/draw.js') diff --git a/client/draw.js b/client/draw.js new file mode 100644 index 0000000..a9c182d --- /dev/null +++ b/client/draw.js @@ -0,0 +1,197 @@ +import { + browser, requestAudioContext, + randint, randrange, clamp, +} from './lib/util' + +import mouse from './lib/mouse' +import color from './lib/color' + +let w, h +let rx, ry + +const canvas = document.createElement('canvas') +const ctx = canvas.getContext('2d') + +document.body.appendChild(canvas) +document.body.addEventListener('resize', resize) +resize() +recenter() +requestAnimationFrame(animate) + +function resize(){ + w = canvas.width = window.innerWidth + h = canvas.height = window.innerHeight + clear() +} +function recenter(){ + rx = randint(w), ry = randint(h) +} +function animate(t){ + requestAnimationFrame(animate) + ctx.save() + ctx.globalAlpha = 0.0001 + ctx.translate(w/2, h/2) + ctx.rotate(0.1) + ctx.translate(-rx, -ry) + ctx.drawImage(canvas, 0, 0) + ctx.restore() +} +function clear(n, x, y, ww, hh){ + ctx.fillStyle = 'rgba(255,255,255,' + (n || 0.5) + ')' + ctx.fillRect(x || 0, y || 0, ww || w, hh || h) + recenter() +} +function triangle(px,py,r){ + setTimeout( () => tri(px,py,r), Math.random()*10) + // setTimeout( () => tri(px,py,r), Math.random()*200) + // setTimeout( () => tri(px,py,r), Math.random()*300) +} +function tri(px, py, r) { + ctx.save() + ctx.globalCompositeOperation = 'multiply' + ctx.fillStyle = color((px+py)/(w+h), 0, 1, 1) + function p(){ + let theta = randrange(0, Math.PI*2) + let x = px + Math.cos(theta) * r + let y = py + Math.sin(theta) * r + return { x, y } + } + ctx.beginPath() + const p0 = p(), p1 = p(), p2 = p() + ctx.moveTo(p0.x, p0.y) + ctx.lineTo(p1.x, p1.y) + ctx.lineTo(p2.x, p2.y) + ctx.lineTo(p0.x, p0.y) + ctx.fill() + ctx.restore() +} +function line(y){ + ctx.beginPath() + ctx.moveTo(0, y) + ctx.lineTo(w, y) + ctx.strokeStyle = "#888" + ctx.strokeWidth = 1 + ctx.stroke() +} +function dot(x, y, r){ + ctx.fillStyle = "#f00" + ctx.beginPath() + ctx.moveTo(x, y) + ctx.arc(x, y, r, 0, 2*Math.PI) + ctx.fill() +} +function waveform(pcm, sr, pos, zoom){ + sr = sr || 44100 + pos = pos || 0 + + var width = w + var height = Math.floor(h/4) + var half_height = Math.floor(height/2) + var x0 = 0 + var y0 = 20 + var ymid = y0 + half_height + var pixels_per_second = 1024 + var max_width_in_seconds = width / pixels_per_second + var max_width_in_samples = max_width_in_seconds * sr + var pcm_length = pcm.length + var len = Math.min(pcm_length, max_width_in_samples) + var pcm_step = sr / pixels_per_second + var i + ctx.save() + + clear(1, x0, y0, width, height) + + line(ymid) + ctx.beginPath() + for (i = 0; i < width; i += 0.5) { + var si = Math.floor(pcm_step * i + pos) + if (si > pcm_length) break + var val = pcm[si] // -1, 1 + // ctx.moveTo(x0 + i, ymid) + ctx.lineTo(x0 + i, ymid + val * half_height) + } + ctx.strokeStyle = "rgba(250,20,0,0.9)" + ctx.strokeWidth = 1 + ctx.stroke() + ctx.restore() +} + +const signalWindows = require('signal-windows').windows +const FFTJS = require('fft.js') +const fft_size = 1024 +const fft = new FFTJS(fft_size) + +function toSpectrum(pcm){ + const ham = signalWindows.construct('ham', fft_size) + const pcm_in = new Array(fft_size) + const pcm_length = pcm.length + const pcm_q_length = Math.ceil(pcm_length / fft_size) * fft_size + + let i, j, fft_out, spec = []; + for (i = 0; i < pcm_q_length; i += fft_size/4) { + for (j = 0; j < fft_size; j++) { + pcm_in[j] = pcm[i+j] * ham[j] || 0 + } + fft_out = fft.createComplexArray() + fft.realTransform(fft_out, pcm_in) + spec.push(fft_out) + } + + return spec +} +function spectrum(pcm, sr){ + sr = sr || 44100 + const spec = toSpectrum(pcm) + + ctx.save() + + const scratch = document.createElement('canvas') + scratch.width = spec.length + scratch.height = fft_size + const scratchCtx = scratch.getContext('2d') + + var imageData = ctx.createImageData(scratch.width, scratch.height) + var data = imageData.data + + let i, j, u, v, _r, _i, col, spec_len = spec.length + for (i = 0; i < spec_len; i++) { + col = spec[i] + for (j = 0; j < fft_size; j++) { + u = (j * spec_len + i) * 4 + v = j * 2 + _r = col[v] + _i = col[v+1] + // red - real part + data[u] = _r * 127 + 127 + // green - imag part + data[u+1] = _i * 127 + 127 + // blue - magnitude + data[u+2] = Math.sqrt(Math.pow(_r, 2) + Math.pow(_i, 2)) * 128 + 127 + // data[u] = 128 + // data[u+1] = 128 + data[u+2] = 128 + data[u+3] = 255 + } + } + + scratchCtx.putImageData(imageData, 0, 0) + + var pcm_length = pcm.length + var pixels_per_second = 1024 + + const width = Math.round(pcm_length / sr * pixels_per_second) // ok not really this + const height = Math.floor(h*3/4) + + const x0 = 0 + const y0 = Math.floor(h/4) + 20 + clear(1, x0, y0, w, height) + ctx.drawImage(scratch, x0, y0, width, height) + + ctx.restore() +} + +export default { + canvas, ctx, + triangle, clear, line, dot, + waveform, spectrum +} \ No newline at end of file -- cgit v1.2.3-70-g09d2