'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 = `
I'm sorry, but your browser does not support the WebMIDI API ☹️🚫🎹
`; } 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 };