block by alexmacy 41bf2c3727c59a3366528807c2c708b2

Web Audio Theremin & Oscilloscope

Full Screen

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.

index.html

<!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>