Skip to content
Snippets Groups Projects
Commit 9f8f8934 authored by RANWEZ Pierre's avatar RANWEZ Pierre :anchor:
Browse files

:sparkles: feat: extension scripts

First audio / MIDI version works.
parent 3a41ddc2
Branches
Tags
1 merge request!1✨ feat: CSSLSD V2
let css = ""; let css = "body {\n\tbackground-color:rgba(0,0,0,[audio.meter.volume]);\n}";
let activate = false;
let audioB = false;
let midiB = false;
//Initialize the CSS storage on startup //Initialize the CSS storage on startup
chrome.runtime.onInstalled.addListener(() => { chrome.runtime.onInstalled.addListener(() => {
chrome.storage.sync.set({ css }); chrome.storage.sync.set({ css });
chrome.storage.sync.set({ activate });
chrome.storage.sync.set({ audioB });
chrome.storage.sync.set({ midiB });
}); });
/**
* Call start from content script
*/
chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
if (changeInfo.status == 'complete' && tab.active) {
chrome.tabs.query({ active: true, currentWindow: true },
function (tabs) {
chrome.tabs.sendMessage(tabs[0].id, { type: 'start'});
});
}
})
var audioEvents = [];
var midiEvents = [];
var audioContext = null;
var meter = null;
var analyser = null;
var rafID = null;
var buflen = 1024;
var buf = new Float32Array(buflen);
var audio = false;
var midi = false;
/**
* Parse CSS with CSSParser and after add events to midi or audio.
* @param {str} cssObj
*/
function parseCSS(cssObj) {
var parser = new CSSParser();
var sheet = parser.parse(cssObj, false, true);
sheet.cssRules.forEach(element => {
let els = document.querySelectorAll(element.mSelectorText);
element.declarations.forEach(css => {
els.forEach(e => {
if (css.valueText.includes('audio')) {
audioEvents.push({
'selector': element.mSelectorText,
'property': css.property,
'value': css.valueText
})
}
if (css.valueText.includes('midi')) {
midiEvents.push({
'selector': element.mSelectorText,
'property': css.property,
'value': css.valueText
})
}
else
e.style[css.property] = css.valueText;
});
});
});
}
/**
* Initialize Midi
*/
function midiApi() {
if (midi) {
WebMidi
.enable()
.then(launchMidi)
.catch(err => alert(err));
}
else {
WebMidi.disable();
}
}
/**
* Launch Midi after being started by midiAPi()
*/
function launchMidi() {
// Display available MIDI input devices
if (WebMidi.inputs.length < 1) {
alert("No device detected.");
} else {
WebMidi.inputs.forEach((device, index) => {
document.body.innerHTML += `${index}: ${device.name} <br>`;
});
const mySynth = WebMidi.inputs[0];
mySynth.channels[1].addListener("noteon", e => {
midiEvent('noteon', e);
});
mySynth.channels[1].addListener("controlchange", e => {
midiEvent('controlchange', e);
});
}
}
/**
* Trigger when midi is used. Change value between 0.0 and 1.0
* @param {str} type type of midi event
* @param {dict} data event object
*/
function midiEvent(type, data) {
midiEvents.forEach(event => {
template = event['value'].substring(
event['value'].indexOf("[") + 1,
event['value'].lastIndexOf("]")
);
eventType = template.split('.')[1];
eventName = template.split('.')[2];
if (type == 'noteon' && eventType == 'controlchange' && eventName == data.note.name) {
value = event['value'].replace('['+template+']', data.value);
let els = document.querySelectorAll(event['selector']);
els.forEach(e => {
e.style[event['property']] = value;
});
}
if (type == 'controlchange' && eventType == 'controlchange' && eventName == data.controller.name) {
value = event['value'].replace('['+template+']', data.value);
let els = document.querySelectorAll(event['selector']);
els.forEach(e => {
e.style[event['property']] = value;
});
}
});
}
function createAudioMeter(audioContext, clipLevel, averaging, clipLag) {
var processor = audioContext.createScriptProcessor(512);
processor.onaudioprocess = volumeAudioProcess;
processor.clipping = false;
processor.lastClip = 0;
processor.volume = 0;
processor.clipLevel = clipLevel || 0.98;
processor.averaging = averaging || 0.95;
processor.clipLag = clipLag || 750;
processor.connect(audioContext.destination);
processor.checkClipping =
function () {
if (!this.clipping)
return false;
if ((this.lastClip + this.clipLag) < window.performance.now())
this.clipping = false;
return this.clipping;
};
processor.shutdown =
function () {
this.disconnect();
this.onaudioprocess = null;
};
return processor;
}
function volumeAudioProcess(event) {
var buf = event.inputBuffer.getChannelData(0);
var bufLength = buf.length;
var sum = 0;
var x;
for (var i = 0; i < bufLength; i++) {
x = buf[i];
if (Math.abs(x) >= this.clipLevel) {
this.clipping = true;
this.lastClip = window.performance.now();
}
sum += x * x;
}
var rms = Math.sqrt(sum / bufLength);
this.volume = Math.max(rms, this.volume * this.averaging);
}
/**
* Initialize audio, request micro to user
*/
function audioApi() {
if (audio) {
window.AudioContext = window.AudioContext || window.webkitAudioContext;
audioContext = new AudioContext();
try {
navigator.getUserMedia =
navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia;
navigator.getUserMedia(
{
"audio": {
"mandatory": {
"googEchoCancellation": "false",
"googAutoGainControl": "false",
"googNoiseSuppression": "false",
"googHighpassFilter": "false"
},
"optional": []
},
}, gotStream, didntGetStream);
} catch (e) {
alert('getUserMedia threw exception :' + e);
}
}
else {
console.log('Audio suspend');
audioContext.suspend();
}
};
function sleepFor(sleepDuration) {
var now = new Date().getTime();
while (new Date().getTime() < now + sleepDuration) { /* Do nothing */ }
}
function didntGetStream() {
alert('Stream generation failed.');
}
var mediaStreamSource = null;
function gotStream(stream) {
// Create an AudioNode from the stream.
mediaStreamSource = audioContext.createMediaStreamSource(stream);
analyser = audioContext.createAnalyser();
analyser.fftSize = 2048;
// analyser.fftSize = 256;
mediaStreamSource.connect(analyser);
// Create a new volume meter and connect it.
meter = createAudioMeter(audioContext);
mediaStreamSource.connect(meter);
audioContext.resume();
console.log('Audio start');
// kick off the visual updating
audioEvent();
}
var noteStrings = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
function noteFromPitch(frequency) {
var noteNum = 12 * (Math.log(frequency / 440) / Math.log(2));
return Math.round(noteNum) + 69;
}
function frequencyFromNoteNumber(note) {
return 440 * Math.pow(2, (note - 69) / 12);
}
function centsOffFromPitch(frequency, note) {
return Math.floor(1200 * Math.log(frequency / frequencyFromNoteNumber(note)) / Math.log(2));
}
var MIN_SAMPLES = 0; // will be initialized when AudioContext is created.
function autoCorrelate(buf, sampleRate) {
var SIZE = buf.length;
var MAX_SAMPLES = Math.floor(SIZE / 2);
var best_offset = -1;
var best_correlation = 0;
var rms = 0;
var foundGoodCorrelation = false;
var correlations = new Array(MAX_SAMPLES);
for (var i = 0; i < SIZE; i++) {
var val = buf[i];
rms += val * val;
}
rms = Math.sqrt(rms / SIZE);
if (rms < 0.01) // not enough signal
return -1;
var lastCorrelation = 1;
for (var offset = MIN_SAMPLES; offset < MAX_SAMPLES; offset++) {
var correlation = 0;
for (var i = 0; i < MAX_SAMPLES; i++) {
correlation += Math.abs((buf[i]) - (buf[i + offset]));
}
correlation = 1 - (correlation / MAX_SAMPLES);
correlations[offset] = correlation; // store it, for the tweaking we need to do below.
if ((correlation > 0.9) && (correlation > lastCorrelation)) {
foundGoodCorrelation = true;
if (correlation > best_correlation) {
best_correlation = correlation;
best_offset = offset;
}
} else if (foundGoodCorrelation) {
var shift = (correlations[best_offset + 1] - correlations[best_offset - 1]) / correlations[best_offset];
return sampleRate / (best_offset + (8 * shift));
}
lastCorrelation = correlation;
}
if (best_correlation > 0.01) {
return sampleRate / best_offset;
}
return -1;
}
function freqToBin(freq, rounding = 'round') {
const max = analyser.frequencyBinCount - 1,
bin = Math[rounding](freq * 256 / audioContext.sampleRate);
return bin < max ? bin : max;
}
function audioEvent(time) {
analyser.getFloatTimeDomainData(buf);
var ac = autoCorrelate(buf, audioContext.sampleRate);
if (ac == -1) {
a = 5;
// $('#analyser1').text("--");
// $('#analyser2').text("-");
// $('#analyser3').text("--");
} else {
// $('#analyser1').text(Math.round(ac));
var note = noteFromPitch(ac);
// $('#analyser2').text(noteStrings[note % 12]);
var detune = centsOffFromPitch(ac, note);
// if (detune == 0) {
// $('#analyser3').text("--");
// } else {
// $('#analyser3').text(Math.abs(detune));
// }
}
var bufferLength = analyser.frequencyBinCount;
var dataArray = new Uint8Array(bufferLength);
analyser.getByteFrequencyData(dataArray);
const presets = {
bass: [20, 250],
lowMid: [250, 500],
mid: [500, 2e3],
highMid: [2e3, 4e3],
treble: [4e3, 16e3]
}
var startFreq, endFreq, startBin, endBin, energy;
energies = [];
Object.keys(presets).forEach(key => {
[startFreq, endFreq] = presets[key];
startBin = freqToBin(startFreq);
endBin = endFreq ? freqToBin(endFreq) : startBin;
energy = 0;
for (let i = startBin; i <= endBin; i++)
energy += dataArray[i];
energies[key] = energy / (endBin - startBin + 1) / 255;
});
audioEvents.forEach(event => {
value = 0;
if (event['value'].includes('[audio.energy.bass]')) {
value = event['value'].replace('[audio.energy.bass]', energies['bass']);
}
if (event['value'].includes('[audio.energy.mid]')) {
value = event['value'].replace('[audio.energy.mid]', energies['mid']);
}
if (event['value'].includes('[audio.energy.treble]')) {
value = event['value'].replace('[audio.energy.treble]', energies['treble']);
}
if (event['value'].includes('[audio.meter.volume]')) {
value = event['value'].replace('[audio.meter.volume]', meter.volume * 4);
}
let els = document.querySelectorAll(event['selector']);
els.forEach(e => {
e.style[event['property']] = value;
});
});
// if (meter.checkClipping())
// canvasContext.fillStyle = "red";
// else
// canvasContext.fillStyle = "green";
// canvasContext.fillRect(0, 0, meter.volume * WIDTH * 1.4, HEIGHT);
// set up the next visual callback
sleepFor(20);
if (audio) {
rafID = window.requestAnimationFrame(audioEvent);
}
}
/**
* Function trigger when new message received.
* @param {type, data} message parameters
*/
function onMessage({ type, data }) {
console.log('onMessage', type, data);
switch (type) {
case 'update': {
const css = data.cssStr;
audioEvents = [];
parseCSS(css)
break;
}
case 'start': {
chrome.storage.sync.get(['activate'], function (result) {
console.log(result.activate);
if (result.activate) {
chrome.storage.sync.get(['css'], function (resultCss) {
parseCSS(resultCss.css)
});
}
});
break;
}
case 'audio': {
chrome.storage.sync.get(['audioB'], function (result) {
audio = result.audioB;
audioApi();
});
break;
}
case 'midi': {
midi = data;
midiApi();
break;
}
}
}
chrome.runtime.onMessage.addListener(onMessage);
/**
//Load the CSS editor * Load the editor and add css
*/
var editor = ace.edit("editor"); var editor = ace.edit("editor");
editor.setTheme("ace/theme/monokai"); editor.setTheme("ace/theme/monokai");
editor.session.setMode("ace/mode/css"); editor.session.setMode("ace/mode/css");
editor.setOptions({
enableBasicAutocompletion: [{
getCompletions: (editor, session, pos, prefix, callback) => {
// note, won't fire if caret is at a word that does not have these letters
callback(null, [
{ value: '[audio.meter.volume]', score: 1, meta: 'Audio meter' },
{ value: '[audio.energy.bass]', score: 1, meta: 'Audio energy bass' },
{ value: '[audio.energy.mid]', score: 1, meta: 'Audio energy mid' },
{ value: '[audio.energy.treble]', score: 1, meta: 'Audio energy treble' },
{ value: '[midi.noteon.C]', score: 1, meta: 'MIDI Note C' },
{ value: '[midi.noteon.D]', score: 1, meta: 'MIDI Note D' },
{ value: '[midi.noteon.E]', score: 1, meta: 'MIDI Note E' },
{ value: '[midi.noteon.F]', score: 1, meta: 'MIDI Note F' },
{ value: '[midi.noteon.G]', score: 1, meta: 'MIDI Note G' },
{ value: '[midi.noteon.A]', score: 1, meta: 'MIDI Note A' },
{ value: '[midi.noteon.B]', score: 1, meta: 'MIDI Note B' },
]);
},
}],
// to make popup appear automatically, without explicit _ctrl+space_
enableLiveAutocompletion: true,
});
chrome.storage.sync.get(['css'], function (result) {
editor.setValue(result.css)
});
//When the CSS editor changes, store it in the shared storage /**
//and call the background parseCSS script * Save CSS in editor via save button
editor.getSession().on('change', function() { */
function updateCss() {
chrome.tabs.query({ active: true, currentWindow: true }, chrome.tabs.query({ active: true, currentWindow: true },
function (tabs) { function (tabs) {
let cssStr = editor.getValue(); let cssStr = editor.getValue();
const payload = {
cssStr
}
chrome.storage.sync.set({ css: cssStr }); chrome.storage.sync.set({ css: cssStr });
chrome.scripting.executeScript({ chrome.tabs.sendMessage(tabs[0].id, { type: 'update', data: payload });
target: { tabId: tabs[0].id },
function: parseCSS
});
}); });
}); }
function parseCSS() {
console.log("Parsing CSS");
//Get access to the css written in the popup
chrome.storage.sync.get("css", function(cssObj) {
//Split the rules $('#save').on('click', function () {
let rules = cssObj.css.split("}"); updateCss();
for(let r of rules) { });
console.log(r);
//Test if not empty
if(r.search(/[\w]+/i)>=0) {
//Retrieve the selector
let sel = r.split("{")[0];
console.log("Selector ", sel);
//Retrieve the corresponding elements /**
let els = document.querySelectorAll(sel); * Activate / Desactivate extension via popup
for(let e of els) { */
var activateButton;
chrome.storage.sync.get(['activate'], function (result) {
activateButton = result.activate;
if (activateButton) {
$('#onOff').text('Désactiver');
} else {
$('#onOff').text('Activer');
}
});
//Apply the properties to each $('#onOff').on('click', function () {
activateButton = !activateButton;
chrome.storage.sync.set({ activate: activateButton }, function () {
if (activateButton) {
$('#onOff').text('Désactiver');
} else {
$('#onOff').text('Activer');
}
});
});
//TEMP, just put the background color in blue /**
e.style["background-color"] = "blue"; * Activate / Desactivate audio button
*/
var audioButton = false;
chrome.storage.sync.get(['audioB'], function (result) {
audioButton = result.audioB;
if (audioButton) {
$('#audio').text('Désactiver le micro')
} else {
$('#audio').text('Activer le micro')
} }
});
$('#audio').on('click', function () {
audioButton = !audioButton;
if (audioButton) {
$('#audio').text('Désactiver le micro')
} else {
$('#audio').text('Activer le micro')
} }
chrome.storage.sync.set({ audioB: audioButton }, function () {
});
chrome.tabs.query({ active: true, currentWindow: true },
function (tabs) {
chrome.tabs.sendMessage(tabs[0].id, { type: 'audio', data: audioButton });
});
});
/**
* Activate / Desactivate midi button
*/
var midiButton = false;
chrome.storage.sync.get(['midiB'], function (result) {
midiButton = result.midiB;
if (midiButton) {
$('#midi').text('Désactiver l\'entrée MIDI')
} else {
$('#midi').text('Activer l\'entrée MIDI')
} }
}); });
$('#midi').on('click', function () {
midiButton = !midiButton;
if (midiButton) {
$('#midi').text('Désactiver l\'entrée MIDI')
} else {
$('#midi').text('Activer l\'entrée MIDI')
} }
chrome.storage.sync.set({ midiB: midiButton }, function () {
});
chrome.tabs.query({ active: true, currentWindow: true },
function (tabs) {
chrome.tabs.sendMessage(tabs[0].id, { type: 'midi', data: midiButton });
});
});
...@@ -9,5 +9,19 @@ ...@@ -9,5 +9,19 @@
"permissions": ["storage", "activeTab", "scripting"], "permissions": ["storage", "activeTab", "scripting"],
"action": { "action": {
"default_popup": "popup.html" "default_popup": "popup.html"
},
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"webmidi.js",
"cssParser.js",
"content.js"
],
"run_at": "document_end",
"match_about_blank": true
} }
]
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment