UNPKG

dspjs

Version:

DSP.js is a comprehensive digital signal processing library for javascript

765 lines (639 loc) 27.5 kB
<!DOCTYPE html> <html> <head> <!-- Load JQuery and JQuery-UI --> <link type="text/css" href="css/hot-sneaks/jquery-ui-1.8.custom.css" rel="stylesheet" /> <script type="text/javascript" src="js/jquery-1.4.2.min.js"></script> <script type="text/javascript" src="js/jquery-ui-1.8.custom.min.js"></script> <!-- Load Processing.js --> <script language="javascript" src="js/processing.js"></script> <script language="javascript" src="js/init.js"></script> <!-- Load DSP.js --> <script src="../dsp.js"></script> <script> $(function() { // Amplitude $('#amp' ).slider({ orientation: 'vertical', range: 'min', value: 0.7, min: 0, max: 1, step: 0.01, slide: changeAmplitude }); // Amp. Envelope $('#A' ).slider({ orientation: 'vertical', range: 'min', value: 0.02, min: 0.01, max: 1.0, step: 0.01 }); $('#D' ).slider({ orientation: 'vertical', range: 'min', value: 0.6, min: 0, max: 1.0, step: 0.01 }); $('#S' ).slider({ orientation: 'vertical', range: 'min', value: 0.3, min: 0, max: 1.0, step: 0.01 }); $('#R' ).slider({ orientation: 'vertical', range: 'min', value: 0.8, min: 0, max: 5.0, step: 0.01 }); // Filter $('#cutoff').slider({ orientation: 'vertical', range: 'min', value: 880, min: 60, max: 5000, step: 1, slide: changeFilter }); $('#res' ).slider({ orientation: 'vertical', range: 'min', value: 0.1, min: 0, max: 1, step: 0.01, slide: changeFilter }); // Filter Envelope $('#FA' ).slider({ orientation: 'vertical', range: 'min', value: 0.02, min: 0.01, max: 1.0, step: 0.01, slide: changeFilterEnvelope }); $('#FD' ).slider({ orientation: 'vertical', range: 'min', value: 0.6, min: 0, max: 1.0, step: 0.01, slide: changeFilterEnvelope }); $('#FS' ).slider({ orientation: 'vertical', range: 'min', value: 0.3, min: 0, max: 1.0, step: 0.01, slide: changeFilterEnvelope }); $('#FR' ).slider({ orientation: 'vertical', range: 'min', value: 0.8, min: 0, max: 5.0, step: 0.01, slide: changeFilterEnvelope }); // Osc Mix $('#osc_mix').slider({ orientation: 'vertical', range: 'min', value: 0.5, min: -0.1, max: 1.1, step: 0.01, slide: changeAmplitude }); changeAmplitude(); // reset amp and mix // Debug toggle $("#debug_toggle").click(function() { $("#debug").html(""); }); // curve automation toggles $("#curve_cutoff_toggle").click(function() { if ( $("#curve_cutoff_toggle").attr("checked") ) { SHOW_SEQUENCER = true; CURVE_EDITOR = true; } }); $("#curve_res_toggle").click(function() { if ( $("#curve_res_toggle").attr("checked") ) { SHOW_SEQUENCER = true; CURVE_EDITOR = true; } }); // Filter type radio selection $("input[name='filter_type']").change(changeFilter); changeFilter(); // reset filter to slider defaults // Oscillator waveform select $("#osc1_waveform").change(changeOsc1); $("#osc2_waveform").change(changeOsc2); // Oscillator osctave select $("#osc1_octave").change(changeOsc1); $("#osc2_octave").change(changeOsc2); // Oscillator osctave select $("#osc1_semi").change(changeOsc1); $("#osc2_semi").change(changeOsc2); changeOsc1(); // reset oscillator to select defaults changeOsc2(); }); </script> <style type="text/css"> body, * { font-family: Arial, sans-serif; font-size: 12px; } .control { padding: 5px; border-left: 1px dotted #ccc; float: left; margin-right: 5px; } .control table td { padding: 10px; width: 20px; color: #999; font-size: 12px; } .control h3 { margin: 0; padding:0; font-size: 12px; margin-bottom: 10px; } .control #debug { background-color: #FFF; font-size: 12px; width: 300px; } .slider { margin-bottom: 16px; width: 8px; } .ui-slider .ui-slider-handle { width: 8px; margin-left: 3px; } </style> </head> <body> <script> var SHOW_SEQUENCER = true; var CURVE_EDITOR = false; // Setup shared variables var sampleRate = 44100; var bufferSize = 1024; var bufferTime = Math.floor(1000 / (sampleRate / bufferSize)); var frequency = 440; var amplitude = 0.7; // Default amplitude at 70% var changeAmplitude = function() { amplitude = $('#amp').slider('option', 'value'); $('#ampLevel').html(Math.round(amplitude * 100) + "%"); var mix = $('#osc_mix').slider('option', 'value'); mix = mix > 1 ? 1 : mix; mix = mix < 0 ? 0 : mix; osc2_amplitude = 1 - (osc1_amplitude = mix); osc1_amplitude *= amplitude; osc2_amplitude *= amplitude; }; var filter = new IIRFilter2(0, 0, 0, sampleRate); var changeFilter = function() { filter.set($('#cutoff').slider('option', 'value'), $('#res').slider('option', 'value')); filter.type = $("input[name='filter_type']:checked").val(); }; var changeFilterEnvelope = function() { }; var osc; var osc1; var osc2; var osc1_waveform; var osc2_waveform; var osc1_octave; var osc2_octave; var osc1_amplitude; var osc2_amplitude; var osc1_semi = 0; var osc2_semi = 0; var osc1_cent = 0; var osc2_cent = 0; var osc1_frequency; var osc2_frequency; var changeOsc1 = function() { osc1_waveform = parseInt($("#osc1_waveform").val()); osc1_octave = parseInt($("#osc1_octave").val()); osc1_semi = parseInt($("#osc1_semi").val()); }; var changeOsc2 = function() { osc2_waveform = parseInt($("#osc2_waveform").val()); osc2_octave = parseInt($("#osc2_octave").val()); osc2_semi = parseInt($("#osc2_semi").val()); }; var bpm = 60; // Beats per minute var sequencerStep = 0; // Sequencer step postion var sequencerTicksPerMillisecond = bpm * 8 / 60000; // Sequencer Ticks each millisecond. A Tick is a partial step. var polyphony = 8; // Number of oscillators that can be played at the same time var oscPool = new Array(); // Pool of active oscillators var sequencer = new Array(32); for ( var i = 0; i < sequencer.length; i++ ) { sequencer[i] = new Array(24); for( var j = 0; j < sequencer[i].length; j++ ) { sequencer[i][j] = false; } } var sequencerNoteOn = new Array(32); for ( var i = 0; i < sequencerNoteOn.length; i++ ) { sequencerNoteOn[i] = new Array(24); for( var j = 0; j < sequencerNoteOn[i].length; j++) { sequencerNoteOn[i][j] = false; } } var curveOver = 0; var curveCutoff = new Array(17); var curveRes = new Array(17); for ( var i = 0; i < 17; i++ ) { curveCutoff[i] = sampleRate / 4; curveRes[i] = 0.1; } var oct = 3; // Root Octave // Borrowed from F1LTER's code var midiNoteFreq = /* 0 */ [ 16.35, 17.32, 18.35, 19.45, 20.6, 21.83, 23.12, 24.5, 25.96, 27.5, 29.14, 30.87, /* 1 */ 32.7, 34.65, 36.71, 38.89, 41.2, 43.65, 46.25, 49, 51.91, 55, 58.27, 61.74, /* 2 */ 65.41, 69.3, 73.42, 77.78, 82.41, 87.31, 92.5, 98, 103.83, 110, 116.54, 123.47, /* 3 */ 130.81, 138.59, 146.83, 155.56, 164.81, 174.61, 185, 196, 207.65, 220, 233.08, 246.94, /* 4 */ 261.63, 277.18, 293.66, 311.13, 329.63, 349.23, 369.99, 392, 415.3, 440, 466.16, 493.88, /* 5 */ 523.25, 554.37, 587.33, 622.25, 659.26, 698.46, 739.99, 783.99, 830.61, 880, 932.33, 987.77, /* 6 */ 1046.5, 1108.73, 1174.66, 1244.51, 1318.51, 1396.91, 1479.98, 1567.98, 1661.22, 1760, 1864.66, 1975.53, /* 7 */ 2093, 2217.46, 2349.32, 2489.02, 2637.02, 2793.83, 2959.96, 3135.96, 3322.44, 3520, 3729.31, 3951.07, /* 8 */ 4186.01, 4434.92, 4698.64, 4978 ]; var writeCount = 0; var timeStamp = (new Date()).getTime(); var silence = new Float32Array(bufferSize); // Setup audio channel var a = new Audio(); if ( typeof a.mozSetup == 'function' ) { a.mozSetup(1, sampleRate); } var timePerWrite = 0; var programStart; var audioWriter = function() { var startTime = (new Date()).getTime(); var currentTime = (new Date()).getTime(); var mergedOsc; // undefined var filterOn = $('#filter_toggle').attr('checked'); if ( programStart === undefined ) { programStart = new Date(); } // keep same pace as a real time //while ( (new Date() - programStart) / 1000 * sampleRate / bufferSize >= writeCount ) { for ( var i = 0; i < oscPool.length; i++ ) { // Generate new frame of the signal if ( oscPool[i].envelope.isActive() ) { oscPool[i].osc1.generate(); // This generates a new frameBuffer of signal and applies the envelope as it goes. oscPool[i].osc2.generate(); // combine osc1 and osc2 oscPool[i].osc1.addSignal(oscPool[i].osc2.signal); // apply the filter if ( filterOn ) { filter.addEnvelope(oscPool[i].filterEnvelope); filter.process(oscPool[i].osc1.signal); } // apply the envelope oscPool[i].envelope.process(oscPool[i].osc1.signal); // Merge down oscPool if ( typeof mergedOsc === 'undefined' ) { mergedOsc = oscPool[i].osc1; } else { mergedOsc.addSignal(oscPool[i].osc1.signal); } } else { oscPool[i] = null; // Mark for deletion } } // Delete marked oscillators from the pool var tmpPool = new Array(); for ( var i = 0; i < oscPool.length; i++ ) { if ( oscPool[i] != null ) { tmpPool.push(oscPool[i]); } } oscPool = tmpPool; // Set the global osc object if ( typeof mergedOsc !== 'undefined' ) { osc = mergedOsc; } else { osc.signal = silence; //osc.signal = undefined; } // Flush buffer a.mozWriteAudio([]); // Write next audio frame a.mozWriteAudio(osc.signal); writeCount++; sequencerStep += (currentTime - timeStamp) * sequencerTicksPerMillisecond; //} timeStamp = (new Date()).getTime(); var endTime = (new Date()).getTime(); timePerWrite = endTime - startTime; } </script> <script target="#signal" type="application/processing"> float scale = 90.0; void setup() { size(512, 200); // setup oscillator to hold the merged signal of osc1 and osc2 osc = new Oscillator(osc1_waveform, 440, 0, bufferSize, sampleRate); setInterval(audioWriter, bufferTime); // start audioWriter timer frameRate(10); } void draw() { var startTime = (new Date()).getTime(); int sequencerCol = Math.floor(sequencerStep) % 32; background(0, 0, 30); var curCol = Math.floor(sequencerCol/2); var nextCol = (curCol + 1) % 17; var alpha = ((sequencerStep % 32) / 2) - curCol; // should be a value between 0 and 0.99 if ( $("#curve_cutoff_toggle").attr("checked") ) { // interpolate value var curveVal = curveCutoff[curCol] + (curveCutoff[nextCol] - curveCutoff[curCol]) * alpha; $('#cutoff').slider('option', 'value', curveVal); changeFilter(); } if ( $("#curve_res_toggle").attr("checked") ) { var curveVal = curveRes[curCol] + (curveRes[nextCol] - curveRes[curCol]) * alpha; $('#res').slider('option', 'value', curveVal); changeFilter(); } // Check sequencer column for notes to trigger for ( int i = 0; i < 24; i ++ ) { if ( sequencer[sequencerCol][i] == true && !sequencerNoteOn[sequencerCol][i] ) { sequencerNoteOn[sequencerCol][i] = true; frequency = midiNoteFreq[12*oct+(24-i)]; osc1_frequency = midiNoteFreq[12 * osc1_octave + (24 - i) + osc1_semi] + osc1_cent; osc2_frequency = midiNoteFreq[12 * osc2_octave + (24 - i) + osc2_semi] + osc2_cent; // Add oscillator to the pool var index = oscPool.length; var osc1 = new Oscillator(osc1_waveform, osc1_frequency, osc1_amplitude, bufferSize, sampleRate); var osc2 = new Oscillator(osc2_waveform, osc2_frequency, osc2_amplitude, bufferSize, sampleRate); var adsr = new ADSR($('#A').slider('option', 'value'), $('#D').slider('option', 'value'), $('#S').slider('option', 'value'), bufferTime / 1000, $('#R').slider('option', 'value'), sampleRate); var fadsr = new ADSR($('#FA').slider('option', 'value'), $('#FD').slider('option', 'value'), $('#FS').slider('option', 'value'), bufferTime / 1000, $('#FR').slider('option', 'value'), sampleRate); oscPool[index] = {}; oscPool[index].osc1 = osc1; oscPool[index].osc2 = osc2; oscPool[index].envelope = adsr; oscPool[index].filterEnvelope = fadsr; } if ( sequencer[sequencerCol][i] == false && sequencerCol > 0 ) { sequencerNoteOn[sequencerCol-1][i] = false; } else if ( sequencer[sequencerCol][i] == false && sequencerCol == 0 ) { sequencerNoteOn[sequencerNoteOn.length -1][i] = false; } } if ( SHOW_SEQUENCER ) { /* * Sequencer View * * Show step sequencer * */ stroke(200, 255, 255, 20); // Render horozontal lines for (int i = 0; i < 24; i++ ) { line(0, i * (height/24), width, i * (height/24)); } // Render vertical lines for (int i = 0; i < 32; i++ ) { if ( i % 8 == 0 ) { strokeWeight(2); stroke(200, 255, 255, 60); } else { stroke(200, 255, 255, 20); strokeWeight(1); } line(i * (width/32), 0, i * (width/32), height); // Render sequencer step position if ( i == Math.floor(sequencerStep % 32) ) { noStroke(); stroke(200, 255, 255, 60); //rect(i * (width/32), 0, width/32, height); //stroke(255, 0, 0); line((sequencerStep % 32) * (width/32), 0, (sequencerStep % 32) * (width/32), height); } } noStroke(); for (int c = 0; c < sequencer.length; c++ ) { for (int r = 0; r < sequencer[c].length; r++ ) { if ( sequencer[c][r] == true ) { if ( CURVE_EDITOR ) { fill(200, 255, 255, 40); } else { fill(200, 255, 255, 100); } if ( c == Math.floor(sequencerStep % 32) ) { rect( (c - 1) * (width/32), (r -1) * (height/24), width/32 * 3, height/24 * 3); } rect(c * (width/32), r * (height/24), width/32, height/24); } } } if ( CURVE_EDITOR ) { strokeWeight(2); var cutoffScale = 50; var resScale = 100; for ( int i = 0; i < 17; i++ ) { fill(0, 0, 30); // Cut off curve if ( $("#curve_cutoff_toggle").attr("checked") ) { stroke(200, 255, 255, 100); } else { stroke(200, 255, 255, 40); } var xPos = i * (width/16); var yPos = height/2 - curveCutoff[i] / cutoffScale; if (i < 16 ) { var xPosNext = (i+1) * (width/16); var yPosNext = height/2 - curveCutoff[i+1] / cutoffScale; line(xPos, yPos, xPosNext, yPosNext); } if ( mouseX > xPos - 7 && mouseX < xPos + 7 && mouseY > yPos -7 && mouseY < yPos + 7 ) { ellipse(xPos, yPos, 7, 7); curveOver = i; } else { ellipse(xPos, yPos, 5, 5); } // Resonance Curve if ( $("#curve_res_toggle").attr("checked") ) { stroke(200, 255, 255, 100); } else { stroke(200, 255, 255, 40); } var xPos = i * (width/16); var yPos = height - curveRes[i] * resScale; if (i < 16 ) { var xPosNext = (i+1) * (width/16); var yPosNext = height - curveRes[i+1] * resScale; line(xPos, yPos, xPosNext, yPosNext); } if ( mouseX > xPos - 7 && mouseX < xPos + 7 && mouseY > yPos -7 && mouseY < yPos + 7 ) { ellipse(xPos, yPos, 7, 7); curveOver = i; } else { ellipse(xPos, yPos, 5, 5); } // draw center line stroke(200, 255, 255, 60); line(0, height/2, width, height/2); } strokeWeight(1); if ( DRAGGING ) { if ( mouseY < height / 2 ) { curveCutoff[curveOver] = (height/2 - mouseY) * cutoffScale; } else if ( mouseY > height / 2 ) { curveRes[curveOver] = (height - mouseY) / resScale; } } } } else { /* * Signal View * * Show signal output * */ strokeWeight(1.5); stroke(155, 200, 220); for ( int i = 0; i < width/4 - 1; i++ ) { line(4*i, scale + 10 - osc.signal[4*i] * scale, 4*(i+1), scale + 10 - osc.signal[4*(i+1)] * scale); } } var endTime = (new Date()).getTime(); if ( $('#debug_toggle').attr('checked') ) { $('#debug').html( "Time/frame (milliseconds): " + ( endTime - startTime ) + "<br>" + "Time/write (milliseconds): " + ( timePerWrite ) + "<br>" + "Audio frame size: " + bufferSize + "<br>" + "Audio frames written: " + writeCount + "<br>" + "Time (seconds): " + ((writeCount * bufferSize) / sampleRate) + "<br>" + "Sequencer BPM: " + bpm + "<br>" + "Sequencer steps/second: " + sequencerTicksPerMillisecond * 1000 + "<br>" + "Active oscillators (polyphony): " + oscPool.length + "<br>" + "Frame rate: " + frameRate.toFixed(1) ); } } boolean DRAGGING = false; void mousePressed() { if ( SHOW_SEQUENCER && CURVE_EDITOR ) { DRAGGING = true; } } void mouseReleased() { if ( SHOW_SEQUENCER && !CURVE_EDITOR ) { // Plot notes in sequencer var col = parseInt(mouseX / (width/32)); var row = parseInt(mouseY / (height/24)); sequencer[col][row] = !sequencer[col][row]; } else if ( SHOW_SEQUENCER && CURVE_EDITOR && DRAGGING ) { DRAGGING = false; } } void keyPressed() { if ( key == 's' ) { SHOW_SEQUENCER = !SHOW_SEQUENCER; } if ( key == 'c' ) { CURVE_EDITOR = !CURVE_EDITOR; } } </script> <h1>Sequencer</h1> <p>Sequencer controlled oscillator with envelope and filter.</p> <p><b>Click in the sequencer grid to plot notes. Switch to the signal view and adjust the envelope and filter to see the resulting waveforms.</b></p> <p>Keyboard Commands:</p> <ul> <li><b>S</b> - toggle between sequencer/signal view</li> <li><b>C</b> - toggle curve editor</li> <li><b>W</b> - change waveform</li> </ul> <div style="float:left;margin-right: 5px;"><canvas id="signal" width="200px" height="200px"></canvas></div> <div class="control"> <h3><input type="checkbox" id="debug_toggle" /> Debug</h3> <div id="debug"></div> </div> <div style="clear:both;"></div> <div class="control"> <h3>Oscillators</h3> <table> <tr> <td style="width: 40px;"><h3>Osc 1</h3></td> <td style="width: 100px;"> <select id="osc1_waveform"> <option value="1">Sine</option> <option value="2">Triangle</option> <option value="3">Saw</option> <option value="4" selected="selected">Square</option> </select> </td> <td style="width: 80px;"> Oct <select id="osc1_octave"> <option value="8">8</option> <option value="7">7</option> <option value="6">6</option> <option value="5">5</option> <option value="4" selected="selected">4</option> <option value="3">3</option> <option value="2">2</option> <option value="1">1</option> <option value="0">0</option> </select> </td> <td style="width: 80px;"> Semi <select id="osc1_semi"> <option value="12">12</option> <option value="11">11</option> <option value="10">10</option> <option value="9">9</option> <option value="8">8</option> <option value="7">7</option> <option value="6">6</option> <option value="5">5</option> <option value="4">4</option> <option value="3">3</option> <option value="2">2</option> <option value="1">1</option> <option value="0" selected="selected">0</option> </select> </td> </tr> <tr> <td><h3>Osc 2</h3></td> <td> <select id="osc2_waveform"> <option value="1">Sine</option> <option value="2">Triangle</option> <option value="3">Saw</option> <option value="4">Square</option> </select> </td> <td> Oct <select id="osc2_octave"> <option value="8">8</option> <option value="7">7</option> <option value="6">6</option> <option value="5">5</option> <option value="4" selected="selected">4</option> <option value="3">3</option> <option value="2">2</option> <option value="1">1</option> <option value="0">0</option> </select> </td> <td style="width: 100px;"> Semi <select id="osc2_semi"> <option value="12">12</option> <option value="11">11</option> <option value="10">10</option> <option value="9">9</option> <option value="8">8</option> <option value="7">7</option> <option value="6">6</option> <option value="5">5</option> <option value="4">4</option> <option value="3">3</option> <option value="2">2</option> <option value="1">1</option> <option value="0" selected="selected">0</option> </select> </td> </tr> </table> </div> <div class="control"> <h3>Mix</h3> <table> <tr> <td><div id="osc_mix" class="slider" style="height: 70px;"></div></td> </tr> </table> </div> <div style="clear:both;"></div> <div class="control"> <h3>Amp.</h3> <table> <tr> <td><div id="amp" class="slider"></div><div id="ampLevel">70%</div></td> </tr> </table> </div> <div class="control"> <h3>Amp. Envelope</h3> <table> <tr> <td><div id="A" class="slider"></div>A</td> <td><div id="D" class="slider"></div>D</td> <td><div id="S" class="slider"></div>S</td> <td><div id="R" class="slider"></div>R</td> </tr> </table> </div> <div class="control"> <h3><input type="checkbox" id="filter_toggle" checked/> Filter</h3> <table> <tr> <td><div id="cutoff" class="slider"></div>F</td> <td><div id="res" class="slider"></div>Q</td> <td style="width: 60px; vertical-align: top;"> <div id="filter_type"> <input type="radio" id="hp12" name="filter_type" value="1"/> <label for="hp12">HP12</label><br /> <input type="radio" id="br12" name="filter_type" value="3"/> <label for="bp12">BR12</label><br /> <input type="radio" id="bp12" name="filter_type" value="2"/> <label for="bp12">BP12</label><br /> <input type="radio" id="lp12" name="filter_type" value="0" checked="checked"/> <label for="lp12">LP12</label><br /> </div> <div style="margin-top: 10px;"> <h3>Automate</h3> <input type="checkbox" id="curve_cutoff_toggle" /> F<br/> <input type="checkbox" id="curve_res_toggle" /> Q </div> </td> </tr> <tr> </tr> </table> </div> <div class="control"> <h3>Filter Envelope</h3> <table> <tr> <td><div id="FA" class="slider"></div>A</td> <td><div id="FD" class="slider"></div>D</td> <td><div id="FS" class="slider"></div>S</td> <td><div id="FR" class="slider"></div>R</td> </tr> </table> </div> </body> </html>