Skip to content

Commit

Permalink
feature (partial): renderer displays backlight color for pad inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
aolsenjazz committed Oct 13, 2024
1 parent f819b36 commit 027b84e
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 105 deletions.
2 changes: 1 addition & 1 deletion src/main/ipc/initialize-input-config-ipc.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { BrowserWindow, ipcMain, IpcMainEvent, Menu } from 'electron';

import { MonoInputConfig } from '@shared/hardware-config';
import {
BaseInputConfig,
InputDTO,
Expand All @@ -9,6 +8,7 @@ import { getQualifiedInputId } from '@shared/util';
import { PluginManifest } from '@shared/plugin-core/plugin-manifest';
import { BaseInputPlugin } from '@shared/plugin-core/base-input-plugin';
import { importInputSubcomponent } from '@plugins/plugin-loader';
import { MonoInputConfig } from '@shared/hardware-config/input-config/mono-input-config';

import { INPUT_CONFIG } from './ipc-channels';
import { WindowProvider } from '../window-provider';
Expand Down
36 changes: 35 additions & 1 deletion src/main/port-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { idForMsg } from '@shared/midi-util';
import { DeviceRegistry } from '@main/device-registry';
import { PluginRegistry } from '@main/plugin-registry';
import { InputRegistry } from '@main/input-registry';
import { MessageTransport } from '@shared/message-transport';

import { PortScanResult, PortManager } from './port-manager';
import { PortPair } from './port-pair';
Expand Down Expand Up @@ -192,6 +193,34 @@ export class HardwarePortServiceSingleton {
this.initDevice(pair);
}

/**
* Loopback messages should be sent both to the connected loopback MIDI port
* as well as to the renderer so that the device rendering can accurately reflect
* remote device state
*/
private createRendererInclusiveLoopbackTransport(
deviceId: string,
inputId: string,
loopbackTransport: MessageTransport
): MessageTransport {
return {
send(msg: NumberArrayWithStatus) {
loopbackTransport.send(msg);

MainWindow.sendReduxEvent({
type: 'recentLoopbackMessages/addMessage',
payload: {
deviceId,
inputId,
message: msg,
},
});
},

applyThrottle: loopbackTransport.applyThrottle,
};
}

private initDevice(pair: PortPair) {
const config = DeviceRegistry.get(pair.id);

Expand All @@ -206,9 +235,14 @@ export class HardwarePortServiceSingleton {
const inputId = idForMsg(msg, false);
const remoteTransport = this.ports.get(config.id)!;
const loopbackTransport = VirtualPortService.ports.get(config.id)!;
const inclusiveLoopback = this.createRendererInclusiveLoopbackTransport(
config.id,
inputId,
loopbackTransport
);

const message = config.process(msg, {
loopbackTransport,
loopbackTransport: inclusiveLoopback,
remoteTransport,
loopbackTransports: VirtualPortService.ports,
remoteTransports: this.ports,
Expand Down
11 changes: 7 additions & 4 deletions src/plugins/input-plugins/backlight-control/gui/gui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,13 @@ export default function BacklightPluginUI(
fxBindings: { ...fxBindings },
fxValueBindings: { ...fxValueBindings },
};
newState.fxBindings[s] = availableFx.find((f) => f.isDefault)!;
newState.fxValueBindings[s] = availableFx.find(
(f) => f.isDefault
)!.defaultVal;

if (availableFx.length > 0) {
newState.fxBindings[s] = availableFx.find((f) => f.isDefault)!;
newState.fxValueBindings[s] = availableFx.find(
(f) => f.isDefault
)!.defaultVal;
}

applyChanges(newState);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,81 +1,56 @@
import { useMemo } from 'react';
import { useEffect, useState } from 'react';

import { PadDriver } from '@shared/driver-types/input-drivers/pad-driver';
import { useAppSelector } from '@hooks/use-app-dispatch';
import { selectRecentLoopbackMessagesById } from '@features/recent-loopback-messages/recent-loopback-messages-slice';
import { msgEquals, subtractMidiArrays } from '@shared/util';
import { testLoopback } from '@features/recent-loopback-messages/recent-loopback-messages-slice';
import { msgEquals, sumMidiArrays } from '@shared/util';
import { Color } from '@shared/driver-types/color';
import { FxDriver } from '@shared/driver-types/fx-driver';

type PropTypes = {
driver: PadDriver;
id: string;
};

function removeNegatives(arr: NumberArrayWithStatus) {
for (let i = 0; i < arr.length; i++) {
arr[i] = arr[i] < 0 ? 0 : arr[i];
}
return arr;
}

export default function Pad(props: PropTypes) {
const { driver, id } = props;

const lastMsgArr = useAppSelector(selectRecentLoopbackMessagesById(id, 1));

const color = useMemo(() => {
if (lastMsgArr.length !== 1) return undefined;
const msg = lastMsgArr[0];

// Map available colors to their arrays, which we will use to determine the base
// color mapping of a message prior to affecting with FX arrays
const unaffectedColorCandidates = driver.availableColors.map(
(c) => c.array
);

// sum(sourceMsgParts) - sum(unaffectedColorCandidateArray). The smallest positive
// difference between these arrays indicates that the correlating array
// represents the relevant color, sans fx
const diffsFromMsg = unaffectedColorCandidates
.map((arr) => subtractMidiArrays(msg, arr))
// .map((arr) => removeNegatives(arr))
.map((diffArr) => diffArr.reduce((a, b) => a + b));

// find the smallest positive difference between arrays, and return the index
const relevantIdx = diffsFromMsg.reduce(
(lowestIndex, num, index, array) =>
num < array[lowestIndex] && num >= 0 ? index : lowestIndex,
0
);

if (driver.number === 32) {
console.log(msg, unaffectedColorCandidates, diffsFromMsg);
// console.log(driver.availableColors[relevantIdx]);
}

return driver.availableColors[relevantIdx];
}, [lastMsgArr, driver.availableColors, driver.number]);

const fx = useMemo(() => {
if (lastMsgArr.length !== 1 || !color) return undefined;
const msg = lastMsgArr[0];

const currentFxVal = subtractMidiArrays(msg, color.array);
return driver.availableFx.find((f) => {
for (let i = 0; i < f.validVals.length; i++) {
if (msgEquals(currentFxVal, f.validVals[i])) return true;
const lastMsgArr = useAppSelector((state) => testLoopback(state, id));

const [color, setColor] = useState<Color>();
const [fx, setFx] = useState<FxDriver>();

useEffect(() => {
if (!lastMsgArr || lastMsgArr.length === 0) return;
const lastMsg = lastMsgArr.at(-1)!;

driver.availableColors.forEach((c) => {
// try to apply fx if they exist
driver.availableFx.forEach((fxDriver) => {
fxDriver.validVals.forEach((fxArr) => {
const affectedColor = sumMidiArrays(c.array, fxArr);
if (msgEquals(affectedColor, lastMsg)) {
setColor(c);
setFx(fxDriver);
}
});
});

// set color independent of fx, if necessary
if (msgEquals(c.array, lastMsg)) {
setColor(c);
}
return false;
});
}, [lastMsgArr, color, driver.availableFx]);
}, [lastMsgArr, driver]);

return (
<div
className="pad interactive-indicator"
style={{
animationName: color?.modifier,
animationName: color?.modifier || fx?.title,
backgroundColor: color?.string,
animationTimingFunction:
color?.modifier === 'pulse' ? 'ease-in-out' : undefined,
fx?.title === 'Pulse' ? 'ease-in-out' : undefined,
borderRadius: driver.shape === 'circle' ? '100%' : 0,
}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default function InputLayout(props: InputLayoutPropTypes) {
>
<InteractiveInputLayout
qualifiedInputId={qualifiedInputId}
driver={driver}
driver={driver as InteractiveInputDriver}
/>
</div>
);
Expand Down
29 changes: 4 additions & 25 deletions src/renderer/components/DevicePanel/DeviceLayoutWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { useCallback } from 'react';
import { useSelector } from 'react-redux';

import { useAppDispatch } from '@hooks/use-app-dispatch';
import { DeviceDriver } from '@shared/driver-types/device-driver';
import {
selectSelectedInputs,
setSelectedInputs,
} from '@features/selected-inputs/selected-inputs-slice';
import { setSelectedInputs } from '@features/selected-inputs/selected-inputs-slice';

import DeviceLayout from './DeviceLayout';
import WarningIcon from '../WarningIcon';
Expand All @@ -24,30 +20,13 @@ export default function DeviceLayoutWrapper(
const { driver } = props;

const dispatch = useAppDispatch();
const selectedInputs = useSelector(selectSelectedInputs);

// on input click (or ctrl+click) update selectedInputs
const onInputSelect = useCallback(
(event: React.MouseEvent, ids: string[]) => {
let next: string[] = [];

if (event.ctrlKey || event.metaKey) {
ids.forEach((id) => {
const idx = selectedInputs.indexOf(id);
const spliced = [...selectedInputs];
spliced.splice(idx, 1);
next = selectedInputs.includes(id)
? spliced
: selectedInputs.concat([id]);
});
} else {
next =
JSON.stringify(selectedInputs) === JSON.stringify(ids) ? [] : ids;
}

dispatch(setSelectedInputs(next));
(_event: React.MouseEvent, ids: string[]) => {
dispatch(setSelectedInputs(ids));
},
[dispatch, selectedInputs]
[dispatch]
);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createSelector, PayloadAction } from '@reduxjs/toolkit';
import { PayloadAction } from '@reduxjs/toolkit';
import { getQualifiedInputId } from '@shared/util';

import type { RootState } from '../../store/store';
Expand All @@ -12,6 +12,8 @@ type AddMessagePayload = {

const initialState: Record<string, NumberArrayWithStatus[]> = {};

// const initialState: Record<string, NumberArrayWithStatus[]> = {};

export const recentLoopbackMessagesSlice = createAppSlice({
name: 'recentLoopbackMessages',

Expand Down Expand Up @@ -39,16 +41,5 @@ export const recentLoopbackMessagesSlice = createAppSlice({
}),
});

export const selectRecentLoopbackMessagesById = (
id: string,
numMessages: number
) =>
createSelector(
(state: RootState) => state.recentLoopbackMessages[id],
(messages = []) => {
if (numMessages >= messages.length) {
return messages;
}
return messages.slice(-numMessages);
}
);
export const testLoopback = (state: RootState, id: string) =>
state.recentLoopbackMessages[id];
15 changes: 13 additions & 2 deletions src/renderer/store/ipc-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,23 @@ import { UnknownAction } from 'redux';
const { ReduxService } = window;

/**
* Listens for actions coming from the backend process, and dispatches them to store
* Listens for actions coming from the backend process, and dispatches them to store.
*
* For whatever reason, React doesn't handle 50+ messages coming from IPC in quick succession
* (1-2ms), and console output will complain that Maximum Update Depth has been exceeded.
* Batching the actions received from IPC for whatever reason fixes this.
*/
export const ipcMiddleware: Middleware = (store) => {
let actions: UnknownAction[] = [];

ReduxService.onReduxEvent((action: UnknownAction) => {
store.dispatch(action);
actions.push(action);
});

setInterval(() => {
actions.forEach((a) => store.dispatch(a));
actions = [];
}, 50);

return (next) => (action) => next(action);
};

0 comments on commit 027b84e

Please sign in to comment.