Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/midi map #145

Merged
merged 25 commits into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
de4e341
add parameter waiting for midi mapping entry
x37v Jun 21, 2024
d463d95
add waitingForMidiMapping to instance and parameter control
x37v Jun 21, 2024
e60acf6
add boolean for isMidiMapped
x37v Jun 21, 2024
5036a30
add actions for midi mapping and fix messages update
x37v Jun 21, 2024
7c74c25
plumb in midi/last updates via OSC
x37v Jun 21, 2024
c6a8e2c
setup midi mapping in param tab
x37v Jun 21, 2024
077f395
lint fix for param
x37v Jun 21, 2024
bd4ea9b
add action to clear param midi mapping
x37v Jun 21, 2024
b0c22df
add indication for midi mapping and action to clear it
x37v Jun 21, 2024
9544125
clear waiting before sending midi map
x37v Jun 21, 2024
11bf7ff
fixed Wat type-o
x37v Jun 22, 2024
0819e79
setInstanceWaitingForMidiMappingOnRemote
x37v Jun 22, 2024
b769686
better error message for report (midi map)
x37v Jun 22, 2024
860f58c
parsed meta method
x37v Jun 22, 2024
864c221
consolidate param iteration and use getParsedMetaObject
x37v Jun 22, 2024
58ba515
only send meta when we actually clear midi
x37v Jun 22, 2024
f9289a3
improved MIDI mapping UI and colorscheme to resemble pieces of Max be…
fde31 Jun 23, 2024
59b3a85
instance display: moved tab panels up in component tree to ensure the…
fde31 Jun 23, 2024
8fc12d0
allow disabling MIDI mapping mode by hitting Escape key
fde31 Jun 23, 2024
77c74f1
disable MIDI mapping when navigating away from params tab or changing…
fde31 Jun 23, 2024
58e89d7
fixed parameter overflow layout
fde31 Jun 23, 2024
64ee5f9
fixed unused imports
fde31 Jun 23, 2024
da9bb46
fixed TS error as typeof [] === "object" = true
fde31 Jun 23, 2024
0e8daf9
fix lint
x37v Jun 23, 2024
086863a
parameter focus doesn't need sending params
x37v Jun 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 109 additions & 3 deletions src/actions/instances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Router from "next/router";
import { ActionBase, AppThunk } from "../lib/store";
import { OSCQueryRNBOInstance, OSCQueryRNBOInstancePresetEntries, OSCValue } from "../lib/types";
import { InstanceStateRecord } from "../models/instance";
import { getInstanceByIndex } from "../selectors/instances";
import { getInstanceByIndex, getInstance } from "../selectors/instances";
import { getAppSetting } from "../selectors/settings";
import { ParameterRecord } from "../models/parameter";
import { MessagePortRecord } from "../models/messageport";
Expand Down Expand Up @@ -249,7 +249,6 @@ export const setInstanceParameterValueNormalizedOnRemote = throttle((instance: I
};

oscQueryBridge.sendPacket(writePacket(message));

// optimistic local state update
dispatch(setInstance(instance.setParameterNormalizedValue(param.id, value)));
}, 100);
Expand Down Expand Up @@ -291,6 +290,34 @@ export const restoreDefaultParameterMetaOnRemote = (_instance: InstanceStateReco
oscQueryBridge.sendPacket(writePacket(message));
};

export const activateParameterMIDIMappingFocus = (instance: InstanceStateRecord, param: ParameterRecord): AppThunk =>
(dispatch) => {
dispatch(setInstance(instance.setParameterWaitingForMidiMapping(param.id)));
};

export const clearParameterMidiMappingOnRemote = (id: InstanceStateRecord["id"], paramId: ParameterRecord["id"]): AppThunk =>
(_dispatch, getState) => {
const state = getState();
const instance = getInstance(state, id);
if (!instance) return;

const param = instance.parameters.get(paramId);
if (!param) return;

const meta = param.getParsedMeta();
if (typeof meta === "object" && !Array.isArray(meta)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixes the TS build error as typeof [] is also an array. I guess the issue here is that AnyJSON means it could be any kind of value. I wonder if really what we want to ensure is that meta is a Record<string, AnyJSON> throughout and if not fall back to {}. Maybe even the Meta Editor should not allow any non JsonMap input as currently one could also provide [] which is valid JSON

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have meta that we support but a user might have other ideas for it.. if they use midi mapping, osc mapping, they need to use objects but if they're simply adding meta that they use in their own way, i figure we shouldn't limit them?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, sure but the [param] object doesn't support it either - in fact doing anything than a Record<string, AnyJson> throws. Additionally setting it to eg [] means that functionality like MIDI mapping won't work so I personally am in favor of enforcing that it needs to be a JSON map at the top level.

delete meta.midi;
const message = {
address: `${param.path}/meta`,
args: [
{ type: "s", value: JSON.stringify(meta) }
]
};

oscQueryBridge.sendPacket(writePacket(message));
}
};

export const setInstanceMessagePortMetaOnRemote = (_instance: InstanceStateRecord, port: MessagePortRecord, value: string): AppThunk =>
() => {
const message = {
Expand Down Expand Up @@ -362,7 +389,7 @@ export const updateInstanceMessages = (index: number, desc: OSCQueryRNBOInstance

const state = getState();
const instance = getInstanceByIndex(state, index);
if (instance) return;
if (!instance) return;

dispatch(setInstance(
instance
Expand Down Expand Up @@ -457,6 +484,85 @@ export const updateInstanceParameterValueNormalized = (index: number, id: Parame
}
};

export const setInstanceWaitingForMidiMappingOnRemote = (id: InstanceStateRecord["id"], value: boolean): AppThunk =>
(dispatch, getState) => {
try {
const state = getState();
const instance = getInstance(state, id);
if (!instance) return;

dispatch(setInstance(instance.setWaitingForMapping(value)));

try {
const message = {
address: `${instance.path}/midi/last/report`,
args: [
{ type: value ? "T" : "F", value: value ? "true" : "false" }
]
};
oscQueryBridge.sendPacket(writePacket(message));
} catch (err) {
dispatch(showNotification({
level: NotificationLevel.error,
title: "Error while trying set midi mapping mode on remote",
message: "Please check the console for further details."
}));
console.log(err);
}
} catch (e) {
console.log(e);
}
};

export const updateInstanceMIDIReport = (index: number, value: boolean): AppThunk =>
(dispatch, getState) => {
try {
const state = getState();
const instance = getInstanceByIndex(state, index);
if (!instance) return;
dispatch(setInstance(instance.setWaitingForMapping(value)));
} catch (e) {
console.log(e);
}
};

export const updateInstanceMIDILastValue = (index: number, value: string): AppThunk =>
(dispatch, getState) => {
try {

const state = getState();

let instance = getInstanceByIndex(state, index);
if (!instance?.waitingForMidiMapping) return;

const midiMeta = JSON.parse(value);

// find waiting, update their meta, set them no longer waiting and update map
instance = instance.set("parameters", instance.parameters.map(param => {
if (param.waitingForMidiMapping) {
fde31 marked this conversation as resolved.
Show resolved Hide resolved
const meta = param.getParsedMetaObject();
meta.midi = midiMeta;

const message = {
address: `${param.path}/meta`,
args: [
{ type: "s", value: JSON.stringify(meta) }
]
};

oscQueryBridge.sendPacket(writePacket(message));
return param.setWaitingForMidiMapping(false);
}
return param;
}));

dispatch(setInstance(instance));

} catch (e) {
console.log(e);
}
};

export const updateInstanceParameterMeta = (index: number, id: ParameterRecord["id"], value: string): AppThunk =>
(dispatch, getState) => {
try {
Expand Down
7 changes: 3 additions & 4 deletions src/components/instance/datarefTab.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Tabs, Text } from "@mantine/core";
import { Stack, Text } from "@mantine/core";
import { FunctionComponent, memo, useCallback } from "react";
import { InstanceTab } from "../../lib/constants";
import { useAppDispatch } from "../../hooks/useAppDispatch";
import DataRefList from "../dataref/list";
import classes from "./instance.module.css";
Expand Down Expand Up @@ -43,15 +42,15 @@ const InstanceDataRefsTab: FunctionComponent<InstanceDataRefTabProps> = memo(fun
}, [dispatch, instance]);

return (
<Tabs.Panel value={ InstanceTab.DataRefs } >
<Stack>
{
!instance.datarefs.size ? (
<div className={ classes.emptySection }>
This patcher instance has no buffers.
</div>
) : <DataRefList datarefs={ instance.datarefs } options={ datafiles } onSetDataRef={ onSetDataRef } onClearDataRef={ onClearDataRef } />
}
</Tabs.Panel>
</Stack>
);
});

Expand Down
16 changes: 12 additions & 4 deletions src/components/instance/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,18 @@ const Instance: FunctionComponent<InstanceProps> = memo(function WrappedInstance
}
</Tabs.List>
<div className={ classes.instanceTabContentWrap } >
<InstanceParameterTab instance={ instance } sortAttr={ paramSortAttr } sortOrder={ paramSortOrder } />
<InstanceMessagesTab instance={ instance } outputEnabled={ enabledMessageOuput.value as boolean } />
<InstanceDataRefsTab instance={ instance } datafiles={ datafiles } />
<InstanceMIDITab instance={ instance } keyboardEnabled={ enabledMIDIKeyboard.value as boolean } />
<Tabs.Panel value={ InstanceTab.Parameters } >
<InstanceParameterTab instance={ instance } sortAttr={ paramSortAttr } sortOrder={ paramSortOrder } />
</Tabs.Panel>
<Tabs.Panel value={ InstanceTab.MessagePorts } >
<InstanceMessagesTab instance={ instance } outputEnabled={ enabledMessageOuput.value as boolean } />
</Tabs.Panel>
<Tabs.Panel value={ InstanceTab.DataRefs } >
<InstanceDataRefsTab instance={ instance } datafiles={ datafiles } />
</Tabs.Panel>
<Tabs.Panel value={ InstanceTab.MIDI } >
<InstanceMIDITab instance={ instance } keyboardEnabled={ enabledMIDIKeyboard.value as boolean } />
</Tabs.Panel>
</div>
</Tabs>
);
Expand Down
7 changes: 3 additions & 4 deletions src/components/instance/messageTab.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Tabs } from "@mantine/core";
import { Stack } from "@mantine/core";
import { FunctionComponent, memo, useCallback } from "react";
import { InstanceTab } from "../../lib/constants";
import { useAppDispatch } from "../../hooks/useAppDispatch";
import MessageInportList from "../messages/inportList";
import { SectionTitle } from "../page/sectionTitle";
Expand Down Expand Up @@ -35,7 +34,7 @@ const InstanceMessagesTab: FunctionComponent<InstanceMessageTabProps> = memo(fun
}, [dispatch, instance]);

return (
<Tabs.Panel value={ InstanceTab.MessagePorts } >
<Stack>
<SectionTitle>Input Ports</SectionTitle>
{
!instance.messageInports.size ? (
Expand Down Expand Up @@ -64,7 +63,7 @@ const InstanceMessagesTab: FunctionComponent<InstanceMessageTabProps> = memo(fun
onSaveMetadata={ onSavePortMetadata }
/>
}
</Tabs.Panel>
</Stack>
);
});

Expand Down
6 changes: 1 addition & 5 deletions src/components/instance/midiTab.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { Tabs } from "@mantine/core";
import { FunctionComponent, memo, useCallback } from "react";
import { InstanceTab } from "../../lib/constants";
import { useAppDispatch } from "../../hooks/useAppDispatch";
import KeyRoll from "../keyroll";
import { InstanceStateRecord } from "../../models/instance";
Expand All @@ -27,9 +25,7 @@ const InstanceMIDITab: FunctionComponent<InstanceMIDITabProps> = memo(function W
}, [dispatch, instance]);

return (
<Tabs.Panel value={ InstanceTab.MIDI } >
<KeyRoll onTriggerNoteOn={ triggerMIDINoteOn } onTriggerNoteOff={ triggerMIDINoteOff } keyboardEnabled={ keyboardEnabled } />
</Tabs.Panel>
<KeyRoll onTriggerNoteOn={ triggerMIDINoteOn } onTriggerNoteOff={ triggerMIDINoteOff } keyboardEnabled={ keyboardEnabled } />
);
});

Expand Down
87 changes: 67 additions & 20 deletions src/components/instance/paramTab.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { ActionIcon, Button, Group, Popover, SegmentedControl, Select, Stack, Tabs, Text, TextInput } from "@mantine/core";
import { ChangeEvent, FC, FunctionComponent, KeyboardEvent, memo, useCallback, useEffect, useRef, useState } from "react";
import { InstanceTab, ParameterSortAttr, SortOrder } from "../../lib/constants";
import { ActionIcon, Button, Group, Popover, SegmentedControl, Select, Stack, Switch, Text, TextInput } from "@mantine/core";
import { ChangeEvent, FC, FunctionComponent, KeyboardEvent as ReactKeyboardEvent, memo, useCallback, useEffect, useRef, useState } from "react";
import { ParameterSortAttr, SortOrder } from "../../lib/constants";
import ParameterList from "../parameter/list";
import { ParameterRecord } from "../../models/parameter";
import classes from "./instance.module.css";
import { useAppDispatch } from "../../hooks/useAppDispatch";
import { InstanceStateRecord } from "../../models/instance";
import { restoreDefaultParameterMetaOnRemote, setInstanceParameterMetaOnRemote, setInstanceParameterValueNormalizedOnRemote } from "../../actions/instances";
import {
restoreDefaultParameterMetaOnRemote, setInstanceParameterMetaOnRemote,
setInstanceParameterValueNormalizedOnRemote,
setInstanceWaitingForMidiMappingOnRemote, clearParameterMidiMappingOnRemote,
activateParameterMIDIMappingFocus
} from "../../actions/instances";
import { OrderedSet as ImmuOrderedSet } from "immutable";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowDownAZ, faArrowUpAZ, faSearch, faSort, faXmark } from "@fortawesome/free-solid-svg-icons";
Expand Down Expand Up @@ -39,7 +44,7 @@ const ParameterSearchInput: FC<ParameterSearchInputProps> = memo(function Wrappe
searchInputRef.current?.focus();
}, [setSearchValue]);

const onKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>) => {
const onKeyDown = useCallback((e: ReactKeyboardEvent<HTMLInputElement>) => {
if (e.key === "Escape") {
if (searchValue.length) {
setSearchValue("");
Expand Down Expand Up @@ -142,6 +147,20 @@ const InstanceParameterTab: FunctionComponent<InstanceParameterTabProps> = memo(
dispatch(restoreDefaultParameterMetaOnRemote(instance, param));
}, [dispatch, instance]);

const onToggleMIDIMapping = useCallback((e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
e.currentTarget.blur();
dispatch(setInstanceWaitingForMidiMappingOnRemote(instance.id, e.currentTarget.checked));
}, [dispatch, instance]);

const onActivateParameterMIDIMapping = useCallback((param: ParameterRecord) => {
dispatch(activateParameterMIDIMappingFocus(instance, param));
}, [dispatch, instance]);

const onClearParameterMidiMapping = useCallback((param: ParameterRecord) => {
dispatch(clearParameterMidiMappingOnRemote(instance.id, param.id));
}, [dispatch, instance]);

const onSearch = useDebouncedCallback((query: string) => {
setSearchValue(query);
}, 150);
Expand All @@ -150,12 +169,32 @@ const InstanceParameterTab: FunctionComponent<InstanceParameterTabProps> = memo(
setSortedParamIds(getSortedParameterIds(instance.parameters, sortAttr.value as ParameterSortAttr, sortOrder.value as SortOrder));
}, [instance, sortAttr, sortOrder]);

useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.code === "Escape" && instance.waitingForMidiMapping && document.activeElement instanceof HTMLElement && document.activeElement.nodeName !== "INPUT") {
dispatch(setInstanceWaitingForMidiMappingOnRemote(instance.id, false));
}
};
document.addEventListener("keydown", onKeyDown);

return () => {
document.removeEventListener("keydown", onKeyDown);
};
}, [instance, dispatch]);

useEffect(() => {
return () => {
dispatch(setInstanceWaitingForMidiMappingOnRemote(instance.id, false));
};
}, [instance.id, dispatch]);

let parameters = sortedParamIds.map(id => instance.parameters.get(id)).filter(p => !!p);
if (searchValue?.length) parameters = parameters.filter(p => p.matchesQuery(searchValue));

return (
<Tabs.Panel value={ InstanceTab.Parameters } >
<Stack gap="md" h="100%">
<Stack gap="md" h="100%">
<Group justify="space-between">
<Switch size="xs" variant="default" color="violet.4" label="MIDI Map" checked={ instance.waitingForMidiMapping } onChange={ onToggleMIDIMapping } />
<Group justify="flex-end" gap="xs">
<ParameterSearchInput onSearch={ onSearch } />
<Popover position="bottom-end" withArrow>
Expand Down Expand Up @@ -188,19 +227,27 @@ const InstanceParameterTab: FunctionComponent<InstanceParameterTabProps> = memo(
</Popover.Dropdown>
</Popover>
</Group>
{
!instance.parameters.size ? (
<div className={ classes.emptySection }>
This patcher instance has no parameters
</div>
) : (
<div className={ classes.paramSectionWrap } >
<ParameterList parameters={ parameters } onSetNormalizedValue={ onSetNormalizedParamValue } onSaveMetadata={ onSaveParameterMetadata } onRestoreMetadata={ onRestoreDefaultParameterMetadata } />
</div>
)
}
</Stack>
</Tabs.Panel>
</Group>
{
!instance.parameters.size ? (
<div className={ classes.emptySection }>
This patcher instance has no parameters
</div>
) : (
<div className={ classes.paramSectionWrap } >
<ParameterList
parameters={ parameters }
isMIDIMapping={ instance.waitingForMidiMapping }
onActivateMIDIMapping={ onActivateParameterMIDIMapping }
onSetNormalizedValue={ onSetNormalizedParamValue }
onSaveMetadata={ onSaveParameterMetadata }
onRestoreMetadata={ onRestoreDefaultParameterMetadata }
onClearMidiMapping={ onClearParameterMidiMapping }
/>
</div>
)
}
</Stack>
);
});

Expand Down
Loading
Loading