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