/*
* @title bookmarklet
* @description my bookmarklet
* @include http://*
* @license MIT License
*/
(function() {
var SoundPlayer = function() { this.initialize.apply(this, arguments); };
SoundPlayer.packToUInt32LE = function(l) {
var f = String.fromCharCode;
return f(l & 255) + f((l >> 8) & 255) + f((l >> 16) & 255) + f((l >> 24) & 255);
};
SoundPlayer.prototype = {
resolution: 0,
initialize: function(resolution) {
this.resolution = resolution;
},
generateWavURI: function(sounds) {
var len = 0;
for (var i = sounds.length; --i >= 0;)
len = Math.max(len, sounds[i].length);
var da;
var res = SoundPlayer.packToUInt32LE(this.resolution);
da = [
"RIFF" + SoundPlayer.packToUInt32LE(len + 36),
"WAVEfmt\x20\x10\x00\x00\x00\x01\x00\x01\x00",
res, res, "\x01\x00\x08\x00data",
SoundPlayer.packToUInt32LE(len)
];
for (var i = 0; i < len; ++i) {
var v = 0;
for (var j = sounds.length, d; --j >= 0;)
v += sounds[j][i];
v = (v < -1 ? -1: (v >= .9 ? .9: v)) * 128 + 128;
da.push(String.fromCharCode(v));
}
return "data:audio/wav," + encodeURIComponent(da.join(""));
},
play: function() {
var d = this.generateWavURI(arguments);
var n = document.createElement("audio");
n.style.position = "absolute";
n.style.left = "-1000px";
document.body.appendChild(n);
n.setAttribute("src", d);
n.play();
}
};
var SoundGenerator = function() { this.initialize.apply(this, arguments); };
SoundGenerator.prototype = {
data: null,
resolution: null,
envelopeGenerator: function(t) { return 1; },
filter: null,
initialize: function(resolution) {
this.data = [];
this.resolution = resolution;
},
generateSquareWave: function(freq, duration, vol) {
var t = 0, d = this.data, res = this.resolution,
eg = this.envelopeGenerator, f = this.filter, o = d.length,
p = freq / res;
var sin = Math.sin, pi = Math.PI;
d[o + duration - 1] = 0;
if (f != null) {
f.reset();
for (var pv = 0; t < duration; ++t, pv = v) {
var m = 2 * pi * t * p, v = pv;
for (var i = (0.5 / p) | 1; (i -= 2) > 0;)
v += sin(m * i) / i;
v /= 2;
d[o + t] = f.apply(4 * v * eg(t) * vol / pi);
}
} else {
for (var pv = 0; t < duration; ++t, pv = v) {
var m = 2 * pi * t * p, v = pv;
for (var i = (0.5 / p) | 1; (i -= 2) > 0;)
v += sin(m * i) / i;
v /= 2;
d[o + t] = (4 * v * eg(t) * vol / pi);
}
}
},
generateSawWave: function(freq, duration, vol) {
var t = 0, d = this.data, res = this.resolution,
eg = this.envelopeGenerator, f = this.filter,
o = d.length, p = freq / res
d[o + duration - 1] = 0;
var sin = Math.sin, pi = Math.PI;
if (f != null) {
f.reset();
for (var pv = 0; t < duration; ++t, pv = v) {
var m = pi * t * p, v = pv;
for (var i = (0.5 / p) | 0; --i > 0; )
v += sin(2 * i * m) / i;
v /= 2;
d[o + t] = f.apply(2 * v * eg(t) * vol / pi);
}
} else {
for (var pv = 0; t < duration; ++t, pv = v) {
var m = pi * t * p, v = pv;
for (var i = (0.5 / p) | 0; --i > 0; )
v += sin(2 * i * m) / i;
v /= 2;
d[o + t] = (2 * v * eg(t) * vol / pi);
}
}
},
generateSilence: function(duration) {
var d = this.data, j;
d[d.length + duration - 1] = 0;
}
};
var MusicBuilder = function() { this.initialize.apply(this, arguments); };
MusicBuilder.prototype = {
initialize: function(sg) {
this.sg = sg;
this.tempo_ = 120;
this.volume_ = .8;
this.tone_ = 'square';
this.initializeTone();
},
initializeTone: function() {
var self = this;
switch (this.tone_) {
case 'saw':
this.sound = function(freq, duration) {
this.sg.generateSawWave(freq, duration, self.volume_);
};
break;
case 'square':
this.sound = function(freq, duration) {
this.sg.generateSquareWave(freq, duration, self.volume_);
};
break;
}
},
calculateDuration: function(len) {
return (this.sg.resolution * 240 / this.tempo_) / len;
},
tone: function(v) {
this.tone_ = v;
this.initializeTone();
},
tempo: function(v) {
this.tempo_ = v;
},
note: function(n, len) {
var f = 55 << (n / 12);
f *= Math.pow(2, (n % 12) / 12);
this.sound(f, this.calculateDuration(len));
},
rest: function(len) {
this.sg.generateSilence(this.calculateDuration(len));
},
envelope: function(eg) {
this.sg.envelopeGenerator = eg;
},
parseMML: function(mml) {
var regex = /([><])|([LMOSTVS@])\s*(\d*)|([ABCDEFG]([#+-]?)|R)\s*(?:(\d+)\s*|(\.+)\s*){0,2}|\s+/gi;
var m;
var pitch_base = 36, default_len = 4;
var syntax_error = { message: "Syntax error" };
var invalid_parameter = { message: "Invalid parameter" };
var pitch_table = {
"a": 0, "A": 0,
"b": 2, "B": 2,
"c": -9, "C": -9,
"d": -7, "D": -7,
"e": -5, "E": -5,
"f": -4, "F": -4,
"g": -2, "G": -2
};
var envelope_cycle = 1000;
var envelopes = [
function(t) { return 1; },
function(t) { return Math.max(0, 1 - t / envelope_cycle); },
function(t) { return Math.min(1, t / envelope_cycle); },
function(t) { return 1 - (t % envelope_cycle) / envelope_cycle; },
function(t) { return (t % envelope_cycle) / envelope_cycle; },
function(t) { return Math.abs((t % (envelope_cycle * 2)) - envelope_cycle) / envelope_cycle; },
function(t) { return 1 - Math.abs((t % (envelope_cycle * 2)) - envelope_cycle) / envelope_cycle; },
function(t) { return t < envelope_cycle ? 1 - t / envelope_cycle: 1; },
function(t) { return t < envelope_cycle ? t / envelope_cycle: 0; }
];
var envelope_table = [
envelopes[0],
envelopes[1],
envelopes[1],
envelopes[1],
envelopes[8],
envelopes[8],
envelopes[8],
envelopes[8],
envelopes[3],
envelopes[1],
envelopes[5],
envelopes[7],
envelopes[4],
envelopes[2],
envelopes[6],
envelopes[8]
];
tone_table = [ 'square', 'saw' ];
while (m = regex.exec(mml)) {
switch (m[1]) {
case "<":
pitch_base += 12;
continue;
case ">":
pitch_base -= 12;
continue;
}
if (m[2]) {
var param = m[3] == '' ? -1: parseInt(m[3]);
switch (m[2]) {
case "l": case "L":
if (param < 0)
throw syntax_error;
default_len = param;
break;
case "o": case "O":
if (param < 0)
throw syntax_error;
pitch_base = (param - 1) * 12;
break;
case "t": case "T":
if (param < 0)
throw syntax_error;
this.tempo(param);
break;
case "v": case "V":
if (param < 0 || param > 255)
throw syntax_error;
this.volume(param / 256);
break;
case 's': case 'S':
if (param < 0)
throw syntax_error;
if (param >= envelope_table.length)
throw invalid_parameter;
this.envelope(envelope_table[param]);
break;
case 'm': case 'M':
if (param < 0)
throw syntax_error;
if (param < 1)
throw invalid_parameter;
envelope_cycle = this.sg.resolution * param / 8000;
break;
case '@':
if (param < 0)
throw syntax_error;
if (param >= tone_table.length)
throw invalid_parameter;
this.tone(tone_table[param]);
}
continue;
}
var l = typeof(m[6]) == 'undefined' ? default_len: parseInt(m[6]),
a = typeof(m[7]) == 'undefined' ? 0: m[7].length,
d = l * (1 << a) / ((1 << (a + 1)) - 1);
if (m[4]) {
this.note(pitch_base + pitch_table[m[4].charAt(0)]
+ (m[5] ? ("#+".indexOf(m[5]) < 0 ? -1: 1): 0), d);
} else {
this.rest(d);
}
}
}
};
var getAllTextsInElement = function(tag) {
var retval = [];
var traverse = function(tag) {
if (tag.localName == "input") {
retval.push(tag.value);
return;
} else if (tag.localName == "textarea") {
retval.push(tag.value);
return;
}
var children = tag.childNodes;
for (var i = 0; i < children.length; i++) {
var child = children.item(i);
switch (child.nodeType) {
case 1:
traverse(child);
break;
case 3:
if (child.nodeValue)
retval.push(child.nodeValue);
break;
}
}
};
traverse(tag);
return retval;
};
var text = getAllTextsInElement(document);
var m = null;
for (var i = 0; i < text.length; i++) {
m = text[i].match(/\{\{\{mml:([^}]+)\}\}\}/i);
if (m)
break;
}
if (m) {
var mmls = m[1].split(/\s*(?:\r|\n|\r\n|;)\s*/i);
console.log(mmls);
var s = [];
for (var i = 0; i < mmls.length; ++i) {
if (mmls[i] == '')
continue;
var m = new MusicBuilder(new SoundGenerator(16000));
m.parseMML(mmls[i]);
s.push(m.sg.data);
}
var sp = new SoundPlayer(16000);
sp.play.apply(sp, s);
}
})();