<div id="synth" tabindex="1">
  Click to start/stop the theremin.
</div>

<div id="controls">
  <ul>
    <li>
      <label>
        Waveform:
        <select id="oscillator-type">
          <option value="sine">sine</option>
          <option value="square">square</option>
          <option value="sawtooth">sawtooth</option>
          <option value="triangle">triangle</option>
        </select>
      </label>
    </li>
    <li>
      <label>
        LFO freq:
        <input id="lfo-freq" type="range" min="0" max="10" step="0.25" value="0"/>
      </label>
    </li>
    <li>
      <label>
        Gain:
        <input id="gain" type="range" />
      </label>
    </li>
    <li>
      <label>
        Filter:
        <select id="filter">
          <option value="none">none</option>
          <option value="lowpass">lowpass</option>
          <option value="highpass">highpass</option>
          <option value="bandpass">bandpass</option>
          <option value="lowshelf">lowshelf</option>
          <option value="highshelf">highshelf</option>
          <option value="peaking">peaking</option>
          <option value="notch">notch</option>
          <option value="allpass">allpass</option>
        </select>
      </label>
    </li>
    <li>
      <label>
        Distortion:
        <input id="distortion" type="range" min="0" max="100" step="1" value="0"/>
      </label>
    </li>
  </ul>
</div>
@mixin uisection ($background) {
  position: absolute;
  width: 300px;
  height: 300px;
  background-color: $background; 
  color: white;
}

#synth {
  @include uisection(#222222);
}

#controls {
  @include uisection(#444444);
  left: 320px;
}

select, input {
  margin-left: 0.5em;
}

li {
  list-style-type: none;
  margin-bottom: 1em;
}
// Create an audio context
var audio_context = window.AudioContext || window.webkitAudioContext;
var context = new audio_context();

// Create Audio Nodes
var oscillator = context.createOscillator();
var lfo = context.createOscillator();
var lfo_amp = context.createGain();
var filter = context.createBiquadFilter();
var distortion = context.createWaveShaper();
distortion.curve = makeDistortionCurve(0);
distortion.oversample = '4x';
var volume = context.createGain();

// Create Audio Routing Graph
lfo.connect(lfo_amp);
lfo_amp.connect(oscillator.frequency);
oscillator.connect(distortion);
distortion.connect(volume);
volume.connect(context.destination);

// Get references to UI elements
var synth = document.getElementById("synth");
var oscillatorTypeSelector = document.getElementById("oscillator-type");
var LFOFrequencyControl = document.getElementById("lfo-freq");
var gainControl = document.getElementById("gain");
var filterControl = document.getElementById("filter");
var distortionControl = document.getElementById("distortion");

// Initialise State
var isOn = false;
gainControl.value = 0;
changeLFOGain();

// Set Theremin Event Listeners
synth.addEventListener("click", function(){
  if (isOn) {
    oscillator.stop();
    lfo.stop();
    isOn = false;
  }
  else {
    oscillator.start();
    lfo.start();
    isOn = true;
  }
});

synth.addEventListener("mousemove", changeVolume);
synth.addEventListener("mousemove", changeOscillatorPitch);

// Set Controls Event Listeners
oscillatorTypeSelector.addEventListener("change", changeWaveform);
LFOFrequencyControl.addEventListener("change", changeLFOFrequency);
gainControl.addEventListener("change", changeLFOGain);
filterControl.addEventListener("change", changeFilter);
distortionControl.addEventListener("change", changeDistortion);

// Theremin Event Handlers
function changeOscillatorPitch(event) {
  oscillator.frequency.value = 350 + 2 * event.clientX;
}

function changeVolume(event) {
  volume.gain.value = event.offsetY / synth.clientHeight;
}

// Controls Event Handlers
function changeWaveform() {
  oscillator.type = oscillatorTypeSelector.value;
}

function changeLFOFrequency() {
   lfo.frequency.value = LFOFrequencyControl.value;
}

function changeLFOGain() {
  lfo_amp.gain.value = gainControl.value;
}

function changeFilter() {
  console.log("changeFilter")
  if (filterControl.value === "none") {
    filter.disconnect(distortion);
    oscillator.disconnect(filter);
    oscillator.connect(distortion);
  }
  else {
    console.log("disconnecting")
    filter.type = filterControl.value;
    oscillator.disconnect(distortion);
    oscillator.connect(filter);
    filter.connect(distortion);
  }
}

function changeDistortion() {
  distortion.curve = makeDistortionCurve(parseInt(distortionControl.value));
}

function makeDistortionCurve(amount) {
  var k = typeof amount === 'number' ? amount : 0,
    n_samples = 44100,
    curve = new Float32Array(n_samples),
    deg = Math.PI / 180,
    i = 0,
    x;
  for ( ; i < n_samples; ++i ) {
    x = i * 2 / n_samples - 1;
    curve[i] = ( 3 + k ) * x * 20 * deg / ( Math.PI + k * Math.abs(x) );
  }
  return curve;
};

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.