Skip to content

Instantly share code, notes, and snippets.

@iamahuman
Last active July 17, 2017 14:52
Show Gist options
  • Save iamahuman/7e6dd8aaa7540f999f8ac411b473d5b9 to your computer and use it in GitHub Desktop.
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.
<!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