/** * Additive synthesis * by Kazuki Maeda * Last-Modified: May 4, 2017 */ var canvasWidth = 600, canvasHeight = 200; var canvasMargin = 20; var controllerNum = 30; var canvas; var wave; var audioCtx; var oscillator; var jsbThread; var jsbClavi, jsbPedal; window.onload = function(){ init(); } function init(){ audioCtx = new (window.AudioContext || window.webkitAudioContext)(); // canvas canvas = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); canvas.setAttribute('width', canvasWidth+canvasMargin*2); canvas.setAttribute('height', canvasHeight+canvasMargin*2); document.getElementById('canvas').removeChild(document.getElementById('canvas').firstChild); document.getElementById('canvas').appendChild(canvas); var box = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); box.setAttribute('width', ''+canvasWidth); box.setAttribute('height', canvasHeight); box.setAttribute('x', canvasMargin); box.setAttribute('y', canvasMargin); box.setAttribute('stroke', 'white'); canvas.appendChild(box); var midline = document.createElementNS('http://www.w3.org/2000/svg', 'line'); midline.setAttribute('x1', canvasMargin); midline.setAttribute('y1', canvasMargin+canvasHeight/2); midline.setAttribute('x2', canvasMargin+canvasWidth); midline.setAttribute('y2', canvasMargin+canvasHeight/2); midline.setAttribute('stroke', 'white'); canvas.appendChild(midline); wave = document.createElementNS('http://www.w3.org/2000/svg', 'polyline'); wave.setAttribute('stroke', '#7FFF00'); wave.setAttribute('stroke-width', '3'); wave.setAttribute('fill', 'none'); canvas.appendChild(wave); // controller var controller = document.getElementById('controller'); var controllerTable = document.createElement('table'); controller.appendChild(controllerTable); for(var i = 0; i < controllerNum/2; ++i){ var tr = document.createElement('tr'); controllerTable.appendChild(tr); for(var j = 0; j < 2; ++j){ var td = document.createElement('td'); tr.append(td); td.style.textAlign = 'right'; td.appendChild(document.createTextNode('$r_{' + (i*2+j+1) + '}=$ ')); var textbox = document.createElement('input'); textbox.id = 'r' + (i*2+j+1); textbox.type = 'text'; textbox.value = i == 0 && j == 0 ? 0.5 : 0; textbox.size = 6; td.appendChild(textbox); td = document.createElement('td'); tr.append(td); td.style.textAlign = 'right'; var range = document.createElement('input'); range.id = 'r' + (i*2+j+1) + 'slider'; range.style.verticalAlign = 'middle'; range.type = 'range'; range.value = i == 0 && j == 0 ? 50 : 0; range.min = 0, range.max = 100; td.appendChild(range); td = document.createElement('td'); tr.append(td); td.style.textAlign = 'right'; td.appendChild(document.createTextNode('$\\phi_{' + (i*2+j+1) + '}=$ ')); textbox = document.createElement('input'); textbox.id = 'phi' + (i*2+j+1); textbox.type = 'text'; textbox.value = 0; textbox.size = 3; td.appendChild(textbox); td = document.createElement('td'); tr.append(td); td.style.textAlign = 'right'; range = document.createElement('input'); range.id = 'phi' + (i*2+j+1) + 'slider'; range.style.verticalAlign = 'middle'; range.type = 'range'; range.value = 0, range.min = 0, range.max = 360, range.step = 6; td.appendChild(range); } } document.getElementById('presetset').addEventListener('click', preset); document.getElementById('play').addEventListener('mousedown', play); document.getElementById('play').addEventListener('mouseup', stop); document.getElementById('jsb').addEventListener('click', playstopJSB); document.getElementById('vol').addEventListener('keypress', updateVolK); document.getElementById('vol').addEventListener('blur', updateVol); document.getElementById('volslider').addEventListener('change', updateVolSlider); document.getElementById('volslider').addEventListener('input', updateVolSlider); for(var k = 1; k <= controllerNum; ++k){ document.getElementById('r'+k).addEventListener('keypress', updateVolK); document.getElementById('r'+k).addEventListener('blur', updateVol); document.getElementById('r'+k+'slider').addEventListener('change', updateVolSlider); document.getElementById('r'+k+'slider').addEventListener('input', updateVolSlider); document.getElementById('phi'+k).addEventListener('keypress', updatePhiK); document.getElementById('phi'+k).addEventListener('blur', updatePhi); document.getElementById('phi'+k+'slider').addEventListener('change', updatePhiSlider); document.getElementById('phi'+k+'slider').addEventListener('input', updatePhiSlider); } draw(); MathJax.typeset(); } function draw(){ wave.setAttribute('stroke', '#7FFF00'); var A = parseFloat(document.getElementById('vol').value); var r = [], phi = []; for(var k = 1; k <= controllerNum; ++k){ r[k] = parseFloat(document.getElementById('r'+k).value); phi[k] = parseFloat(document.getElementById('phi'+k).value); } var val = 0; for(var k = 1; k <= controllerNum; ++k) val += r[k] * Math.sin(Math.PI*phi[k]/180); val *= A; if(val > 1){ val = 1; wave.setAttribute('stroke', 'red'); } if(val < -1){ val = -1; wave.setAttribute('stroke', 'red'); } var points = '' + canvasMargin + ',' + (canvasMargin+parseFloat(canvasHeight)/2*(1.0-val)); for(var j = 1; j <= canvasWidth; ++j){ val = 0; for(var k = 1; k <= controllerNum; ++k) val += r[k] * Math.sin(2.0*Math.PI*k*j/canvasWidth+Math.PI*phi[k]/180); val *= A; if(val > 1){ val = 1; wave.setAttribute('stroke', 'red'); } if(val < -1){ val = -1; wave.setAttribute('stroke', 'red'); } points += ' ' + (canvasMargin+parseFloat(canvasWidth)*j/canvasWidth) + ',' + (canvasMargin+parseFloat(canvasHeight)/2*(1.0-val)); } wave.setAttribute('points', points); } function updateVol(e){ document.getElementById(e.target.id + 'slider').value = parseFloat(e.target.value)*100; draw(); } function updateVolK(e){ if(e.keyCode == 13) updateVol(e); } function updateVolSlider(e){ document.getElementById(e.target.id.slice(0, -6)).value = parseFloat(e.target.value)/100; draw(); } function updatePhi(e){ document.getElementById(e.target.id + 'slider').value = parseFloat(e.target.value); draw(); } function updatePhiK(e){ if(e.keyCode == 13) updatePhi(e); } function updatePhiSlider(e){ document.getElementById(e.target.id.slice(0, -6)).value = parseFloat(e.target.value); draw(); } function play(e){ oscillator = createOscillator(parseFloat(document.getElementById('freq').value)); oscillator.start(); } function stop(e){ oscillator.stop(); } function preset(e){ var w = document.getElementById('preset').value; if(w == 'sine'){ document.getElementById('r1').value = 1.0; document.getElementById('r1slider').value = 100; document.getElementById('phi1').value = 0; document.getElementById('phi1slider').value = 0; for(var k = 2; k <= controllerNum; ++k){ document.getElementById('r'+k).value = 0; document.getElementById('r'+k+'slider').value = 0; document.getElementById('phi'+k).value = 0; document.getElementById('phi'+k+'slider').value = 0; } } else if(w == 'triangle'){ for(var k = 1; k <= controllerNum; ++k){ if(k%2){ document.getElementById('r'+k).value = 8.0/k/k/Math.PI/Math.PI; document.getElementById('r'+k+'slider').value = 8.0/k/k/Math.PI/Math.PI*100; document.getElementById('phi'+k).value = parseInt(k/2)%2 ? 180 : 0; document.getElementById('phi'+k+'slider').value = parseInt(k/2)%2 ? 180 : 0; } else { document.getElementById('r'+k).value = 0; document.getElementById('r'+k+'slider').value = 0; document.getElementById('phi'+k).value = 0; document.getElementById('phi'+k+'slider').value = 0; } } } else if(w == 'square'){ for(var k = 1; k <= controllerNum; ++k){ if(k%2){ document.getElementById('r'+k).value = 4.0/k/Math.PI/1.18; document.getElementById('r'+k+'slider').value = 4.0/k/Math.PI*100/1.18; document.getElementById('phi'+k).value = 0; document.getElementById('phi'+k+'slider').value = 0; } else { document.getElementById('r'+k).value = 0; document.getElementById('r'+k+'slider').value = 0; document.getElementById('phi'+k).value = 0; document.getElementById('phi'+k+'slider').value = 0; } } } else if(w == 'saw'){ for(var k = 1; k <= controllerNum; ++k){ document.getElementById('r'+k).value = 2.0/k/Math.PI/1.18; document.getElementById('r'+k+'slider').value = 2.0/k/Math.PI*100/1.18; document.getElementById('phi'+k).value = 0; document.getElementById('phi'+k+'slider').value = 0; } } draw(); } function createOscillator(freq){ var osc = audioCtx.createOscillator(); osc.frequency.value = freq; var cosine = new Float32Array(controllerNum+1); var sine = new Float32Array(controllerNum+1); cosine[0] = 0, sine[0] = 0; var vol = parseFloat(document.getElementById('vol').value); var r = [], phi = []; r[0] = 0, phi[0] = 0; for(var k = 1; k <= controllerNum; ++k){ r[k] = parseFloat(document.getElementById('r'+k).value); phi[k] = parseFloat(document.getElementById('phi'+k).value); cosine[k] = r[k]*Math.sin(Math.PI/180*phi[k]); sine[k] = r[k]*Math.cos(Math.PI/180*phi[k]); } osc.setPeriodicWave(audioCtx.createPeriodicWave(cosine, sine)); var gain = audioCtx.createGain(); var g = 0; for(var j = 0; j < canvasWidth; ++j){ // find peak value var val = 0; for(var k = 1; k <= controllerNum; ++k) val += r[k] * Math.sin(2.0*Math.PI*k*j/canvasWidth+Math.PI*phi[k]/180); g = Math.max(g, val); } gain.gain.value = vol*g; osc.connect(gain); gain.connect(audioCtx.destination); return osc; } function playstopJSB(e){ if(e.target.value == 'Stop'){ e.target.value = 'J.S. Bach'; stopJSB(); } else { e.target.value = 'Stop'; playJSB(); } } var t; function playJSB(){ t = 0; jsbThread = setInterval('jsb();', 220); } function stopJSB(){ jsbClavi.stop(); jsbPedal.stop(); clearInterval(jsbThread); document.getElementById('jsb').value = 'J.S. Bach'; } var nst = 100; // stop var nct = 1000; // cont. var nc2 = -33, ndf2 = -32, nd2 = -31, nef2 = -30, ne2 = -29, nf2 = -28, ngf2 = -27, ng2 = -26, naf2 = -25, na2 = -24, nbf2 = -23, nb2 = -22; var nc3 = -21, ndf3 = -20, nd3 = -19, nef3 = -18, ne3 = -17, nf3 = -16, ngf3 = -15, ng3 = -14, naf3 = -13, na3 = -12, nbf3 = -11, nb3 = -10; var nc4 = -9, ndf4 = -8, nd4 = -7, nef4 = -6, ne4 = -5, nf4 = -4, ngf4 = -3, ng4 = -2, naf4 = -1, na4 = 0, nbf4 = 1, nb4 = 2; var nc5 = 3, ndf5 = 4, nd5 = 5, nef5 = 6, ne5 = 7, nf5 = 8, ngf5 = 9, ng5 = 10, naf5 = 11, na5 = 12, nbf5 = 13, nb5 = 14; // BWV645 var jsbScoreClavi = [nbf3, nct, nef4, nf4, ng4, nct, ng4, nct, nf4, nct, naf4, nct, ng4, nct, nbf3, nct, naf3, nct, ng4, nef4, nf4, nct, naf3, nct, ng3, nct, nd4, nct, nef4, nct, nst, nct, nbf3, nct, nef4, nf4, ng4, nct, ng4, nct, nf4, nct, naf4, nct, ng4, nct, nbf3, nct, naf3, nct, ng4, nef4, nf4, nct, naf3, nct, ng3, nct, nd4, nct, nef4, nct, nst, nct, nbf4, nct, nbf4, nct, nct, nct, naf4, ng4, nf4, nef4, nf4, nef4, nd4, nc4, nbf3, nct, nc4, nd4, nef4, nf4, ng4, nf4, naf4, ng4, nf4, nef4, ng4, nct, nf4, nct, nst, nct, nbf3, nct, ng4, nct, na4, nct, nct, nct, nbf4, nct, nef4, nd4, nef4, nct, nst, nct, nc4, nct, na4, nct, nbf4, nct, nct, nct, nc5, nct, nef4, nd4, nef4, nct, nst, nct, nef5, nct, nd5, nc5, nbf4, nct, nbf4, na4, nbf4, nct, nbf4, na4, ng4, nf4, nef4, nd4, nc4, nbf3, nc4, nd4, nef4, nct, nef4, nd4, nef4, nct, nef4, na4, nbf4, nc5, nbf4, na4, ng4, nf4, nbf4, nct, nf4, nct, nd4, nct, nc4, nbf3, nbf3, nef4, nd4, nc4, nd4, nct, nbf3, nct, ng3, nct, nct, na3, na3, nct, nct, na3, nbf3, nct, nct, nct, nst, nct, nbf3, nct, nef4]; var jsbScorePedal = [nct, nct, nef2, nct, nct, nct, nef2, nct, nct, nct, nef2, nct, nct, nct, ng2, nct, nct, nct, naf2, nct, nct, nct, nbf2, nct, nct, nct, nef2, nct, nct, nct, nst, nct, nct, nct, nef2, nct, nct, nct, nef2, nct, nct, nct, nef2, nct, nct, nct, ng2, nct, nct, nct, naf2, nct, nct, nct, nbf2, nct, nct, nct, nef2, nct, nct, nct, nst, nct, nct, nct, ng2, nct, nct, nct, nc3, nct, nct, nct, nbf2, nct, nct, nct, naf2, nct, nct, nct, ng2, nct, nct, nct, nef2, nct, nct, nct, nbf2, nct, nct, nct, nd3, nct, nct, nct, nef3, nct, nct, nct, nd3, nct, nct, nct, nc3, nct, nct, nct, nef3, nct, nct, nct, nf3, nct, nct, nct, ng3, nct, nct, nct, na3, nct, nct, nct, nf3, nct, nct, nct, nbf3, nct, nct, nct, na3, nct, nct, nct, ng3, nct, nct, nct, nf3, nct, nct, nct, nef3, nct, nct, nct, nd3, nct, nct, nct, nc3, nct, nct, nct, nef3, nct, nct, nct, nd3, nct, nbf2, nct, nf3, nct, nf2, nct, nef2, nct, na2, nct, nbf2, nct, nd2, nct, nef2, nct, nc2, nct, nf2, nct, nct, nct, nbf2, nct, nc3, nct, nbf2, nct, naf2, nct, nef2]; function jsb(){ if(jsbScoreClavi[t] == nst){ if(t > 0) jsbClavi.stop(); } else if(jsbScoreClavi[t] != nct) { if(t > 0) jsbClavi.stop(); jsbClavi = createOscillator(440*Math.pow(2.0, parseFloat(jsbScoreClavi[t])/12)); jsbClavi.start(); } if(jsbScorePedal[t] == nst){ if(t > 2) jsbPedal.stop(); } else if(jsbScorePedal[t] != nct) { if(t > 2) jsbPedal.stop(); jsbPedal = createOscillator(440*Math.pow(2.0, parseFloat(jsbScorePedal[t])/12)); jsbPedal.start(); } ++t; if(t >= jsbScoreClavi.length) t = 3; // repeat }