Skip to content

Instantly share code, notes, and snippets.

@alexmacy
Last active May 16, 2017 20:27
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/d31b0819f11fc881add341ace6483bd3 to your computer and use it in GitHub Desktop.
Save alexmacy/d31b0819f11fc881add341ace6483bd3 to your computer and use it in GitHub Desktop.
Web Audio Sampler with Effects
license: mit

Another update to this sampler, with these new features:

  • Ability to brush and zoom on the main wave.

  • An effect for fading in or out, shaped using d3's native easing types.

  • A delay effect with adjustable delay intervals calculated when loading the sample

<!DOCTYPE html>
<html>
<head>
<style>
button {margin-right: 10px}
input {margin-left: 10px}
path {fill: none; stroke: black; stroke-width: .5;}
line {stroke: red; stroke-width: 2; visibility: hidden;}
.position {fill: red; fill-opacity: .5; visibility: hidden;}
.zoom {fill: none; pointer-events: all;}
</style>
<script src="//d3js.org/d3.v4.min.js"></script>
</head>
<body style="margin: 10px">
<div>
<select id="file-select"></select>
<button id="loadRepo" onclick="loadFromSelect()">Load From Select</button>
or
<input type="file" id="load-file" onchange="loadLocalFile()" style="margin-right: 100px;"/>
<button onclick="clearCues()">Clear Cues</button>
<button id="start-stop" onclick="playing ? stopSound() : playSound()">Start</button>
<input type="checkbox" onclick="looping = !looping" checked/> Loop
</div>
<div style="margin-top: 10px;">
Fade:
<select id="fade-direction">
<option value="in">In</option>
<option value="out">Out</option>
</select>
<select id="ease-select">
<option value="easeLinear">easeLinear</option>
<option value="easePolyIn">easePolyIn</option>
<option value="easePolyOut">easePolyOut</option>
<option value="easePolyInOut">easePolyInOut</option>
<option value="easeQuadIn">easeQuadIn</option>
<option value="easeQuadOut">easeQuadOut</option>
<option value="easeQuadInOut">easeQuadInOut</option>
<option value="easeCubicIn">easeCubicIn</option>
<option value="easeCubicOut">easeCubicOut</option>
<option value="easeCubicInOut">easeCubicInOut</option>
<option value="easeSinIn">easeSinIn</option>
<option value="easeSinOut">easeSinOut</option>
<option value="easeSinInOut">easeSinInOut</option>
<option value="easeExpIn">easeExpIn</option>
<option value="easeExpOut">easeExpOut</option>
<option value="easeExpInOut">easeExpInOut</option>
<option value="easeCircleIn">easeCircleIn</option>
<option value="easeCircleOut">easeCircleOut</option>
<option value="easeCircleInOut">easeCircleInOut</option>
<option value="easeElasticIn">easeElasticIn</option>
<option value="easeElasticOut">easeElasticOut</option>
<option value="easeElasticInOut">easeElasticInOut</option>
<option value="easeBackIn">easeBackIn</option>
<option value="easeBackOut">easeBackOut</option>
<option value="easeBackInOut">easeBackInOut</option>
<option value="easeBounceIn">easeBounceIn</option>
<option value="easeBounceOut">easeBounceOut</option>
<option value="easeBounceInOut">easeBounceInOut</option>
</select>
<button id="apply-ease" onclick="fade()" style="margin-right: 50px;">Apply Fade</button>
Delay:
<select id="delay-Value"></select>
<button id="apply-delay" onclick="delay()" style="margin-right: 50px;">Apply Delay</button>
<button id="reset" onclick="reset()">Reset</button>
</div>
<svg></svg>
</body>
<script>
var width = Math.max(940, innerWidth-20),
height = 450,
waveHeight = 200,
sampRateAdj,
origBuffer;
var audioCtx = new (window.AudioContext || window.webkitAudioContext)(),
beatData, source, playing, looping = true;
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height)
var timeScale = d3.scaleLinear().range([0, width]),
x1 = d3.scaleLinear().range([0, width]),
x2 = d3.scaleLinear().range([0, width]),
y1 = d3.scaleLinear().range([waveHeight, 0]),
y2 = d3.scaleLinear().range([waveHeight/4, 0]);
var brush = d3.brushX()
.extent([[0, 0], [width, waveHeight/4]])
.on("brush end", brushed);
var zoom = d3.zoom()
.scaleExtent([1, Infinity])
.translateExtent([[0, 0], [width, waveHeight]])
.extent([[0, 0], [width, waveHeight]])
.on("zoom", zoomed);
var wave1 = d3.line()
.curve(d3.curveMonotoneX)
.x(function(d, i) {return x1(i)})
.y(function(d) {return y1(d)})
var wave2 = d3.line()
.curve(d3.curveMonotoneX)
.x(function(d, i) {return x2(i)})
.y(function(d) {return y2(d)})
var focus = svg.append("g")
.attr("id", "focus")
var context = svg.append("g")
.attr("id", "context")
.attr("transform", "translate(0," + (waveHeight + 30) + ")");
var waveShape1 = focus.append("path")
.datum([])
.attr("d", wave1)
.attr("transform", "translate(0,30)");
var waveShape2 = context.append("path")
.attr("transform", "translate(0,30)");
var cues = focus.append("g"),
cues2 = context.append("g");
var position = context.append("rect")
.attr("class", "position")
.attr("y", 30)
.attr("width", 10)
.attr("height", waveHeight/4)
var brushRect = context.append("g")
.attr("class", "brush")
.attr("transform", "translate(0,30)")
.call(brush)
.call(brush.move, x1.range());;
var zoomRect = svg.append("rect")
.attr("class", "zoom")
.attr("width", width)
.attr("height", waveHeight)
.attr("transform", "translate(0,30)")
.call(zoom)
.on("mousedown.zoom touchstart.zoom touchmove.zoom touchend.zoom", null)
.on("click", function() {
var t = x1.invert(d3.event.offsetX)
d3.event.shiftKey ? addCue(t) : playSound(timeScale.invert(x2(t)))
})
d3.select(document).on("keydown", keyEvent)
d3.json("https://api.github.com/repos/alexmacy/loops/contents/", function(error, loopList) {
d3.select("#file-select").selectAll("option")
.data(loopList)
.enter().append("option")
.attr("value", function(d) {return d.download_url})
.property("selected", function(d) {return (d.name == "back_on_the_streets_again.wav")})
.text(function(d) {return d.name})
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;
origBuffer = [...beatData.getChannelData(0)]
var delayVals = [
{key: "1/2 sample length", value: -Math.floor(beatData.length/2)},
{key: "1/3 sample length", value: -Math.floor(beatData.length/3)},
{key: "1/4 sample length", value: -Math.floor(beatData.length/4)},
{key: "1/6 sample length", value: -Math.floor(beatData.length/6)},
{key: "1/8 sample length", value: -Math.floor(beatData.length/8)},
{key: "1/12 sample length", value: -Math.floor(beatData.length/12)},
{key: "1/16 sample length", value: -Math.floor(beatData.length/16)},
{key: "1/24 sample length", value: -Math.floor(beatData.length/24)},
{key: "1/32 sample length", value: -Math.floor(beatData.length/32)},
{key: "-1/2 sample length", value: Math.floor(beatData.length/2)},
{key: "-1/3 sample length", value: Math.floor(beatData.length/3)},
{key: "-1/4 sample length", value: Math.floor(beatData.length/4)},
{key: "-1/6 sample length", value: Math.floor(beatData.length/6)},
{key: "-1/8 sample length", value: Math.floor(beatData.length/8)},
{key: "-1/12 sample length", value: Math.floor(beatData.length/12)},
{key: "-1/16 sample length", value: Math.floor(beatData.length/16)},
{key: "-1/24 sample length", value: Math.floor(beatData.length/24)},
{key: "-1/32 sample length", value: Math.floor(beatData.length/32)},
]
d3.select("#delay-Value").html("").selectAll("option")
.data(delayVals)
.enter().append("option")
.attr("value", function(d) {return d.value})
.text(function(d) {return d.key})
sampRateAdj = Math.floor(beatData.length/(width*10));
var waveData = drawWithAvgs(0)
timeScale.domain([0, beatData.duration]);
x1.domain([0, waveData.length]);
y1.domain(d3.extent(waveData));
x2.domain(x1.domain());
y2.domain(y1.domain());
waveShape1.datum(waveData).attr("d", wave1)
waveShape2.datum(waveData).attr("d", wave2)
cues.html("")
cues2.html("")
for (i=0; i<8; i++) {
addCue(i * (waveData.length/8))
}
}
}
function playSound(t=0) {
if (source) {source.stop();}
d3.select("#start-stop").text("stop")
playing = true;
source = audioCtx.createBufferSource();
source.buffer = beatData;
source.connect(audioCtx.destination);
source.start(0, t);
position.style("visibility", "visible")
.transition().duration(0)
.attr("x", timeScale(t))
.transition().duration((beatData.duration - t) * 1000).ease(d3.easeLinear)
.attr("x", width)
.on("end", function() {
playing && looping ? playSound() : stopSound();
})
}
function addCue(t) {
var cueNum = cues.selectAll("text").nodes().length
if (cueNum > 8) return alert("Reached maximum number of cues")
cues.append("path")
.attr("class", "cue")
.datum(t)
.attr("d", function(d) {return "M" + x1(d) + ",30V" + (waveHeight+30)})
cues.append("text")
.datum(t)
.attr("x", function(d) {return x1(d)})
.attr("y", 20)
.text(cueNum + 1)
cues2.append("path")
.attr("class", "cue")
.datum(t)
.attr("d", function(d) {return "M" + x2(d) + ",30V" + (waveHeight/4+30)})
cues2.append("text")
.datum(t)
.attr("id", "text" + (cueNum + 1))
.attr("x", function(d) {return x2(d)})
.attr("y", 20)
.text(cueNum + 1)
}
function keyEvent() {
var thisKey = d3.event.keyCode-48;
if (thisKey == -16) playing ? stopSound() : playSound(0);
if (!d3.select("#text" + thisKey).empty()) {
playSound(timeScale.invert(+d3.select("#text" + thisKey).attr("x")))
}
}
function brushed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return;
var s = d3.event.selection || x2.range();
x1.domain(s.map(x2.invert, x2));
brushZoomUpdate()
svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
.scale(width / (s[1] - s[0]))
.translate(-s[0], 0));
}
function zoomed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return;
var t = d3.event.transform;
x1.domain(t.rescaleX(x2).domain());
brushZoomUpdate()
context.select(".brush").call(brush.move, x1.range().map(t.invertX, t));
}
function brushZoomUpdate() {
waveShape1.attr("d", wave1);
cues.selectAll("path")
.attr("d", function(d) {return "M" + x1(d) + ",30V" + (waveHeight + 30)})
cues.selectAll("text")
.attr("x", function(d) {return x1(d)})
}
function fade(c=0) {
var direction = d3.select("#fade-direction").property("value");
var easeMethod = d3.select("#ease-select").property("value")
if (direction == "in") {
for (i in beatData.getChannelData(c)) {
beatData.getChannelData(c)[i] *= d3[easeMethod](i/beatData.length)
}
} else {
for (i in beatData.getChannelData(c)) {
beatData.getChannelData(c)[beatData.length - i] *= d3[easeMethod](i/beatData.length)
}
}
waveShape1.datum(drawWithAvgs(0)).transition().duration(500).attr("d", wave1)
if (beatData.numberOfChannels > 1 && c == 0) fade(1);
}
function delay(c=0) {
var delayValue = +d3.select("#delay-Value").property("value")
var tempBuffer = [...beatData.getChannelData(c)]
for (i=0; i<beatData.length; i++) {
if (tempBuffer[i+delayValue]) {
beatData.getChannelData(c)[i] = (beatData.getChannelData(c)[i] + tempBuffer[i+delayValue])/2
}
}
y1.domain(d3.extent(beatData.getChannelData(0)));
waveShape1.datum(drawWithAvgs(0)).transition().duration(500).attr("d", wave1)
if (beatData.numberOfChannels > 1 && c == 0) delay(1);
}
function loadFromSelect() {
importAudio(d3.select("#file-select").property("selectedOptions")[0].value)
}
function loadLocalFile() {
var reader = new FileReader();
reader.onload = function(e) {importAudio(e.target.result);};
reader.readAsDataURL(d3.select("#load-file").property("files")[0]);
}
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 stopSound() {
d3.select("#start-stop").text("start")
position.interrupt().style("visibility", "hidden")
playing = false;
if (source) {source.stop();}
}
function reset() {
for (i in beatData.getChannelData(0)) {
beatData.getChannelData(0)[i] = origBuffer[i]
}
y1.domain(d3.extent(beatData.getChannelData(0)));
waveShape1.datum(drawWithAvgs(0)).transition().duration(500).attr("d", wave1)
}
function clearCues() {
cues.html("")
cues2.html("")
addCue(0);
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment