Skip to content

Instantly share code, notes, and snippets.

@morrislaptop
Created January 28, 2016 08:09
Show Gist options
  • Save morrislaptop/dbc8654d92685512f51d to your computer and use it in GitHub Desktop.
Save morrislaptop/dbc8654d92685512f51d to your computer and use it in GitHub Desktop.
Stereo Panner based on Alpha Rotation
<!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