Skip to content

Instantly share code, notes, and snippets.

@alexmacy
Last active March 7, 2020 03:23
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexmacy/41bf2c3727c59a3366528807c2c708b2 to your computer and use it in GitHub Desktop.
Save alexmacy/41bf2c3727c59a3366528807c2c708b2 to your computer and use it in GitHub Desktop.
Web Audio Theremin & Oscilloscope
license: mit

This is a theremin with a built-in oscilloscope, made using the Web Audio API. This was my first time working with Web Audio and the next thing I want to integrate is using multiple oscillators and filters to manipulate the waveform when moving the mouse horizontally. Until then, vertical movement changes frequency while horizontal movement changes the gain.

** Update: Added the ability to select a waveform from a drop-down at the top left.

<!DOCTYPE html>
<html>
<head>
<style>
body {margin: 0; overflow:hidden;}
#wave-select {position: absolute; left:10px; top:10px;}
svg {border: black; cursor: none; background: #FCF4B5}
circle {fill: black; fill-opacity: .75;}
#wave {fill: none; stroke: #F1896F; stroke-width:2;}
#ticker {fill: green; stroke: #222; stroke-width:.5; fill-opacity: .2;}
</style>
<script src="//d3js.org/d3.v4.min.js"></script>
</head>
<body>
<div id="wave-select">
Waveform:
<select id="waveType" onchange="oscillator.type = this.value">
<option value="sine">Sine</option>
<option value="square">Square</option>
<option value="sawtooth">Sawtooth</option>
<option value="triangle">Triangle</option>
</select>
</div>
</body>
<script>
var audioCtx = new (window.AudioContext || window.webkitAudioContext)(),
oscillator = audioCtx.createOscillator(),
gainNode = audioCtx.createGain(),
analyser = audioCtx.createAnalyser();
oscillator.connect(audioCtx.destination);
gainNode.connect(audioCtx.destination);
oscillator.connect(gainNode);
oscillator.connect(analyser);
var bufferLength = analyser.frequencyBinCount;
var dataArray = new Uint8Array(analyser.frequencyBinCount);
gainNode.gain.value = -1
oscillator.frequency.value = 0
oscillator.start(0);
var width = innerWidth,
height = innerHeight;
var scaleY = d3.scalePow().exponent(-.25).domain([height,10]).range([100,5000]),
scaleX = d3.scaleLinear().domain([0,bufferLength]).range([0,width]),
gainScale = d3.scaleLinear().domain([0,width]).range([-1,0]),
octaves = [110,220,440,880,1760,3520]
var tickerHist = [0]
var line = d3.line()
.x(function(d, i) {return scaleX(i)})
.y(function(d) {return (d-122.5) * (gainNode.gain.value+1)})
var tickerLine = d3.area()
.x(function(d, i) {return (i - tickerHist.length)*2})
.y0(height)
.y1(function(d) {return scaleY.invert(d)})
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
svg.on("mouseover", oscStart)
.on("mouseout", oscStop)
.on("mousemove", oscChange)
.on("touchstart", oscStart)
.on("touchend", oscStop)
.on("touchmove", oscChange)
function oscStart() {
if (oscillator.noteOn) oscillator.noteOn(0);
circle.style("visibility", "visible")
}
function oscStop() {
circle.style("visibility", "hidden")
oscillator.frequency.value = 0;
gainNode.gain.value = -1
updateWave(100)
}
function oscChange() {
circle.attr("cx", d3.event.pageX).attr("cy", d3.event.pageY)
ticker.attr("transform", `translate(${d3.event.pageX},0)`)
oscillator.frequency.value = scaleY(d3.event.pageY);
gainNode.gain.value = gainScale(d3.event.pageX);
updateWave(1);
}
document.addEventListener("touchmove", function(e) {e.preventDefault();}, false);
var octavesLines = svg.append("g")
.attr("class", "octaves").selectAll("path")
.data(octaves)
.enter().append("path")
.style("stroke", "#9CD2B8")
.attr("stroke-dasharray",[5,5])
.attr("d", function(d) {return `M0 ${scaleY.invert(d)} H ${width+11}`})
var circle = svg.append("circle")
.attr("r", 10)
.style("visibility", "hidden")
var freq = svg.append("text")
.attr("x", 10)
.attr("y", height - 10)
.text('Frequency: -')
var waveShape = svg.append("g").append("path")
.datum(dataArray)
.attr("id", "wave")
.attr("transform", `translate(0,${height/2})`)
var ticker = svg.append("g").append("path")
.attr("transform", `translate(${width+10},0)`)
.attr("id", "ticker")
.datum(tickerHist)
updateWave()
d3.timer(function() {
updateTicker(oscillator.frequency.value);
}, 100)
function updateTicker(fVal) {
tickerHist.push(fVal)
if (tickerHist.length > width*1.1) {
tickerHist.shift()
}
ticker.attr("d", tickerLine)
}
function updateWave(duration) {
analyser.getByteTimeDomainData(dataArray);
waveShape.transition().duration(duration).ease(d3.easeLinear).attr("d",line)
freq.text(`Frequency: ${d3.format(',.0f')(oscillator.frequency.value)} Hz`);
}
</script>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment