Skip to content

Instantly share code, notes, and snippets.

@alexmacy
Last active November 27, 2016 05:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexmacy/5f5d6f75a44f27f19a2eebf7771ed73e to your computer and use it in GitHub Desktop.
Save alexmacy/5f5d6f75a44f27f19a2eebf7771ed73e to your computer and use it in GitHub Desktop.
Phase Shifting
license: mit

This is an experiment in manipulating audio at the bit level, using the Web Audio API. It's also a kind of homage to a couple Steve Reich pieces: Come Out and It's Gonna Rain. Where Reich spliced audio tape, this offsets the second track on each repetition, thus creating the phase.

  • The select menu at the top left selects between the two pieces.
  • Another select menu adjusts the phase offset/rate of change.
  • The 'One Step' button lets you skip forward if you get impatient.
  • And the 'Reset' button brings the two tracks back into phase.

Also see: I Am Sitting in a Room - Alvin Lucier

<!DOCTYPE html>
<html>
<head>
<style>
button {margin-right: 10px;}
path {fill: none; stroke: black; stroke-width: .5;}
rect {fill: red; fill-opacity: .5; visibility: hidden;}
</style>
<script src="//d3js.org/d3.v4.min.js"></script>
</head>
<body style="margin: 10px">
<div>
<select id="file-select">
<option value="https://raw.githubusercontent.com/alexmacy/Audio-clips/master/come%20out.wav">Come out</option>
<option value="https://raw.githubusercontent.com/alexmacy/Audio-clips/master/rain.wav">It's Gonna Rain</option>
</select>
<button id="load-file" onclick="loadFromSelect()" style="margin-right: 50px">Load File</button>
Phase offset:
<select id="offset-select">
<option value=".001">1 millisecond</option>
<option value=".005">5 milliseconds</option>
<option value=".01">10 milliseconds</option>
<option value=".05">50 milliseconds</option>
<option value=".1">100 milliseconds</option>
</select>
<button id="step" onclick="phase()">One Step</button>
<button id="reset" onclick="reset()">Reset</button>
<button id="start-stop" onclick="playing ? stopSound() : playSound()">Start</button>
</div>
<svg></svg>
</body>
<script>
var width = Math.max(940, innerWidth-20),
height = 470,
waveHeight = 200;
var audioCtx = new (window.AudioContext || window.webkitAudioContext)(),
beatData, sampRateAdj, source, playing;
var offset = 0;
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height)
var x = d3.scaleLinear().range([0, width]),
y = d3.scaleLinear().range([waveHeight, 0]);
var wave = d3.line()
.curve(d3.curveMonotoneX)
.x(function(d, i) {return x(i)})
.y(function(d) {return y(d)})
var waveShape1 = svg.append("g")
.append("path")
.datum([])
.attr("d", wave)
.attr("transform", "translate(0,30)");
var waveShape2 = svg.append("g")
.attr("transform", "translate(0," + (waveHeight + 30) + ")")
.append("path")
.datum([])
.attr("d", wave)
.attr("transform", "translate(0,30)");
var position = svg.append("rect")
.attr("y", 20)
.attr("width", 20)
.attr("height", height-20)
loadFromSelect()
function importAudio(url) {
stopSound();
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
request.onload = function() {
audioCtx.decodeAudioData(request.response, function(buffer) {
loadAudio(buffer);
},
function(){alert("Error decoding audio data")}
);
}
request.send();
function loadAudio(buffer) {
beatData = buffer;
sampRateAdj = Math.floor(beatData.length/(width*10));
var waveData = drawWithAvgs(0)
x.domain([0, waveData.length]);
y.domain(d3.extent(beatData.getChannelData(0)));
waveShape1.datum(waveData).attr("d", wave)
waveShape2.datum(waveData).attr("d", wave)
}
}
function playSound() {
d3.select("#start-stop").text("stop")
playing = true;
source = audioCtx.createBufferSource();
source.buffer = beatData;
source.connect(audioCtx.destination);
source.start();
position.style("visibility", "visible")
.transition().duration(0)
.attr("x", 0)
.transition().duration(beatData.duration * 1000).ease(d3.easeLinear)
.attr("x", width)
.on("end", function() {
if (playing) playSound()
phase()
})
}
function phase() {
offset += Math.floor(+d3.select("#offset-select").property("value") * beatData.length);
for (i=0; i<beatData.length; i++) {
beatData.getChannelData(1)[i] = beatData.getChannelData(0)[(i+offset) % beatData.length]
}
waveShape2.datum(drawWithAvgs(1)).attr("d", wave)
}
function drawWithAvgs(channel) {
var Avgs = []
for (i=0; i<beatData.length - sampRateAdj; i += sampRateAdj) {
Avgs.push(d3.mean(beatData.getChannelData(channel).slice(i, i+sampRateAdj)))
}
return Avgs
}
function loadFromSelect() {
importAudio(d3.select("#file-select").property("value"))
}
function stopSound() {
d3.select("#start-stop").text("start")
position.interrupt().style("visibility", "hidden")
playing = false;
if (source) {source.stop();}
}
function reset() {
offset = 0;
beatData.copyFromChannel(beatData.getChannelData(1), 0)
waveShape2.datum(drawWithAvgs(0)).attr("d", wave)
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment