diff --git a/all.js b/all.js new file mode 100644 index 0000000..f4be1d0 --- /dev/null +++ b/all.js @@ -0,0 +1,507 @@ +'use strict'; + +let libraryInfo = [ + { + file: 'khrang.ogg', + author: 'Jerobeam Fenderson', + title: 'Khrậng', + link: 'https://www.youtube.com/watch?v=vAyCl4IHIz8', + swap: true, + }, + { + file: 'oscillofun.ogg', + author: 'ATOM DELTA', + title: 'Oscillofun', + link: 'https://www.youtube.com/watch?v=o4YyI6_y6kw', + invert: true, + }, + { + file: 'alpha_molecule.ogg', + author: 'Alexander Taylor', + title: 'The Alpha Molecule', + link: 'https://www.youtube.com/watch?v=XM8kYRS-cNk', + invert: true, + }, +]; + +let libraryDict = {}; +for (let e of libraryInfo) { + libraryDict[e.file] = e; +} + +let query = parseq(location.search); +if (!query.file) { + query = libraryInfo[0]; +} + +let file = query.file; +let audioUrl = './' + file; +let swap = query.swap; +let invert = query.invert; + +let audioCtx = new AudioContext(); +let audioData = null; +let quadIndex = null; +let vertexIndex = null; +let nSamples = 4096; +let scratchBuffer = new Float32Array(nSamples*4); +let doBloom = false; +let frameBuffer = null; +let lineTexture = null; +let blurTexture = null; +let blurTexture2 = null; +let outQuadArray = null; + +function axhr(url, callback, progress) { + let request = new XMLHttpRequest(); + request.open('GET', url, true); + request.responseType = 'arraybuffer'; + request.onprogress = progress; + request.onload = function() { + audioCtx.decodeAudioData(request.response, function(buffer) { + callback(buffer); + }); + } + request.send(); +} + +window.onload = function() { + let canvas = $('c'), + gl = initGl(canvas); + + updatePageInfo(); + + gl.lineShader = CreateShader(gl, getText('vsLine'), getText('fsLine')); + gl.blurShader = CreateShader(gl, getText('vsBlurTranspose'), getText('fsBlurTranspose')); + gl.outputShader = CreateShader(gl, getText('vsOutput'), getText('fsOutput')); + + let loop = function() { + draw(gl); + requestAnimationFrame(loop); + } + + axhr(audioUrl, function(buffer) { + $('htmlAudio').volume = 0.5; + $('htmlAudio').src = audioUrl; + $('htmlAudio').play(); + + quadIndex = makeQuadIndex(gl); + vertexIndex = makeVertexIndex(gl); + outQuadArray = makeOutQuad(gl); + + { + let tmp = makeFrameBuffer(gl, canvas.width, canvas.height); + frameBuffer = tmp.frameBuffer; + lineTexture = tmp.lineTexture; + blurTexture = tmp.blurTexture; + blurTexture2 = tmp.blurTexture2; + } + + audioData = prepareAudioData(gl, buffer); + loop(); + + }, function(e) { + console.log('progress: ' + e.loaded + ' / ' + e.total); + }); +}; + +function parseq(search) { + search = search.replace(/^\?/, ''); + let obj = {}; + for (let pair of search.split('&')) { + pair = pair.split('='); + obj[decodeURIComponent(pair[0])] = + pair.length > 1 ? decodeURIComponent(pair[1]) : true; + } + return obj; +} + +function dumpq(obj) { + return Object.keys(obj).map(function(key) { + if (obj[key] === true) { + return encodeURIComponent(key); + } else { + return encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]); + } + }).join('&'); +} + +function updatePageInfo() { + if (file in libraryDict) { + let info = libraryDict[file], + text = document.createTextNode(info.author + ' — ' + info.title + ' '), + songInfo = $('songInfo'), + a = document.createElement('a'), + linkText = document.createTextNode('[link]'); + + a.appendChild(linkText); + a.href = info.link; + songInfo.innerHTML = ''; + songInfo.appendChild(text); + songInfo.appendChild(a); + } + + let ul = $('playList'); + ul.innerHTML = ''; + for (let song of libraryInfo) { + let a = document.createElement('a'), + li = document.createElement('li'); + a.appendChild(document.createTextNode(song.title)); + + let q = {file: song.file}; + if (song.swap) { q.swap = true; } + if (song.invert) { q.invert = true; } + a.href = '?' + dumpq(q); + + li.appendChild(a); + ul.appendChild(li); + } +} + +function initGl(canvas) { + let gl = canvas.getContext('webgl'); + gl.clearColor( 0.0, 0.0, 0.0, 1.0 ); + return gl; +} + +function CreateShader(gl, vsSource, fsSource) { + if (typeof WebGLRenderingContext !== 'function' || + !(gl instanceof WebGLRenderingContext)) { + throw new Error('CreateShader: no WebGL context'); + } + + let vs = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vs, vsSource); + gl.compileShader(vs); + if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) { + let infoLog = gl.getShaderInfoLog(vs); + gl.deleteShader(vs); + throw new Error('CreateShader, vertex shader compilation:\n' + infoLog); + } + + let fs = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fs, fsSource); + gl.compileShader(fs); + if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) { + let infoLog = gl.getShaderInfoLog(fs); + gl.deleteShader(vs); + gl.deleteShader(fs); + throw new Error('CreateShader, fragment shader compilation:\n' + infoLog); + } + + let program = gl.createProgram(); + + gl.attachShader(program, vs); + gl.deleteShader(vs); + + gl.attachShader(program, fs); + gl.deleteShader(fs); + + gl.linkProgram(program); + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + let infoLog = gl.getProgramInfoLog(program); + gl.deleteProgram(program); + throw new Error('CreateShader, linking:\n' + infoLog); + } + + return program; +}; + +function makeQuadIndex(gl) { + let index = new Int16Array(nSamples*2); + for (let i = index.length; i--; ) { + index[i] = i; + } + let vbo = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, vbo); + gl.bufferData(gl.ARRAY_BUFFER, index, gl.STATIC_DRAW); + gl.bindBuffer(gl.ARRAY_BUFFER, null); + + return vbo; +} + +function makeVertexIndex(gl) { + let len = (nSamples-1)*2*3, + index = new Uint16Array(len); + for (let i = 0, pos = 0; i < len; ) { + index[i++] = pos; + index[i++] = pos+2; + index[i++] = pos+1; + index[i++] = pos+1; + index[i++] = pos+2; + index[i++] = pos+3; + pos += 4; + } + let vbo = gl.createBuffer(); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, vbo); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index, gl.STATIC_DRAW); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); + + return vbo; +} + +function makeOutQuad(gl) { + let data = new Int16Array([ + -1, -1, 0, 0, + -1, 1, 0, 1, + 1, -1, 1, 0, + 1, 1, 1, 1, + ]); + let vbo = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, vbo); + gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); + gl.bindBuffer(gl.ARRAY_BUFFER, null); + + return vbo; +} + +function makeTargetTexture(gl, width, height) { + let texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); + gl.generateMipmap(gl.TEXTURE_2D); + gl.bindTexture(gl.TEXTURE_2D, null); + return texture; +} + +function makeFrameBuffer(gl, width, height) { + let frameBuffer = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer); + frameBuffer.width = 1024; + frameBuffer.height = 1024; + + gl.renderBuffer = gl.createRenderbuffer(); + + return { + frameBuffer: frameBuffer, + lineTexture: makeTargetTexture(gl, frameBuffer.width, frameBuffer.height), + blurTexture: makeTargetTexture(gl, frameBuffer.width, frameBuffer.height), + blurTexture2: makeTargetTexture(gl, frameBuffer.width, frameBuffer.height), + }; +} + +function prepareAudioData(gl, buffer) { + let left = buffer.getChannelData(0), + right = buffer.getChannelData(1); + + if (swap) { + let tmp = left; + left = right; + right = tmp; + } + + let vbo = gl.createBuffer(); + return { + vbo: vbo, + left: left, + right: right, + }; +} + +function loadWaveAtPosition(gl, position) { + position += 1/120; + position = Math.floor(position*44100); + let end = Math.min(audioData.left.length, position+nSamples) - 1, + len = end - position; + let subArr = scratchBuffer, + left = audioData.left, + right = audioData.right; + for (let i = 0; i < len; i++) { + let t = i*8, + p = i+position; + subArr[t] = subArr[t+2] = subArr[t+4] = subArr[t+6] = left[p]; + subArr[t+1] = subArr[t+3] = subArr[t+5] = subArr[t+7] = right[p]; + } + gl.bindBuffer(gl.ARRAY_BUFFER, audioData.vbo); + gl.bufferData(gl.ARRAY_BUFFER, subArr, gl.STATIC_DRAW); + gl.bindBuffer(gl.ARRAY_BUFFER, null); +} + +function $(id) { return document.getElementById(id); } + +function getText(id) { + let c = $(id); + return c && c.firstChild && c.firstChild.data; +} + +function activateTargetTexture(gl, texture) { + gl.bindRenderbuffer(gl.RENDERBUFFER, gl.renderBuffer); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, frameBuffer.width, frameBuffer.height); + + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, gl.renderBuffer); + + gl.bindTexture(gl.TEXTURE_2D, null); + gl.bindRenderbuffer(gl.RENDERBUFFER, null); +} + +function draw(gl) { + loadWaveAtPosition(gl, $('htmlAudio').currentTime); + + let width = 800, + height = 800; + + if (!doBloom) { + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.viewport(0, 0, width, height); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + drawLine(gl, gl.lineShader); + } else { + + gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer); + activateTargetTexture(gl, lineTexture); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + gl.viewport(0, 0, width, height); + drawLine(gl, gl.lineShader); + + { // generate mipmap + gl.bindTexture(gl.TEXTURE_2D, lineTexture); + gl.generateMipmap(gl.TEXTURE_2D); + gl.bindTexture(gl.TEXTURE_2D, null); + } + + // downscale + activateTargetTexture(gl, blurTexture2); + gl.viewport(0, 0, width/2, height/2); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + drawTexture(gl, lineTexture, width, gl.outputShader); + + // blur x + activateTargetTexture(gl, blurTexture); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + drawTexture(gl, blurTexture2, width/2, gl.blurShader); + + // blur y + activateTargetTexture(gl, blurTexture2); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + drawTexture(gl, blurTexture, width/2, gl.blurShader); + + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + gl.viewport(0, 0, width, height); + drawTexture(gl, lineTexture, width, gl.outputShader); + drawTexture(gl, blurTexture2, width/2, gl.outputShader, 0.5); + } +} + +function drawLine(gl, shader) { + gl.useProgram(shader); + { + let tmpPos = gl.getUniformLocation(shader, 'uInvert'); + if (tmpPos && tmpPos !== -1) { + gl.uniform1f(tmpPos, (invert) ? -1 : 1); + } + tmpPos = gl.getUniformLocation(shader, 'uSize'); + if (tmpPos && tmpPos !== -1) { + gl.uniform1f(tmpPos, 0.012); + } + tmpPos = gl.getUniformLocation(shader, 'uIntensity'); + if (tmpPos && tmpPos !== -1) { + gl.uniform1f(tmpPos, 1); + } + } + + let attribs = []; + + { + gl.bindBuffer(gl.ARRAY_BUFFER, quadIndex); + let idxAttr = gl.getAttribLocation(shader, 'aIdx'); + if (idxAttr > -1) { + gl.enableVertexAttribArray(idxAttr); + gl.vertexAttribPointer(idxAttr, 1, gl.SHORT, false, 2, 0); + attribs.push(idxAttr); + } + } + + { + gl.bindBuffer(gl.ARRAY_BUFFER, audioData.vbo); + let tmpPos = gl.getAttribLocation(shader, 'aStart'); + if (tmpPos > -1) { + gl.enableVertexAttribArray(tmpPos); + gl.vertexAttribPointer(tmpPos, 2, gl.FLOAT, false, 8, 0); + attribs.push(tmpPos); + } + + tmpPos = gl.getAttribLocation(shader, 'aEnd'); + if (tmpPos > -1) { + gl.enableVertexAttribArray(tmpPos); + gl.vertexAttribPointer(tmpPos, 2, gl.FLOAT, false, 8, 8*4); + attribs.push(tmpPos); + } + } + + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE); + + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, vertexIndex); + gl.drawElements(gl.TRIANGLES, (nSamples-1)*2, gl.UNSIGNED_SHORT, 0); + + gl.disable(gl.BLEND); + + for (let a of attribs) { + gl.disableVertexAttribArray(a); + } + + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); + gl.bindBuffer(gl.ARRAY_BUFFER, null); + gl.useProgram(null); +} + +function drawTexture(gl, texture, size, shader, alpha) { + alpha = alpha || 1; + gl.useProgram(shader); + + let attribs = []; + gl.bindBuffer(gl.ARRAY_BUFFER, outQuadArray); + + { + let tmpPos = gl.getAttribLocation(shader, 'aPos'); + if (tmpPos > -1) { + gl.enableVertexAttribArray(tmpPos); + gl.vertexAttribPointer(tmpPos, 2, gl.SHORT, false, 8, 0); + attribs.push(tmpPos); + } + + tmpPos = gl.getAttribLocation(shader, 'aST'); + if (tmpPos > -1) { + gl.enableVertexAttribArray(tmpPos); + gl.vertexAttribPointer(tmpPos, 2, gl.SHORT, false, 8, 4); + attribs.push(tmpPos); + } + } + + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, texture); + { + let tmpPos = gl.getUniformLocation(shader, 'uTexture'); + if (tmpPos && tmpPos !== -1) { + gl.uniform1i(tmpPos, 0); + } + tmpPos = gl.getUniformLocation(shader, 'uSize'); + if (tmpPos && tmpPos !== -1) { + gl.uniform1f(tmpPos, size); + } + tmpPos = gl.getUniformLocation(shader, 'uAlpha'); + if (tmpPos && tmpPos !== -1) { + gl.uniform1f(tmpPos, alpha); + } + } + + gl.enable(gl.BLEND); + gl.blendFunc(gl.ONE, gl.SRC_ALPHA); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + + for (let a of attribs) { + gl.disableVertexAttribArray(a); + } + + gl.bindBuffer(gl.ARRAY_BUFFER, null); + gl.bindTexture(gl.TEXTURE_2D, null); + gl.useProgram(null); +} diff --git a/index.html b/index.html index 1e6659a..e20ec2e 100644 --- a/index.html +++ b/index.html @@ -12,513 +12,25 @@ ul { text-align: center; font-size: 20px; } li { display: inline; margin: 0 10px 0 10px; } a { color: #2d2; } - + -