Created
January 28, 2016 08:09
-
-
Save morrislaptop/dbc8654d92685512f51d to your computer and use it in GitHub Desktop.
Stereo Panner based on Alpha Rotation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title>StereoPannerShim</title> | |
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Source+Sans+Pro"> | |
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/prettify/r298/prettify.min.css"> | |
<style> | |
body { font-family: "Source Sans Pro", sans-serif } | |
canvas { width: 100%; height: 240px; background: black } | |
hr { border: none } | |
#app { margin: 10px 0 } | |
#app .btn { width: 100px } | |
.prettyprint { padding: 0; margin: 0; background: white; border: none !important } | |
</style> | |
<script src="//cdn.jsdelivr.net/es6-promise/1.0.0/promise.min.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/0.11.4/vue.min.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/prettify/r298/prettify.min.js"></script> | |
<script src="//mohayonao.github.io/stereo-analyser-node/build/stereo-analyser-node.js"></script> | |
<script src="//mohayonao.github.io/promise-decode-audio-data/build/promise-decode-audio-data.js"></script> | |
<script src="//mohayonao.github.io/get-float-time-domain-data/build/get-float-time-domain-data.js"></script> | |
<script src="bower_components/stereo-panner-shim/build/stereo-panner-shim.js"></script> | |
<script src="bower_components/fulltilt/dist/fulltilt.min.js"></script> | |
</head> | |
<body> | |
<div class="container"> | |
<div id="app"> | |
<div class="page-header"> | |
<h1>StereoPannerShim</h1> | |
</div> | |
<div class="alert" role="alert" v-class="alertClass" v-text="alert"></div> | |
<div class="row"> | |
<div class="col-md-4"> | |
<button class="btn btn-default" v-on="click: toggle">{{ !audioBuffer ? "Loading.." : isPlaying ? "Stop" : "Start" }}</button> | |
<h5>Parameters</h5> | |
<div class="form-group"> | |
<label>Rate: </label> <span>{{rate.toFixed(3)}}Hz</span> | |
<input type="range" name="rate" value="27" v-on="input: input"> | |
</div> | |
<div class="form-group"> | |
<label>Amount: </label> <span>{{amount.toFixed(3)}}</span> | |
<input type="range" name="amount" value="100" v-on="input: input"> | |
</div> | |
<div class="form-group"> | |
<label>Position: </label> <span>{{pos.toFixed(3)}}</span> | |
<input id="pos" type="range" name="pos" value="50" v-on="input: input"> | |
</div> | |
</div> | |
<div class="col-md-4"> | |
<canvas id="canvasL"></canvas> | |
</div> | |
<div class="col-md-4"> | |
<canvas id="canvasR"></canvas> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
var AudioContext = window.AudioContext || window.webkitAudioContext; | |
var $ = document.getElementById.bind(document); | |
var visualiser; | |
function fetch(url) { | |
return new Promise(function(resolve, reject) { | |
var xhr = new XMLHttpRequest(); | |
xhr.open("GET", url); | |
if (url.indexOf(".mp3") !== -1) { | |
xhr.responseType = "arraybuffer"; | |
} | |
xhr.onload = function() { | |
resolve({ | |
text: function() { | |
return xhr.response; | |
}, | |
arrayBuffer: function() { | |
return xhr.response; | |
} | |
}); | |
}; | |
xhr.onerror = reject; | |
xhr.send(); | |
}); | |
} | |
var isNativeStereoPanner = (function() { | |
return AudioContext.prototype.createStereoPanner.toString().indexOf("native") !== -1; | |
})(); | |
</script> | |
<script id="example"> | |
var audioContext = new AudioContext(); | |
var app = {}; | |
// audio from YouTube Audio Library | |
fetch("Sunshine_in_My_Heart.mp3").then(function(res) { | |
return audioContext.decodeAudioData(res.arrayBuffer()); | |
}).then(function(audioBuffer) { | |
app.audioBuffer = audioBuffer; | |
}); | |
var bufferSource, autoPanRate, autoPanAmount, panner; | |
function start() { | |
bufferSource = audioContext.createBufferSource(); // +------------------+ +-----------------+ | |
autoPanRate = audioContext.createOscillator(); // | BufferSourceNode | | OscillatorNode | | |
autoPanAmount = audioContext.createGain(); // +------------------+ | frequency: Rate | | |
panner = audioContext.createStereoPanner(); // | +-----------------+ | |
// | | | |
bufferSource.buffer = app.audioBuffer; // | +--------------+ | |
bufferSource.start(audioContext.currentTime); // | | GainNode | | |
bufferSource.onended = function() { // | | gain: Amount | | |
app.stop(); // | +--------------+ | |
}; // | | | |
autoPanRate.frequency.value = app.rate; // +------------------+ | | |
autoPanRate.start(audioContext.currentTime); // | StereoPannerNode | | | |
autoPanAmount.gain.value = app.amount; // | pan: Position <------+ | |
panner.pan.value = app.pos; // +------------------+ | |
bufferSource.connect(panner); | |
bufferSource.connect(panner); | |
autoPanRate.connect(autoPanAmount); | |
autoPanAmount.connect(panner.pan); | |
panner.connect(visualiser); | |
function linlin(value, inMin, inMax, outMin, outMax) { | |
return (value - inMin) / (inMax - inMin) * (outMax - outMin) + outMin; | |
} | |
var deviceOrientation = FULLTILT.getDeviceOrientation({'type': 'world'}); | |
deviceOrientation.then(function(orientationData) { | |
orientationData.listen(function() { | |
var screenAdjustedEvent = orientationData.getScreenAdjustedEuler(); | |
var rotation = screenAdjustedEvent.alpha; | |
var radians = rotation * Math.PI / 180; | |
var pan = Math.sin(radians); | |
app.pos = pan; // vue | |
document.getElementById('pos').value = linlin(pan, -1, +1, 0, 100); // input | |
updateParameters(app.rate, app.amount, app.pos); // audio | |
}); | |
}); | |
} | |
function stop() { | |
bufferSource.stop(audioContext.currentTime); | |
bufferSource.disconnect(); | |
autoPanRate.disconnect(); | |
autoPanRate.stop(audioContext.currentTime); | |
autoPanAmount.disconnect(); | |
panner.disconnect(); | |
} | |
function updateParameters(rate, amount, pos) { | |
autoPanRate.frequency.value = rate; | |
autoPanAmount.gain.value = amount; | |
panner.pan.value = pos; | |
} | |
</script> | |
<script> | |
window.addEventListener("load", function() { | |
"use strict"; | |
var fftSize = 2048; | |
var arrayL = new Float32Array(fftSize); | |
var arrayR = new Float32Array(fftSize); | |
function initVisualiser() { | |
visualiser = new StereoAnalyserNode(audioContext); | |
visualiser.fftSize = fftSize; | |
visualiser.connect(audioContext.destination); | |
} | |
function animate() { | |
visualiser.getFloatTimeDomainData(arrayL, arrayR); | |
app.drawTimeDomainData(arrayL, arrayR); | |
if (app.isPlaying) { | |
setTimeout(function() { | |
requestAnimationFrame(animate); | |
}, 50); | |
} | |
} | |
function linlin(value, inMin, inMax, outMin, outMax) { | |
return (value - inMin) / (inMax - inMin) * (outMax - outMin) + outMin; | |
} | |
function linexp(value, inMin, inMax, outMin, outMax) { | |
return Math.pow(outMax / outMin, (value - inMin) / (inMax - inMin)) * outMin; | |
} | |
function initCanvas(name) { | |
var canvas = $(name); | |
canvas.width = 512; | |
canvas.height = 200; | |
canvas.context = canvas.getContext("2d"); | |
canvas.context.fillStyle = "rgba(0, 0, 0, 0.5)"; | |
canvas.context.strokeStyle = "#2ecc71"; | |
return canvas; | |
} | |
var canvasL = initCanvas("canvasL"); | |
var canvasR = initCanvas("canvasR"); | |
function clear(canvas) { | |
canvas.context.fillRect(0, 0, canvas.width, canvas.height); | |
} | |
function drawTimeDomainData(canvas, array) { | |
var context = canvas.context; | |
var height = canvas.height; | |
var dx = canvas.width / array.length; | |
context.beginPath(); | |
for (var i = 0; i < array.length; i++) { | |
var x = Math.round(i * dx); | |
var y = Math.round((array[i] * height + height) * 0.5); | |
context.lineTo(x, y); | |
} | |
context.stroke(); | |
} | |
app = new Vue({ | |
el: "#app", | |
data: { | |
audioBuffer: app.audioBuffer, | |
isPlaying: false, | |
rate: 0.05, | |
amount: 1, | |
pos: 0, | |
alert: '', | |
alertClass: '' | |
}, | |
methods: { | |
toggle: function() { | |
if (this.audioBuffer) { | |
if (this.isPlaying) { | |
this.stop(); | |
} else { | |
this.start(); | |
} | |
} | |
}, | |
start: function() { | |
if (!this.isPlaying) { | |
this.isPlaying = true; | |
if (!visualiser) { | |
initVisualiser(); | |
} | |
start(); | |
requestAnimationFrame(animate); | |
} | |
}, | |
stop: function() { | |
if (this.isPlaying) { | |
this.isPlaying = false; | |
stop(); | |
} | |
}, | |
input: function(e) { | |
switch (e.target.name) { | |
case "rate": | |
this.rate = linexp(e.target.value, 0, 100, 0.1, 40); | |
break; | |
case "amount": | |
this.amount = linlin(e.target.value, 0, 100, 0, 1); | |
break; | |
case "pos": | |
this.pos = linlin(e.target.value, 0, 100, -1, +1); | |
break; | |
} | |
if (panner) { | |
updateParameters(this.rate, this.amount, this.pos); | |
} | |
}, | |
drawTimeDomainData: function(arrayL, arrayR) { | |
clear(canvasL); | |
drawTimeDomainData(canvasL, arrayL); | |
clear(canvasR); | |
drawTimeDomainData(canvasR, arrayR); | |
} | |
} | |
}); | |
if (isNativeStereoPanner) { | |
app.alert = "StereoPannerShim is not enabled for exists the native StereoPanner."; | |
app.alertClass = "alert-info"; | |
} else { | |
app.alert = "StereoPannerShim is enabled."; | |
app.alertClass = "alert-success"; | |
} | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment