dspjs
Version:
DSP.js is a comprehensive digital signal processing library for javascript
765 lines (639 loc) • 27.5 kB
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>