Last active
July 17, 2017 14:52
-
-
Save iamahuman/7e6dd8aaa7540f999f8ac411b473d5b9 to your computer and use it in GitHub Desktop.
Some practice around using HTML5 Javascript APIs, especially AudioContext. Not meant to be clean, readable or well-written.
This file contains 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> | |
<title>Dial</title> | |
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |
<style> | |
<!-- | |
button.dial { | |
padding: 10px 0; | |
width: 3.5em; | |
} | |
fieldset { | |
display: inline-block; | |
} | |
--> | |
</style> | |
<script type="text/javascript"> | |
<!-- | |
function Namespace(window, document, undefined) { | |
var AudioContext; | |
if (typeof window.AudioContext === "function") { | |
AudioContext = window.AudioContext; | |
} else if (typeof window.webkitAudioContext === "function") { | |
AudioContext = window.webkitAudioContext; | |
} else { | |
document.getElementById("notice").innerHTML = "<b>Note: <code>AudioContext</code> is not supported</b>"; | |
return false; | |
} | |
var ctx = new AudioContext(); | |
var ns = this; | |
this.context = ctx; | |
this.destination = ctx.createGain(); | |
this.destination.connect(ctx.destination); | |
this.gain_factor = 0.01; | |
this.count = 0; | |
this.tracks = {}; | |
this.creators = { | |
'us_dial': function() { return ns.play([350, 440]); }, | |
'us_dial_pbx': function() { return ns.play_altonoff([350, 440], 0.5, 0.5); }, | |
'us_offhook': function() { return ns.play_altonoff([1400, 2060, 2450, 2600], 0.1, 0.1); }, | |
'us_ring': function() { return ns.play_altonoff([440, 480], 2, 4); }, | |
'us_reorder': function() { return ns.play_altonoff([480, 620], 0.25, 0.25); }, | |
'us_busy': function() { return ns.play_altonoff([480, 620], 0.5, 0.5); }, | |
}; | |
var $e = document.getElementById.bind(document); | |
var ctrl_gain = $e("ctrl_gain"); | |
this.gain = +ctrl_gain.value; | |
this.update_gain(); | |
ctrl_gain.addEventListener("change", function (ev) { | |
ns.gain = +ctrl_gain.value; | |
ns.update_gain(); | |
}); | |
function tone_play(key) { | |
if (!ns.tracks[key]) { | |
var creator = ns.creators[key]; | |
if (!creator && key.substring(0, 5) == 'freq#') { | |
creator = function() { return ns.play([+key.substring(5)]); }; | |
} | |
(ns.tracks[key] = creator()).start(ns.context.currentTime); | |
} | |
} | |
function tone_stop(key) { | |
if (ns.tracks[key]) { | |
ns.tracks[key].stop(); | |
delete ns.tracks[key]; | |
} | |
} | |
function freq_play(freq) { tone_play('freq#' + freq); } | |
function freq_stop(freq) { tone_stop('freq#' + freq); } | |
var oh_to = null; | |
function tone_checkbox(key, elem, initial) { | |
if (typeof initial === 'undefined' || typeof initial === 'null') | |
initial = elem.checked; | |
else | |
elem.checked = initial = !!initial; | |
function handler (ev) { | |
elem.checked ? tone_play(key) : tone_stop(key); | |
for (var qkey in ns.creators) { | |
if (qkey == key || !ns.creators.hasOwnProperty(qkey)) continue; | |
var oelem = $e(qkey); | |
if (oelem) { | |
oelem.checked = false; | |
tone_stop(qkey); | |
//if (qkey == 'us_ring' && rring_to != null) | |
// clearTimeout(rring_to); | |
} | |
} | |
if (!(key == 'us_ring' && elem.checked) && rring_to != null) { | |
clearTimeout(rring_to); | |
} | |
if (key == 'us_dial' || key == 'us_dial_pbx') { | |
if (oh_to != null) | |
clearTimeout(oh_to); | |
if (elem.checked) { | |
oh_to = setTimeout(function () { | |
elem.checked = false; | |
tone_stop(key); | |
$e('us_offhook').checked = true; | |
$e('us_offhook').dispatchEvent(new Event('change')); | |
}, 30000); | |
} | |
} | |
} | |
elem.addEventListener("change", handler); | |
if (initial) handler(null); | |
}; | |
function tone_button(key, elem) { | |
elem.addEventListener("mousedown", function (ev) { | |
tone_play(key); | |
}); | |
elem.addEventListener("mouseup", function (ev) { | |
tone_stop(key); | |
}); | |
} | |
tone_checkbox('us_dial', $e("us_dial"), false); | |
tone_checkbox('us_dial_pbx', $e("us_dial_pbx"), true); | |
tone_checkbox('us_offhook', $e("us_offhook"), false); | |
tone_checkbox('us_ring', $e("us_ring"), false); | |
tone_checkbox('us_reorder', $e("us_reorder"), false); | |
tone_checkbox('us_busy', $e("us_busy"), false); | |
var ring_to = null; | |
var dial_to = null; | |
var rring_to = null; | |
function ring_timeout() { | |
$e('us_dial').checked = false; | |
$e('us_dial_pbx').checked = false; | |
tone_stop('us_dial'); | |
tone_stop('us_dial_pbx'); | |
$e('us_reorder').checked = true; | |
tone_play('us_reorder'); | |
} | |
var dial = false; | |
var pbx = false; | |
var invalid = false; | |
var dtmf_ids = ['dial_1', 'dial_2', 'dial_3', 'dial_A', | |
'dial_4', 'dial_5', 'dial_6', 'dial_B', | |
'dial_7', 'dial_8', 'dial_9', 'dial_C', | |
'dial_star', 'dial_0', 'dial_sharp', 'dial_D']; | |
var dtmf_cols = [1209, 1336, 1477, 1633]; | |
var dtmf_rows = [697, 770, 852, 941]; | |
function dtmf_button(key_nr) { | |
var freq1 = dtmf_cols[key_nr & 3]; | |
var freq2 = dtmf_rows[key_nr >> 2]; | |
var elem = $e(dtmf_ids[key_nr]); | |
function down(ev) { | |
var a = ($e('us_dial').checked); | |
var b = ($e('us_dial_pbx').checked); | |
if (a || b) { | |
dial = a; | |
pbx = b; | |
} | |
$e('us_dial').checked = false; | |
$e('us_dial_pbx').checked = false; | |
tone_stop('us_dial'); | |
tone_stop('us_dial_pbx'); | |
if (oh_to != null) | |
clearTimeout(oh_to); | |
freq_play(freq1); | |
freq_play(freq2); | |
if (typeof elem.setCapture === 'function') elem.setCapture(true); | |
} | |
function up(ev) { | |
if (typeof document.releaseCapture === 'function') document.releaseCapture(); | |
if (pbx && !invalid && key_nr == 10) { | |
if (ring_to != null) | |
clearTimeout(ring_to); | |
if (rring_to != null) | |
clearTimeout(rring_to); | |
ring_to = setTimeout(function () { | |
$e('us_dial').checked = true; | |
$e('us_dial').dispatchEvent(new Event('change')); | |
$e('us_dial_pbx').checked = false; | |
tone_stop('us_dial_pbx'); | |
dial = true; pbx = false; invalid = false; | |
}, 1000); | |
} else if (pbx || dial) { | |
var id = (key_nr == 12 || pbx || invalid) ? 'us_reorder' : 'us_ring'; | |
if (rring_to != null) | |
clearTimeout(rring_to); | |
if (id == 'us_reorder') { | |
dial = pbx = false; | |
invalid = true; | |
} | |
if (ring_to != null) { | |
clearTimeout(ring_to); | |
ring_to = null; | |
} | |
ring_to = setTimeout(function () { | |
ring_to = null; | |
$e(id).checked = true; | |
tone_play(id); | |
dial = pbx = invalid = false; | |
if (id == 'us_ring') { | |
if (rring_to != null) | |
clearTimeout(rring_to); | |
rring_to = setTimeout(function () { | |
$e('us_ring').checked = false; | |
tone_stop('us_ring'); | |
$e('us_busy').checked = true; | |
tone_play('us_busy'); | |
}, 59000); | |
} | |
}, id == 'us_reorder' ? 500 : 2000); | |
} | |
freq_stop(freq1); | |
freq_stop(freq2); | |
} | |
elem.addEventListener("mousedown", down); | |
elem.addEventListener("mouseup", up); | |
return {down: down, up: up}; | |
//elem.addEventListener("mouseout", up); | |
} | |
var funcs = []; | |
for (var i = 0; i < 16; i++) { | |
funcs[i] = dtmf_button(i); | |
} | |
var kmap_key = {'1': 0, '2': 1, '3': 2, 'a': 3, 'A': 3, | |
'4': 4, '5': 5, '6': 6, 'b': 7, 'B': 7, | |
'7': 8, '8': 9, '9': 10, 'c': 11, 'C': 11, | |
'*': 12, '0': 13, '#': 14, 'd': 15, 'D': 15}; | |
function getKeyInfoByEvent (ev) { | |
var i = -1; | |
do { | |
if (typeof ev.key !== 'undefined') { | |
i = kmap_key[ev.key]; | |
break; | |
} | |
if (typeof ev.keyCode !== 'undefined') { | |
var kc = ev.keyCode; | |
if ((kc >= 0x30 && kc <= 0x39) || (kc >= 0x41 && kc <= 0x44)) | |
i = kmap_key[String.fromCharCode(kc)]; | |
else if (kc == 0xAA) // DOM_VK_ASTERISK | |
i = kmap_key['*']; | |
else if (kc == 0xA3) // DOM_VK_HASH | |
i = kmap_key['#']; | |
break; | |
} | |
} while (0); | |
if (i >= 0 && i < 16) | |
return funcs[i]; | |
} | |
document.addEventListener("keydown", function (ev) { | |
var ki = getKeyInfoByEvent(ev); | |
if (ki) ki.down(); | |
}, {capture: true, passive: true}); | |
document.addEventListener("keyup", function (ev) { | |
var ki = getKeyInfoByEvent(ev); | |
if (ki) ki.up(); | |
}, {capture: true, passive: true}); | |
} | |
(function(po) { | |
po.update_gain = function update_gain() { | |
var gain = (this.gain * this.gain_factor) / this.count; | |
if (!Number.isNaN(gain)) { | |
if (gain > 1) gain = 1; | |
if (gain < 0) gain = 0; | |
this.destination.gain.value = gain; | |
} | |
} | |
po.create_buf_altonoff = function create_buf_altonoff(pairs) { | |
var ctx = this.context; | |
var rate = ctx.sampleRate; | |
var i, length = 0, lens = [], vals = []; | |
for (i = 0; i < pairs.length; i++) { | |
var pair = pairs[i]; | |
var len = Math.trunc(pair[1] * rate); | |
if (isNaN(len)) | |
throw new Error('invalid length'); | |
length += len; | |
if (length > Math.MAX_SAFE_INTEGER) | |
throw new Error('too long'); | |
lens[i] = len; | |
vals[i] = pair[0]; | |
} | |
var abuf = ctx.createBuffer(1, length, rate); | |
var ci, off, chans = abuf.numberOfChannels; | |
for (ci = 0; ci < chans; ci++) { | |
var data = abuf.getChannelData(ci); | |
off = 0; | |
for (i = 0; i < vals.length; i++) { | |
var val = vals[i]; | |
var limit = off + lens[i]; | |
for (; off < limit; off++) { | |
data[off] = val; | |
} | |
} | |
} | |
return abuf; | |
}; | |
po.create_nodes = function create_nodes(dest, freqs) { | |
var nodes = []; | |
var ctx = this.context; | |
var state = 0; | |
for (var i = 0; i < freqs.length; i++) { | |
var osc = ctx.createOscillator(); | |
osc.type = 'sine'; | |
osc.frequency.value = freqs[i]; | |
osc.connect(dest); | |
nodes[i] = osc; | |
} | |
return ({ | |
nodes: nodes, | |
ns: this, | |
start: function start(ts) { | |
if (state != 0) return false; | |
state = 1; | |
this.ns.count += nodes.length; | |
this.ns.update_gain(); | |
for (var i = 0; i < nodes.length; i++) { | |
nodes[i].start(ts); | |
} | |
return true; | |
}, | |
stop: function stop() { | |
if (state != 1) return false; | |
state = 2; | |
if (!dest) return; | |
for (var i = 0; i < nodes.length; i++) { | |
var n = nodes[i]; | |
n.stop(); | |
//n.disconnect(dest); | |
} | |
this.ns.count -= nodes.length; | |
this.ns.update_gain(); | |
dest = null; | |
return true; | |
} | |
}); | |
}; | |
po.play = function play(freqs) { | |
return this.create_nodes(this.destination, freqs); | |
}; | |
po.play_altonoff = function play_altonoff(freqs, on_time, off_time) { | |
var ctx = this.context; | |
var dest = this.destination; | |
var gainNode = ctx.createGain(); | |
gainNode.gain.value = 0; | |
var gabuf = this.create_buf_altonoff([[1, on_time], [0, off_time]]); | |
var ganode = ctx.createBufferSource(); | |
ganode.buffer = gabuf; | |
ganode.loop = true; | |
ganode.connect(gainNode.gain); | |
var obj = this.create_nodes(gainNode, freqs); | |
gainNode.connect(dest); | |
ctx = null; | |
var ret = ({ | |
obj: obj, | |
node: ganode, | |
start: function start() { | |
if (this.obj.start.apply(this.obj, arguments)) | |
this.node.start.apply(this.node, arguments); | |
}, | |
stop: function stop() { | |
if (!dest || !this.obj || !this.node) return; | |
this.node.stop.apply(this.node, arguments); | |
this.obj.stop.apply(this.obj, arguments); | |
//this.node.disconnect(dest); | |
// Drop references | |
this.obj = null; | |
this.node = null; | |
dest = gainNode = gabuf = dest = ctx = null; | |
} | |
}); | |
ganode = null; | |
return ret; | |
}; | |
})(Namespace.prototype); | |
window.onload = function () { | |
window.mgr = new Namespace(window, window.document); | |
} | |
// --> | |
</script> | |
</head> | |
<body> | |
<div id="notice"></div> | |
<div> | |
<fieldset> | |
<legend>Tones</legend> | |
<input type="checkbox" id="us_dial" /> | |
<label for="us_dial">Dial</label> | |
<input type="checkbox" id="us_dial_pbx" /> | |
<label for="us_dial_pbx">Dial PBX</label> | |
<input type="checkbox" id="us_offhook" /> | |
<label for="us_offhook">Off-hook</label> | |
<input type="checkbox" id="us_ring" /> | |
<label for="us_ring">Ring</label> | |
<input type="checkbox" id="us_reorder" /> | |
<label for="us_reorder">Reorder</label> | |
<input type="checkbox" id="us_busy" /> | |
<label for="us_busy">Busy</label> | |
<input type="range" id="ctrl_gain" value="10" min="0" max="100" /> | |
<label for="ctrl_gain">Gain</label> | |
</fieldset> | |
</div> | |
<br /> | |
<div> | |
<fieldset> | |
<legend>Dialpad</legend> | |
<div id="dialpad"> | |
<div class="dial_row"> | |
<button class="dial" id="dial_1">1</button> | |
<button class="dial" id="dial_2">2</button> | |
<button class="dial" id="dial_3">3</button> | |
<button class="dial" id="dial_A">A</button> | |
</div> | |
<div class="dial_row"> | |
<button class="dial" id="dial_4">4</button> | |
<button class="dial" id="dial_5">5</button> | |
<button class="dial" id="dial_6">6</button> | |
<button class="dial" id="dial_B">B</button> | |
</div> | |
<div class="dial_row"> | |
<button class="dial" id="dial_7">7</button> | |
<button class="dial" id="dial_8">8</button> | |
<button class="dial" id="dial_9">9</button> | |
<button class="dial" id="dial_C">C</button> | |
</div> | |
<div class="dial_row"> | |
<button class="dial" id="dial_star">*</button> | |
<button class="dial" id="dial_0">0</button> | |
<button class="dial" id="dial_sharp">#</button> | |
<button class="dial" id="dial_D">D</button> | |
</div> | |
</div> | |
</fieldset> | |
</div> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment