User:Xenwolf/common.js
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audio = new AudioContext();
// the first element should be 0
const builtin_spectra = {
sine: [0, 1],
semisine: [0],
flute: [0, 1, 0.55, 0.2, 0.2, 0.05],
violin: [0, 1, 0.5, 0.45, 0.5, 0.7, 0.6, 0.75, 0.6, 0.45, 0.35, 0.25, 0.05, 0.2, 0.15],
};
for (var i = 1; i <= 100; i++)
builtin_spectra.semisine.push(1 / (i * i));
function construct_wave(harmonic_spectrum){
var zeros = new Array(harmonic_spectrum.length).fill(0);
return audio.createPeriodicWave(harmonic_spectrum, zeros);
}
// delay `node` by `duration` ms
function delay(node, duration){
var delay = audio.createDelay(120);
delay.delayTime.value = duration / 1000;
node.connect(delay);
return delay;
}
// connect `nodes` to audio destination and schedule a disconnect after `duration` ms
function connect_all(nodes, duration){
for (var i = 0; i < nodes.length; i++)
nodes[i].connect(audio.destination);
setTimeout(function(){
for (var i = 0; i < nodes.length; i++)
nodes[i].disconnect();
}, duration);
}
// limit `node` to ~`duration` ms, also filtering out frequencies >= 20000Hz
function limit(node, duration){
var gain = audio.createGain();
gain.gain.value = 0;
gain.gain.linearRampToValueAtTime(1, audio.currentTime + 0.05);
gain.gain.linearRampToValueAtTime(1, audio.currentTime + duration / 1000);
gain.gain.exponentialRampToValueAtTime(0.00001, audio.currentTime + duration / 1000 + 0.05);
node.connect(gain);
node = gain;
var filter = audio.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 20000;
node.connect(filter);
return filter;
}
// set gain of the `node` to `value`
function set_gain(node, value){
var gain = audio.createGain();
gain.gain.value = value;
node.connect(gain);
return gain;
}
// construct an oscillator sound active for `duration` ms after `shift` ms
function play_frequency(frequency, duration, shift, gain, wave){
var node = audio.createOscillator();
node.frequency.value = frequency;
node.setPeriodicWave(wave);
node.start();
node.stop(audio.currentTime + duration / 1000 + 0.1);
node = limit(node, duration);
node = set_gain(node, gain);
node = delay(node, shift);
return node;
}
// whitespace-separated tokens which are either:
// * wait:DURATION - increase global time shift by DURATION ms.
// * FREQUENCY:DURATION:GAIN[:SHIFT[:INSTRUMENT]] - see play_frequency().
// SHIFT here is local time shift, global time shift will remain intact. Default: 0.
// INSTRUMENT is the name of the corresponding instrument. Default: sine.
function parse_sequence(s){
var output = [];
var notes = s.split(/\s+/);
for (var i = 0; i < notes.length; i++){
var values = notes[i].split(':');
var note_data = {};
if (values[0] == 'wait'){
note_data.wait = true;
note_data.dur = parseInt(values[1]); // duration in ms
} else {
note_data.freq = parseFloat(values[0]); // frequency
note_data.dur = parseInt(values[1]); // duration in ms
note_data.gain = parseFloat(values[2]); // gain
note_data.shift = parseInt(values[3] || '0'); // shift in ms
note_data.instrument = values[4] || 'sine';
}
output.push(note_data);
}
return output;
}
// parse and play a sequence
function demonstrate_sequence(s, custom_spectra){
var seq = parse_sequence(s);
var waves = {};
for (var i = 0; i < seq.length; i++){
if (seq[i].instrument && waves[seq[i].instrument] === undefined){
waves[seq[i].instrument] = construct_wave(
builtin_spectra[seq[i].instrument] || custom_spectra[seq[i].instrument]
);
}
}
var notes = [];
var total_duration = 0;
var total_shift = 0;
for (var i = 0; i < seq.length; i++){
if (seq[i].wait){
total_shift += seq[i].dur;
} else {
notes.push(play_frequency(
seq[i].freq,
seq[i].dur,
total_shift + seq[i].shift,
seq[i].gain,
waves[seq[i].instrument]
));
if (total_shift + seq[i].shift + seq[i].dur > total_duration)
total_duration = total_shift + seq[i].shift + seq[i].dur;
}
}
total_duration += 100;
connect_all(notes, total_duration);
return total_duration;
}
document.querySelectorAll('.sequence-audio').forEach(function(button){
// error handling
if (!button.hasAttribute('data-sequence')){
console.error('Audio element requires "data-sequence" attribute to be set!');
console.log(button);
button.classList.add('sequence-audio-invalid');
return;
}
if (button.hasAttribute('data-playing')){
console.error('Audio element should not have "data-playing" attribute set!');
console.log(button);
button.classList.add('sequence-audio-invalid');
return;
}
var seq = parse_sequence(button.getAttribute('data-sequence'));
for (var i = 0; i < seq.length; i++){
if (seq[i].instrument && builtin_spectra[seq[i].instrument] === undefined){
if (!button.hasAttribute('data-timbre-' + seq[i].instrument)){
console.error('Audio element requires "data-timbre-' + seq[i].instrument + '" attribute to be set!');
console.log(button);
button.classList.add('sequence-audio-invalid');
return;
}
}
}
// indicate that audio JS has been loaded
button.classList.add('sequence-audio-valid')
button.addEventListener('click', function(){
// should the button lock while the sequence is playing?
var lock = button.hasAttribute('data-lock');
if (lock && button.hasAttribute('data-playing')){
// do nothing
} else {
if (lock){
button.setAttribute('data-playing', 'true');
button.classList.add('sequence-audio-playing');
}
var s = button.getAttribute('data-sequence');
var custom_spectra = {};
for (var i = 0; i < button.attributes.length; i++){
if (button.attributes[i].name.startsWith('data-timbre-')){
var key = button.attributes[i].name.substring(12);
var val = button.attributes[i].value.split(/\s+/);
for (var j = 0; j < val.length; j++)
val[j] = parseFloat(val[j]);
custom_spectra[key] = val;
}
}
console.log('Custom spectra:', custom_spectra);
console.log('Playing sequence: ' + s);
var duration = demonstrate_sequence(s, custom_spectra);
if (lock){
setTimeout(function(){
button.classList.remove('sequence-audio-playing');
button.removeAttribute('data-playing');
}, duration);
}
}
});
});