User:Plumtree/common.js
Jump to navigation
Jump to search
Note: After saving, 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)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Go to Menu → Settings (Opera → Preferences on a Mac) and then to Privacy & security → Clear browsing data → Cached images and files.
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();
if (shift >= 0){
node.stop(audio.currentTime + duration / 1000 + 0.1);
node = limit(node, duration);
node = delay(node, shift);
} else {
node.stop(audio.currentTime + (shift + duration) / 1000 + 0.1);
node = limit(node, shift + duration);
}
node = set_gain(node, gain);
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, start_from){
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 = -(start_from || 0);
for (var i = 0; i < seq.length; i++){
if (seq[i].wait){
total_shift += seq[i].dur;
} else if (total_shift + seq[i].shift + seq[i].dur > 0) {
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);
var stop = function(){
for (var i = 0; i < notes.length; i++)
notes[i].disconnect();
};
return [total_duration, stop];
}
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;
}
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')
var start_time = null;
var paused_at = null;
var stop_function = null;
button.addEventListener('click', function(){
// should the button lock while the sequence is playing?
var lock = button.hasAttribute('data-lock');
// should the sequence be paused instead of being stopped on interruptions?
var pause = button.hasAttribute('data-pause');
if (lock && button.classList.contains('sequence-audio-playing')){
if (pause){
// try pausing the sequence
if (start_time !== null){
const end_time = new Date();
if (paused_at !== null){
paused_at += end_time.getTime() - start_time.getTime();
} else {
paused_at = end_time.getTime() - start_time.getTime();
}
start_time = null;
console.log('Pausing at ' + paused_at);
}
if (typeof stop_function === 'function'){
stop_function();
stop_function = null;
button.classList.add('sequence-audio-paused');
} else {
console.error('Could not pause the sequence.');
}
} else {
// try stopping the sequence
if (typeof stop_function === 'function'){
stop_function();
stop_function = null;
} else {
console.error('Could not stop the sequence.');
}
}
} else {
if (lock){
button.classList.add('sequence-audio-playing');
button.classList.remove('sequence-audio-paused');
}
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);
if (paused_at !== null){
console.log('Starting from:' + paused_at)
}
var result = demonstrate_sequence(s, custom_spectra, paused_at);
var duration = result[0];
var stop = result[1];
start_time = new Date();
if (lock){
const cleanup = setTimeout(function(){
button.classList.remove('sequence-audio-playing');
paused_at = null;
}, duration);
stop_function = function(){
stop();
clearTimeout(cleanup);
button.classList.remove('sequence-audio-playing');
}
}
}
});
});
const timbre_selectors = document.querySelectorAll('.sequence-audio-timbre-selector');
if (timbre_selectors.length > 0){
// loading OOUI
mw.loader.using(['oojs-ui-core', 'oojs-ui-widgets']).done(function(){
timbre_selectors.forEach(function(element){
var targets = [];
for (var i = 0; i < element.attributes.length; i++){
if (element.attributes[i].name.startsWith('data-target')){
const target_id = element.attributes[i].value;
const target = document.getElementById(target_id);
if (target === null){
console.error('Timbre selector requires each "data-target" to point to a valid HTML element!');
console.log(element);
continue;
}
if (!target.classList.contains('sequence-audio')){
console.error('Timbre selector requires each "data-target" to point to a sequence-audio element!')
console.log(element);
continue;
}
if (!target.classList.contains('sequence-audio-valid')){
console.error('Timbre selector requires each "data-target" to point to a valid sequence-audio element!')
console.log(element);
continue;
}
targets.push(target);
}
}
var instruments = Object.keys(builtin_spectra);
for (var i = 0; i < targets.length; i++){
for (var j = 0; j < targets[i].attributes.length; j++){
const name = targets[i].attributes[j].name;
if (name.startsWith('data-timbre-')){
const instrument = name.substring(12);
if (!instruments.includes(instrument))
instruments.push(instrument);
}
}
}
var default_instrument = null;
var storage_key = null;
if (element.hasAttribute('data-key')){
storage_key = element.getAttribute('data-key');
const storage_value = window.localStorage.getItem('timbre-' + storage_key);
if (storage_value && instruments.includes(storage_value)){
default_instrument = storage_value;
}
}
if (!default_instrument && element.hasAttribute('data-default')){
const attribute_value = element.getAttribute('data-default');
if (instruments.includes(attribute_value)){
default_instrument = attribute_value;
}
}
if (!default_instrument){
default_instrument = 'sine';
}
var options = [];
for (var i = 0; i < instruments.length; i++){
options.push(new OO.ui.ButtonOptionWidget({
data: i,
label: instruments[i]
}));
}
var selector = new OO.ui.ButtonSelectWidget({
items: options
});
selector.on('select', function(item){
const instrument = item.label;
console.log('Switching to ' + instrument);
if (storage_key){
window.localStorage.setItem('timbre-' + storage_key, instrument);
}
for (var i = 0; i < targets.length; i++){
var seq = parse_sequence(targets[i].getAttribute('data-sequence'));
var s = '';
for (var j = 0; j < seq.length; j++){
if (j > 0){ s += ' '; }
if (seq[j].wait){
s += 'wait:' + seq[j].dur;
} else {
s += seq[j].freq + ':' + seq[j].dur + ':' + seq[j].gain + ':' + seq[j].shift + ':' + instrument
}
}
console.log(s);
targets[i].setAttribute('data-sequence', s);
}
});
selector.selectItemByLabel(default_instrument);
element.replaceWith(selector.$element[0]);
});
});
}