variabletime/bin/web/js/midiController.js

769 lines
28 KiB
JavaScript
Raw Normal View History

2023-09-24 18:39:52 +02:00
'use strict'
import {
mix,
getMix,
mixObject,
} from './utils.js'
const PhysicalMidiMapping = {
"Launch Control MIDI 1": {
"knobs": [
21, 22, 23, 24, 25, 26, 27, 28, // first row
41, 42, 43, 44, 45, 46, 47, 48, // second row
],
"buttons": [
9, 10, 11, 12, 25, 26, 27, 28,
],
"arrows": [
114, 115, 116, 117, // up, down, left, right
]
}
};
const generalControl = {
"Launch Control MIDI 1": {}
};
const MidiController = function() {
window.mixObject = mixObject;
const element = document.querySelector('#midiController');
const openCloseButton = document.querySelector('#midi_open');
const buttons = element.querySelector(".buttons");
let inputs;
let outputs;
let isPanelOpen = false;
let layers = [];
let debugLog = false;
let isInitialized = false;
let activeLayer = 0;
let activePropSet = 0;
window.activeLayer = activeLayer;
let artboardWidth = 1920;
let lastSelectionPoint = -1;
let playbackSpeed = 1;
let setFontVariation = (layer, layerIndex, button, midiValue) => {
if (layer.theatreObject.value.hasOwnProperty('fontVariationAxes') &&
typeof layer.theatreObject.value.fontVariationAxes === 'object') {
const axes = layer.theatreObject.value.fontVariationAxes;
const index = button - 46;
const keys = Object.keys(axes);
if (index < keys.length) {
const key = keys[index];
const axesProps = layer.props.fontVariationAxes.props[key];
const min = axesProps.range[0];
const max = axesProps.range[1];
const v = (midiValue / 127.0) * (max - min) + min;
if (!currentValues[layerIndex].hasOwnProperty('fontVariationAxes')) {
currentValues[layerIndex].fontVariationAxes = {};
}
currentValues[layerIndex].fontVariationAxes[key] = v;
//tp.studio.transaction(({
//set
//}) => {
//set(layer.theatreObject.props.fontVariationAxes[key], v);
//});
}
}
};
let setLetterDelays = (layer, layerIndex, button, midiValue) => {
if (layer.theatreObject.value.hasOwnProperty('letterDelays')) {
const letterDelays = layer.theatreObject.value.letterDelays;
const keys = Object.keys(letterDelays);
const min = 0;
const max = 2000;
const v = (midiValue / 127.0) * (max - min) + min;
//console.log('MidiController::setLetterDelays - font has letterDelays', JSON.parse(JSON.stringify(letterDelays)));
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (typeof letterDelays[key] === 'object') {
const subKeys = Object.keys(letterDelays[key]);
for (let si = 0; si < subKeys.length; si++) {
const subKey = subKeys[si];
if (!currentValues[layerIndex].hasOwnProperty('letterDelays')) {
currentValues[layerIndex].letterDelays = {};
}
if (!currentValues[layerIndex].letterDelays.hasOwnProperty(key)) {
currentValues[layerIndex].letterDelays[key] = {};
}
currentValues[layerIndex].letterDelays[key][subKey] = v;
//tp.studio.transaction(({
//set
//}) => {
//set(layer.theatreObject.props.letterDelays[key][subKey], v);
//});
}
} else {
if (!currentValues[layerIndex].hasOwnProperty('letterDelays')) {
currentValues[layerIndex].letterDelays = {};
}
currentValues[layerIndex].letterDelays[key] = v;
//tp.studio.transaction(({
//set
//}) => {
//set(layer.theatreObject.props.letterDelays[key], v);
//});
}
}
} else {
//console.log('MidiController::setLetterDelays - font has no letterDelays');
}
};
let mirror_x = false;
let mirror_y = false;
let mirror_xy = false;
let setMirror = (button, midiValue) => {
}
let setLayer = (button, midiValue) => {
const layers = getLayers();
if (button === 116 && midiValue === 127) {
activeLayer = (activeLayer + 1) % layers.length;
} else if (button === 117 && midiValue === 127) {
activeLayer = (activeLayer - 1 + layers.length) % layers.length;
}
layers[activeLayer].showBoundingBoxDiv();
setTimeout(() => {
layers[activeLayer].hideBoundingBoxDiv();
}, 100);
};
let setProject = (button, midiValue) => {
const projects = tp.listProjects();
const activeProject = projects.indexOf(tp.sheet.project.address.projectId);
let direction;
if (button === 114 && midiValue === 127) {
direction = 1;
} else if (button === 115 && midiValue === 127) {
direction = -1;
}
const nextProjectIndex = (activeProject + direction + projects.length) % projects.length;
const nextProject = projects[nextProjectIndex];
tp.reloadToProject(nextProject, true);
};
let setBackgroundOpacity = (knob, midiValue) => {
let prop = 'backgroundColor.a';
setValue(getArtboard(), layers.length, prop, midiValue, [0, 1]);
};
let setBackgroundColor = (button, midiValue) => {
let pr = 'backgroundColor.r';
let pg = 'backgroundColor.g';
let pb = 'backgroundColor.b';
let r = Math.random() * 127.0;
let g = Math.random() * 127.0;
let b = Math.random() * 127.0;
setValue(getArtboard(), layers.length, pr, r, [0, 1]);
setValue(getArtboard(), layers.length, pg, g, [0, 1]);
setValue(getArtboard(), layers.length, pb, b, [0, 1]);
};
let setSpeed = (knob, midiValue) => {
if (midiValue >= 62 && midiValue <= 64) {
tp.sheet.sequence.pause();
} else {
const min = -6;
const max = 6;
let v = (midiValue / 127.0) * (max - min) + min;
if (v > 0) {
tp.sheet.sequence.play({
iterationCount: Infinity,
rate: v
});
} else if (v < 0) {
tp.sheet.sequence.play({
direction: 'reverse',
iterationCount: Infinity,
rate: Math.abs(v)
});
}
playbackSpeed = Math.abs(v);
}
};
const sentValues = [];
const sentMidiValues = [];
const currentValues = [];
const valueBuffer = [];
const populateValueBuffer = (_layers) => {
_layers.forEach((layer) => {
currentValues.push({});
sentValues.push({});
sentMidiValues.push({});
valueBuffer.push(new Map());
//console.log('pushed -------------------> ', _layers.length, JSON.parse(JSON.stringify(layer.theatreObject.value)));
});
// artboard
currentValues.push({});
sentValues.push({});
sentMidiValues.push({});
valueBuffer.push(new Map());
};
const addValuesToBuffer = (layerIndex, values, time_s, start_time_s) => {
const layerValueBuffer = valueBuffer[layerIndex];
const copiedValues = JSON.parse(JSON.stringify(values));
if (start_time_s !== -1) {
layerValueBuffer.forEach((value, value_time_s) => {
if (value_time_s > start_time_s && value_time_s <= time_s) {
const keys = Object.keys(values);
for (let k = 0; k < keys.length; k++) {
delete layerValueBuffer.get(value_time_s)[keys[k]];
if (Object.keys(layerValueBuffer.get(value_time_s)).length === 0) {
layerValueBuffer.delete(value_time_s);
break;
}
};
}
});
}
if (layerValueBuffer.has(time_s)) {
layerValueBuffer.set(time_s, {...layerValueBuffer.get(time_s), ...copiedValues});
} else {
layerValueBuffer.set(time_s, copiedValues);
}
};
const getValuesFromBuffer = (layerIndex, time_s) => {
if (valueBuffer[layerIndex].size === 0) {
return {};
} else {
valueBuffer[layerIndex] = new Map([...valueBuffer[layerIndex].entries()].sort());
let mergedValues = {};
let didMergeValues = {};
valueBuffer[layerIndex].forEach((value, value_time_s) => {
if (value_time_s < time_s) {
mergedValues = {...mergedValues, ...value};
} else {
if (Object.keys(didMergeValues).length === 0) {
didMergeValues = JSON.parse(JSON.stringify(mergedValues));
}
Object.keys(value).forEach((key) => {
if(!didMergeValues.hasOwnProperty(key)) {
mergedValues[key] = value[key];
}
});
}
});
return mergedValues;
}
};
this.getValuesFromBuffer = getValuesFromBuffer;
this.currentValue = currentValues;
this.valueBuffer = valueBuffer;
const smoothed = {
184: [21, 22, 23, 24, 26, 27, 28, 41, 42, 43, 44, 45, 46, 47, 48]
};
const ledButtonRowStatus = [0, 0, 0, 0, 0, 0, 0, 0];
const ledButtonRowMapping = [9, 10, 11, 12, 25, 26, 27, 28];
const ledColors = [ 0, 12, 13, 15, 29, 63, 62, 28, 60 ];
const setLed = (button, color, statusCode = 152) => {
outputs.forEach((midiOutput) => {
if (midiOutput.name === "Launch Control MIDI 1") {
midiOutput.send([statusCode, button, color]);
}
});
}
this.ledTimeColor = 2;
this.setLed = setLed;
this.ledMapping = ledButtonRowMapping;
this.ledColors = ledColors;
const mapping = {
"Launch Control MIDI 1": {
'general': {
184: { // status code
114: setProject,
115: setProject,
116: setLayer,
117: setLayer,
28: setSpeed,
25: setBackgroundOpacity,
},
// buttons
152: { // down
9: setBackgroundColor,
10: setBackgroundColor,
11: setBackgroundColor,
12: setBackgroundColor,
25: setBackgroundColor,
26: setBackgroundColor,
27: setBackgroundColor,
28: setBackgroundColor,
},
},
'props': [{
// knobs
184: {
// first row
21: ['x', [0, 1920]],
22: ['y', [0, 1080]],
23: ['fontSize_px', [-1000, 1000]],
24: ['rotation', [-360, 360]],
25: ['transformOrigin', ['top_left', 'top_right', 'center', 'bottom_left', 'bottom_right']],
26: ['letterSpacing', [-3, 3]],
27: ['lineHeight', [0, 10]],
// 28 free
// second row
41: ['color.r', [0, 1]],
42: ['color.g', [0, 1]],
43: ['color.b', [0, 1]],
44: ['color.a', [0, 1]],
45: setLetterDelays,
46: setFontVariation,
47: setFontVariation,
48: setFontVariation,
},
152: { // down
//9: setPropsSet,
//10: setPropsSet,
//11:
//12:
//25:
//26:
//27:
//28:
},
136: { // up
//9:
//10:
//11:
//12:
//25:
//26:
//27:
//28:
}
}]
}
};
this.mapping = mapping;
let updatingMidiValues = {};
let appliedMidiValues = {};
let doApplyMidiValues = true;
window.applyMidiTimeoutMs = 30;
this.mix = mix;
const applyMidiValuesInterval = () => {
if (doApplyMidiValues) {
// apply midi values
const device = "Launch Control MIDI 1";
const mkeys = Object.keys(updatingMidiValues);
if (updatingMidiValues.hasOwnProperty(device)) {
const keys = Object.keys(updatingMidiValues[device]);
for (let i = 0; i < keys.length; i++) {
const key = parseInt(keys[i]);
const statusCode = updatingMidiValues[device][key][0];
const midiValue = updatingMidiValues[device][key][1];
if (!appliedMidiValues[device].hasOwnProperty(keys[i]) ||
appliedMidiValues[device][keys[i]] !== midiValue) {
if (typeof mapping[device].general[statusCode][key] === 'function') {
mapping[device].general[statusCode][key](key, midiValue);
} else if (mapping[device].props[activePropSet][statusCode].hasOwnProperty(key)) {
const pm = mapping[device].props[activePropSet][statusCode][key];
if (typeof pm === 'function') {
pm(getLayers()[activeLayer], activeLayer, key, midiValue);
} else {
setValue(getLayers()[activeLayer], activeLayer, pm[0], midiValue, pm[1]);
}
}
appliedMidiValues[device][key] = updatingMidiValues[device][key];
}
delete updatingMidiValues[device][key];
}
}
setTimeout(() => {
if (doApplyMidiValues) {
requestAnimationFrame(applyMidiValuesInterval);
}
}, window.applyMidiTimeoutMs);
}
};
const directlyApplyMidiValues = (device, key, statusCode, midiValue) => {
if (typeof mapping[device].general[statusCode][key] === 'function') {
mapping[device].general[statusCode][key](key, midiValue);
} else if (mapping[device].props[activePropSet][statusCode].hasOwnProperty(key)) {
const pm = mapping[device].props[activePropSet][statusCode][key];
if (typeof pm === 'function') {
pm(getLayers()[activeLayer], activeLayer, key, midiValue);
} else {
setValue(getLayers()[activeLayer], activeLayer, pm[0], midiValue, pm[1]);
}
}
};
window.mapping = mapping;
if (!("requestMIDIAccess" in navigator)) {
element.innerHTML = `<h1>:-/</h1><p>I'm sorry, but your browser does not support the WebMIDI API ☹️🚫🎹</p>`;
}
const registerEvents = () => {
openCloseButton.addEventListener('click', () => {
if (!isPanelOpen) {
isPanelOpen = true;
element.style.display = 'flex';
} else {
isPanelOpen = false;
element.style.display = 'none';
}
});
const buttonOn = document.createElement('div');
buttonOn.innerHTML = "light on";
buttonOn.addEventListener('click', () => {
outputs.forEach((midiOutput) => {
midiOutput.send([152, 9, 2]);
});
});
const buttonOff = document.createElement('div');
buttonOff.innerHTML = "light off";
buttonOff.addEventListener('click', () => {
outputs.forEach((midiOutput) => {
midiOutput.send([152, 9, 0]);
});
});
const buttonDebug = document.createElement('div');
buttonDebug.innerHTML = "debug on";
buttonDebug.addEventListener('click', () => {
if (debugLog) {
debugLog = false;
buttonDebug.innerHTML = "debug on";
} else {
debugLog = true;
buttonDebug.innerHTML = "debug off";
}
});
buttons.append(buttonOn);
buttons.append(buttonOff);
buttons.append(buttonDebug);
};
window.debugCallTimes = [];
const tryGetLayers = (resolve) => {
if (getLayers().length > 0 && tp.isProjectLoaded) {
resolve();
} else {
setTimeout(() => {
tryGetLayers(resolve);
}, 10);
}
};
const tryGetLayersP = () => {
return new Promise((resolve) => {
tryGetLayers(resolve);
});
};
const selectLayers = () => {
return new Promise((resolve) => {
let delay = 500;//parseInt(localStorage.getItem('debugdelay'));
for (let i = 0; i <= layers.length; i++) {
setTimeout(() => {
if (i < layers.length) {
tp.studio.setSelection([layers[i].theatreObject]);
} else {
tp.studio.setSelection([]);
resolve();
}
}, i * delay);
}
});
};
window.ofRA = true;
window.ofUpdateMS = 1000 / 30;
const timeline_head = document.querySelector('#timeline_head');
const timeline = document.querySelector('#timeline');
let last_realtime_s = -999999;
let last_time_s = -999999;
let last_touchtime_s = -999999;
let last_processed_touchtime_s = -999999;
const ofUpdater = () => {
const realtime_s = performance.now() / 1000.0;
const time_s = tp.sheet.sequence.position;
const percent = time_s / tp.duration * 100;
{
const led = 114;
const statusCode = 184;
const color = [3,2,1,0][Math.floor(realtime_s * 4.0) % 4];
setLed(led, color, statusCode);
}
{
const led = 115;
const statusCode = 184;
const color = [0,1,2,3][Math.floor(realtime_s * 4.0) % 4];
setLed(led, color, statusCode);
}
{
const led = 117;
const statusCode = 184;
const color = [3,2,1,2][Math.floor(realtime_s * 6.0) % 4];
setLed(led, color, statusCode);
}
{
const led = 116;
const statusCode = 184;
const color = [1,2,3,2][Math.floor(realtime_s * 6.0) % 4];
setLed(led, color, statusCode);
}
for (let b = 0; b < ledButtonRowMapping.length; b++) {
const percentIndex = Math.floor((percent * 0.01) * ledButtonRowMapping.length);
if (b === percentIndex) {
ledButtonRowStatus[b] = Math.floor(Math.random() * 127.0);
}
setLed(ledButtonRowMapping[b], ledButtonRowStatus[b]);
}
let currentlyTouching = false;
if (realtime_s - last_touchtime_s < config.midi.touchTimeThreshold_s) {
currentlyTouching = true;
}
if (Object.keys(currentValues[activeLayer]).length > 0 && last_touchtime_s !== last_processed_touchtime_s) {
let starttime_s = -1;
if (realtime_s - last_realtime_s < config.midi.touchTimeThreshold_s) {
starttime_s = last_time_s; // fires first time prematurely, but this is okay (=> -1)
}
addValuesToBuffer(activeLayer, currentValues[activeLayer], time_s, starttime_s);
last_processed_touchtime_s = last_touchtime_s;
last_realtime_s = realtime_s;
last_time_s = time_s;
}
timeline_head.style.left = `calc(${percent}% - 10px)`;
timeline.style.background = currentlyTouching ? 'red' : 'grey';
for (let i = 0; i <= layers.length; i++) {
let bufferValues = JSON.parse(JSON.stringify(getValuesFromBuffer(i, time_s)));
bufferValues = {...bufferValues, ...currentValues[i]};
sentMidiValues[i] = mixObject(sentMidiValues[i], bufferValues, config.midi.smoothingMix);
if (i < layers.length) {
const values = {...layers[i].theatreObject.value, ...sentMidiValues[i]};
sentValues[i] = mixObject(sentValues[i], values, config.midi.smoothingMix);
let p = layers[i].values2cppProps(values);
if (p !== false) {
Module.setProps(p, layers[i].id());
}
} else {
const artboardValues = {...getArtboard().theatreObject.value, ...sentMidiValues[i]}
sentValues[i] = mixObject(sentValues[i], artboardValues, config.midi.smoothingMix);
let cppProps = getArtboard().values2cppProps(artboardValues);
Module.setArtboardProps(cppProps);
}
}
if (!currentlyTouching) {
for (let i = 0; i < currentValues.length; i++) {
currentValues[i] = {};
}
}
if (window.ofRA) {
requestAnimationFrame(ofUpdater);
} else {
setTimeout(() => {
ofUpdater();
}, window.ofUpdateMS);
}
}
const init = () => {
tryGetLayersP().then(() => {
layers = getLayers();
//console.log('what... this is layers' , layers);
const promises = [];
layers.forEach((layer) => {
promises.push(layer.updateFonts());
});
if (tp.sheet.project.address.projectId === 'rudi-midi') {
mapping["Launch Control MIDI 1"].props[0][184][23] = ['fontSize_px', [-128, 128]];
}
if (tp.sheet.project.address.projectId === 'sam-midi') {
mapping["Launch Control MIDI 1"].props[0][184][23] = ['fontSize_px', [-256, 256]];
}
Promise.all(promises).then(() => {
layers.forEach((layer, layerI) => {
const letterDelayProps = [
{
sequenced: true,
prop: ['color'],
},
{
sequenced: true,
prop: ['letterSpacing']
},
{
sequenced: true,
prop: ['fontSize_px']
},
];
if (layer.props.hasOwnProperty('fontVariationAxes')) {
const keys = Object.keys(layer.props.fontVariationAxes.props);
keys.forEach((key) => {
const detail = {
sequenced: true,
prop: ['fontVariationAxes', key],
};
letterDelayProps.push(detail);
});
}
letterDelayProps.forEach((detail, i) => {
// only update theatre for the last one
const updateTheatre = i === letterDelayProps.length - 1;
layer.handleSequenceEvent(detail, updateTheatre)
.then((updatedTheatre) => {
if (updatedTheatre && layerI === layers.length - 1) {
populateValueBuffer(layers);
ofUpdater();
isInitialized = true;
}
});
});
});
});
//selectLayers().then(() => {
//});
});
registerEvents();
navigator.requestMIDIAccess()
.then((access) => {
// Get lists of available MIDI controllers
inputs = access.inputs;
outputs = access.outputs;
const inputText = [];
const outputText = [];
inputs.forEach((midiInput) => {
inputText.push(`FOUND: ${midiInput.name}\n`);
updatingMidiValues[midiInput.name] = {};
appliedMidiValues[midiInput.name] = {};
midiInput.onmidimessage = function(message) {
//window.debugCallTimes.push(performance.now());
if (midiInput.name === "Launch Control MIDI 1") {
const isGeneral =
mapping[midiInput.name]
.general.hasOwnProperty(message.data[0]) &&
mapping[midiInput.name]
.general[message.data[0]].hasOwnProperty(message.data[1]);
const isProp =
mapping[midiInput.name]
.props[activePropSet].hasOwnProperty(message.data[0]) &&
mapping[midiInput.name]
.props[activePropSet][message.data[0]].hasOwnProperty(message.data[1]);
if (isInitialized && (isGeneral || isProp)) {
last_touchtime_s = performance.now() / 1000.0;
updatingMidiValues[midiInput.name][message.data[1]] = [message.data[0], message.data[2]];
//directlyApplyMidiValues(midiInput.name, message.data[1], message.data[0], message.data[2]);
}
autoSwitchPerhaps();
}
if (debugLog) {
element.querySelector(".midiMessages").innerText += `# ${midiInput.name}
${new Date()}
==================================
- Status: ${message.data[0]}
- Data 1: ${message.data[1]}
- Data 2: ${message.data[2]}
==================================\n\n`;
}
};
});
outputs.forEach((midiOutput) => {
outputText.push(`FOUND: ${midiOutput.name}\n`);
});
element.querySelector(".inputs").innerText = inputText.join('');
element.querySelector(".outputs").innerText = outputText.join('');
applyMidiValuesInterval();
// lalalaload another project
//autoSwitchPerhaps();
});
};
let autoSwitchTimeout = false;
const autoSwitchPerhaps = () => {
clearTimeout(autoSwitchTimeout);
autoSwitchTimeout = setTimeout(() => {
setProject(114, 127);
}, 5 * 60 * 1000);
};
const setValue = (layer, layerIndex, prop, value, minMax) => {
let v;
let propName = prop;
if (minMax.length > 2) {
const index = Math.floor((value / 128.0) * minMax.length);
v = minMax[index];
} else {
const min = minMax[0];
const max = minMax[1];
v = (value / 127.0) * (max - min) + min;
if (propName.indexOf('color') === 0) {
propName = propName.split('.')[1];
let color;
if (currentValues[layerIndex].hasOwnProperty('color')) {
color = {...layer.theatreObject.value.color, ...currentValues[layerIndex].color};
} else {
color = layer.theatreObject.value.color;
}
color[propName] = v;
propName = 'color';
v = color;
}
if (propName.indexOf('backgroundColor') === 0) {
propName = propName.split('.')[1];
let backgroundColor;
if (currentValues[layerIndex].hasOwnProperty('backgroundColor')) {
backgroundColor = {...layer.theatreObject.value.backgroundColor, ...currentValues[layerIndex].backgroundColor};
} else {
backgroundColor = layer.theatreObject.value.backgroundColor;
}
backgroundColor[propName] = v;
propName = 'backgroundColor';
v = backgroundColor;
}
}
currentValues[layerIndex][propName] = v;
//tp.studio.transaction(({
//set
//}) => {
//set(layer.theatreObject.props[propName], v);
//});
};
this.init = init;
this.getInputs = () => {
return inputs;
}
this.getOutputs = () => {
return outputs;
}
};
export {
MidiController
};