diff --git a/README.md b/README.md index 8093bda43976ff8bb8756ac318142111f204a046..e85e12485db2993b6a5b7898d167db671d2bd383 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ To ***save*** your template, click on `Save` button or use keybind `CTRL`+`S`. T |-------------|-----------|------------------------------------------| | note | id | Note on, use case : [note:21] | | cc | id, range | Control change, use case : [cc:10,0,100] | -| pb | | Pitchbend, use case : [pb,0,100] | +| pb | range | Pitchbend, use case : [pb,0,100] | ### Audio Variables @@ -58,6 +58,16 @@ a { } ``` +## Looping + +***- - WIP FEATURE - -*** + +Click record button, play on MIDI device then stop recording. + +On click, all tick defined buy a BPM, save midi envent in a disctionnary with a timestamp. + +On loop play, execute CSS style in relation with loop. + ## Ressources - Chrome extensions : https://developer.chrome.com/docs/extensions/mv3/getstarted/ diff --git a/background.js b/background.js index 97f1689271ce082480a907e19c8830f309771be8..602f013f827d2e25132f4e6d510c514bfd31cace 100644 --- a/background.js +++ b/background.js @@ -6,6 +6,7 @@ let midiB = false; let audioI = false; let midiI = false; let popup = false; +let loopPlay = false; //Initialize the CSS storage on startup chrome.runtime.onInstalled.addListener(() => { @@ -17,6 +18,7 @@ chrome.runtime.onInstalled.addListener(() => { chrome.storage.sync.set({ audioI }); chrome.storage.sync.set({ midiI }); chrome.storage.sync.set({ popup }); + chrome.storage.sync.set({ loopPlay }); }); /** diff --git a/content.js b/content.js index cdb0159c392aff70d618e35965af99679138396c..e336df91eb08a3c83b5593db2d9bbab388bc5bf3 100644 --- a/content.js +++ b/content.js @@ -9,6 +9,17 @@ var buf = new Float32Array(buflen); var audio = false; var midi = false; var audioSample = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var record = false; +var records = []; // array of recorded values +var recording = { + time: new Date().getTime(), + events: [ + [1.0, 'controlchange', '1', 0], + [2.0, 'controlchange', '1', 1], + ] +}; // dict with current record +var time; +var playLoop = false; /** * Parse CSS with CSSParser and after add events to midi or audio. @@ -179,7 +190,7 @@ function midiEvent(type, data) { }); midiEvents.forEach(event => { midiValue = data.value; - templates = templateToDict(event['value']); + templates = event['templates']; templates.forEach(template => { midiValue = (template['min'] + ((template['max'] - template['min']) * midiValue)); if (type == 'noteon' && template['eventType'] == 'note' && template['eventName'] == data.note.number) { @@ -205,8 +216,80 @@ function midiEvent(type, data) { } }); }); + + var newTime = new Date().getTime(); + bpm = ((1 / ((newTime - time) / 1000)) * 60); + time = newTime; + if(record && bpm == 120){ + switch (type) { + case 'controlchange': + recording['events'].push([(newTime - time)/1000, type, data.controller.name, data.value]); + break; + case 'noteon': + recording['events'].push([(newTime - time)/1000, type, data.note.numer, data.value]); + break; + case 'pitchbend': + recording['events'].push([(newTime - time)/1000, type, type, data.value]); + break; + } + } + } +// Function that play recorded midi loop +function playMidiLoop(loopId) { + if (records[loopId]) { + playLoopInfo = true; + while (playLoopInfo && playLoop && records[loopId]['events'].length > 0) { + chrome.storage.sync.get(['loopPlay'], function (result) { + playLoopInfo = result.loopPlay; + }); + console.log(playLoopInfo); + for (let index = 0; index < records[loopId]['events'].length; index++) { + const event = records[loopId]['events'][index]; + const eventNext = records[loopId]['events'][(index+1)%records[loopId]['events'].length]; + // !!! TODO: add a way to play midi events + audioEvents.forEach(event => { + midiValue = event[3]; + console.log(event[3]); + templates = event['templates']; + templates.forEach(template => { + midiValue = (template['min'] + ((template['max'] - template['min']) * midiValue)); + if (event[1] == 'noteon' && template['eventType'] == 'note' && template['eventName'] == event[2]) { + value = event['value'].replace('[' + template['templateFull'] + ']', midiValue); + let els = document.querySelectorAll(event['selector']); + els.forEach(e => { + e.style[event['property']] = value; // Note velocity + }); + } + if (event[1] == 'controlchange' && template['eventType'] == 'cc' && template['eventName'] == event[2]) { + value = event['value'].replace('[' + template['templateFull'] + ']', midiValue); + let els = document.querySelectorAll(event['selector']); + els.forEach(e => { + e.style[event['property']] = value; // Control value + }); + } + if (event[1] == 'pitchbend' && template['eventType'] == 'pb') { + value = event['value'].replace('[' + template['templateFull'] + ']', midiValue); + let els = document.querySelectorAll(event['selector']); + els.forEach(e => { + e.style[event['property']] = value; // Pitchbend value + }); + } + if (event[1] == 'loud' && template['eventType'] == 'loud') { + value = event['value'].replace('[' + template['templateFull'] + ']', midiValue); + let els = document.querySelectorAll(event['selector']); + els.forEach(e => { + e.style[event['property']] = value; // Pitchbend value + }); + } + }); + }); + sleepFor((event[0] - eventNext[0])*1000); + } + } + } +} function createAudioMeter(audioContext, clipLevel, averaging, clipLag) { var processor = audioContext.createScriptProcessor(512); @@ -283,7 +366,7 @@ function audioApi() { } } else { - console.warn('Audio suspend'); + console.info('Audio suspend'); audioContext.suspend(); chrome.storage.sync.set({ audioI: false }); audio = false; @@ -320,7 +403,7 @@ function gotStream(stream) { meter = createAudioMeter(audioContext); mediaStreamSource.connect(meter); audioContext.resume(); - console.warn('Audio start'); + console.info('Audio start'); chrome.storage.sync.set({ audioI: true }); chrome.runtime.sendMessage({ type: 'updateUi', data: true }); // kick off the visual updating @@ -481,6 +564,14 @@ function audioEvent() { }); } }); + var newTime = new Date().getTime(); + bpm = ((1 / ((newTime - time) / 1000)) * 60); + time = newTime; + if(record && bpm == 120){ + + recording['events'].push([(newTime - time)/1000, 'loud', 'data.controller.name', meter.volume * 1.4]); + + } // sleepFor(20); if (audio) { rafID = window.requestAnimationFrame(audioEvent); @@ -524,6 +615,33 @@ function onMessage({ type, data }) { midiApi(); break; } + case 'playLoop': { + playLoop = !playLoop; + playMidiLoop(data); + break; + } + case 'getLoop': { + chrome.runtime.sendMessage({ type: 'midiRecords', data: records }); + break; + } + case 'record': { + record = data; + // If record in stopped add it to the list of records. + if (!record) { + recording['time'] = (new Date().getTime() - recording['time']) / 1000; + records.push(recording); + recording = { + time: new Date().getTime(), + events: [] + }; + chrome.runtime.sendMessage({ type: 'midiRecords', data: records }); + } + else { + time = new Date().getTime(); + recording['time'] = new Date().getTime(); + } + break; + } } } diff --git a/main.js b/main.js index 10a475bcf731719ca9a54847b19254309c68b088..895bd8d527fe76e8132e9c0f4ca31eac896d2a6c 100644 --- a/main.js +++ b/main.js @@ -96,6 +96,11 @@ function initUi() { $('.midiI').removeClass('active'); } }); + chrome.tabs.query({ active: true, currentWindow: true }, + function (tabs) { + chrome.tabs.sendMessage(tabs[0].id, { type: 'getLoop', data: false }); + } + ); } @@ -142,6 +147,13 @@ function onMessage({ type, data }) { $('#midiDevices').text(data); break; } + case 'midiRecords': { + $('#loopList').text(''); + for (let i = 0; i < data.length; i++) { + $('#loopList').append('<tr><td>Loop ' + i + '</td><td>' + data[i]['time'] + 'ms</td><td>' + data[i]['events'].length + '</td><td><button id="loopPlay" value="' + i + '">▶️ Play</button></td></tr>'); + } + break; + } } } @@ -245,6 +257,40 @@ $('.midiI').on('click', function () { initUi(); }); +/** + * Record loop button + */ +var record = false; +$('#record').on('click', function () { + record = !record; + if (record) { + $('#record').text('⏹️ Stop'); + } + else { + $('#record').text('⏺️ Record'); + } + chrome.tabs.query({ active: true, currentWindow: true }, + function (tabs) { + chrome.tabs.sendMessage(tabs[0].id, { type: 'record', data: record }); + }); + +}); +$(document).on('click', '#loopPlay', function () { + if ($(this).text().includes('Play')) { + $(this).text('⏸️ Pause'); + chrome.storage.sync.set({ loopPlay: true }); + } else { + $(this).text('▶️ Play'); + chrome.storage.sync.set({ loopPlay: false }); + + } + id = $(this).val(); + chrome.tabs.query({ active: true, currentWindow: true }, + function (tabs) { + chrome.tabs.sendMessage(tabs[0].id, { type: 'playLoop', data: id }); + }); +}); + // Wait messages from content script chrome.runtime.onMessage.addListener(onMessage); diff --git a/popup.html b/popup.html index c36207d9ae612ff576b5b6c1da3c1631ae415778..c0a42df8b58937450ee273719c713934e5d6ecbd 100644 --- a/popup.html +++ b/popup.html @@ -22,6 +22,24 @@ </div> <div id="editor"></div> + <div> + <h3>Loop Settings</h3> + <button id="record">Record</button> BPM : <input id="bpm" type="number" value="120" disabled> + <table width="100%"> + <thead> + <tr> + <th>Nom</th> + <th>Temps</th> + <th>Nombre d'evenements</th> + <th>Actions</th> + </tr> + </thead> + <tbody id="loopList"> + + </tbody> + </table> + </div> + <script src="jquery-3.6.0.min.js" type="text/javascript" charset="utf-8"></script> <script src="ace/ace.js" type="text/javascript" charset="utf-8"></script> <script src="ace/ext-language_tools.js" type="text/javascript" charset="utf-8"></script>