Skip to content

Instantly share code, notes, and snippets.

@alexmacy
Last active April 20, 2023 16:10
Show Gist options
  • Save alexmacy/0ecc505b717acc18936284464c4b8185 to your computer and use it in GitHub Desktop.
Save alexmacy/0ecc505b717acc18936284464c4b8185 to your computer and use it in GitHub Desktop.
Web Audio Sampler v2
license: mit

This is a continuation of a previous sampler/sequencer built using the Web Audio API. This time with some added features to make it a more useful tool. Most significantly, additional samples can be loaded and the cues can be customized. I also started a repository for storing loops (I'd love it if other people wanted to add to it!)

Changing the sample:

  • Choose from the dropdown menu on the left. The options are the files in the the loop repository mentioned above.
  • Load a local file client-side.
  • For best results, use .mp3, .ogg, or .wav file formats.
  • File size doesn't seem to be much of an issue when loading files client-side, though I haven't loaded tracks longer than about 15 minutes....

Customizing the cues:

  • Shift+click places a custom cue along the timeline. (cues are limited to 1-9)
  • The 'Clear Cues' button removes existing cues, including any of the default cues that are placed when the sample is loaded.

Other updates:

  • The checkbox at the top right toggles looping.
  • Clicking the timeline starts the audio from the location of the click.
<!DOCTYPE html>
<html>
<head>
<style>
button {margin-right: 10px}
input {margin-left: 10px}
path {fill: none; stroke: black; stroke-width: .5;}
.hover {stroke-width: 1; stroke: blue;}
line {stroke: red; stroke-width: 2; visibility: hidden;}
</style>
<script src="//d3js.org/d3.v4.min.js"></script>
</head>
<body style="margin: 10px">
<div>
<select id="drop-down"></select>
<button id="loadRepo" onclick="loadFromSelect()">Load From Dropdown</button>
or
<input type="file" id="load-file" onchange="loadLocalFile()" style="margin-right: 100px;"/>
<button onclick="clearCues()">Clear Cues</button>
<button onclick="stopSound()">Stop</button>
<input type="checkbox" id="loop-check" checked/> Loop
</div>
<svg></svg>
</body>
<script>
var width = 940,
height = 230,
waveHeight = 200;
var timeScale = d3.scaleLinear().range([0, width]);
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height)
.style("cursor", "none")
var waveShape = svg.append("g")
.attr("id", "waveShape")
.append("path")
.attr("class", "wave")
.attr("transform", "translate(0,30)")
var cues = svg.append("g");
var hoverLine = svg.append("path")
.attr("class", "hover")
.style("visibility", "hidden")
.attr("d", "M0,30V" + height)
var position = svg.append("line")
.attr("y2", waveHeight)
svg.on("mouseover", function() {
hoverLine.style("visibility", "visible")
})
.on("mousemove", function() {
hoverLine.attr("transform", "translate(" + d3.event.x + ",0)")
})
.on("mouseout", function() {
hoverLine.style("visibility", "hidden")
})
.on("click", function() {
d3.event.shiftKey ? addCue(d3.event.x) : playSound(timeScale.invert(d3.event.x))
})
d3.select(document).on("keydown", keyEvent)
var audioCtx = new (window.AudioContext || window.webkitAudioContext)(),
beatData, source, playing;
d3.json("https://api.github.com/repos/alexmacy/loops/contents/", function(error, loopList) {
d3.select("#drop-down").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();
clearCues();
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;
var waveData = beatData.getChannelData(0),
sampRateAdj = waveData.length > 1000000 ? 500 : 20,
waveData = waveData.filter(function(d,i) {return i % sampRateAdj == 0});
timeScale.domain([0, beatData.duration]);
var line = d3.line()
.x(function(d, i) {return i/waveData.length * width})
.y(function(d) {return waveHeight/2 * d + waveHeight/2});
waveShape.datum(waveData).attr("d",line)
cues.html("")
for (i=0; i<8; i++) {
addCue(i * (width/8))
}
}
}
function addCue(t) {
var cueNum = cues.selectAll("text").nodes().length
if (cueNum > 8) return alert("Reached maximum number of cues")
cues.append("path")
.attr("d", "M" + t + ",30V" + height)
cues.append("text")
.attr("id", "text" + (cueNum + 1))
.attr("x", t)
.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 playSound(t = 0) {
if (source) {source.stop();}
source = audioCtx.createBufferSource();
source.buffer = beatData;
source.connect(audioCtx.destination);
source.start(0, t);
playing = true;
position.style("visibility", "visible")
.transition().duration(0)
.attr("transform", "translate(" + timeScale(t) + ",30)")
.transition().duration((beatData.duration - t) * 1000).ease(d3.easeLinear)
.attr("transform", "translate(" + width + ",30)")
.on("end", function() {
d3.select("#loop-check").property("checked") ? playSound() : stopSound()
})
}
function stopSound() {
if (source) {source.stop();}
playing = false;
position.interrupt().style("visibility", "hidden")
}
function clearCues() {
cues.html("")
addCue(0);
}
function loadFromSelect() {
importAudio(d3.select("#drop-down").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]);
}
</script>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment