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

Add Global MIDI Mapping View #191

Merged
merged 17 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
37 changes: 12 additions & 25 deletions src/actions/patchers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Router from "next/router";
import { ActionBase, AppThunk } from "../lib/store";
import { OSCQueryRNBOInstance, OSCQueryRNBOInstancePresetEntries, OSCQueryRNBOPatchersState, OSCValue, ParameterMetaJsonMap } from "../lib/types";
import { MIDIMetaMapping, OSCQueryRNBOInstance, OSCQueryRNBOInstancePresetEntries, OSCQueryRNBOPatchersState, OSCValue, ParameterMetaJsonMap } from "../lib/types";
import { PatcherInstanceRecord } from "../models/instance";
import { getPatcherInstanceByIndex, getPatcherInstance, getPatcherInstanceParametersByInstanceIndex, getPatcherInstanceParameter, getPatcherInstanceMessageInportsByInstanceIndex, getPatcherInstanceMesssageOutportsByInstanceIndex, getPatcherInstanceMessageInportByPath, getPatcherInstanceMessageOutportByPath, getPatcherInstanceMesssageOutportsByInstanceIndexAndTag, getPatcherInstanceParameterByPath, getPatcherInstanceParametersByInstanceIndexAndName, getPatcherInstanceMessageInportsByInstanceIndexAndTag } from "../selectors/patchers";
import { getAppSetting } from "../selectors/settings";
Expand All @@ -16,7 +16,8 @@ import { AppSetting } from "../models/settings";
import { DataRefRecord } from "../models/dataref";
import { DataFileRecord } from "../models/datafile";
import { PatcherExportRecord } from "../models/patcher";
import { cloneJSON } from "../lib/util";
import { cloneJSON, parseMIDIMappingDisplayValue } from "../lib/util";
import { MIDIMetaMappingType } from "../lib/constants";

export enum PatcherActionType {
INIT_PATCHERS = "INIT_PATCHERS",
Expand Down Expand Up @@ -575,7 +576,7 @@ export const clearParameterMIDIMappingOnRemote = (id: PatcherInstanceRecord["id"
oscQueryBridge.sendPacket(writePacket(message));
};

export const setParameterMIDIChannelOnRemote = (id: PatcherInstanceRecord["id"], paramId: ParameterRecord["id"], channel: number): AppThunk =>
export const setParameterMIDIMappingOnRemote = (id: PatcherInstanceRecord["id"], paramId: ParameterRecord["id"], type: MIDIMetaMappingType, mapping: MIDIMetaMapping): AppThunk =>
(_dispatch, getState) => {
const state = getState();
const instance = getPatcherInstance(state, id);
Expand All @@ -585,8 +586,7 @@ export const setParameterMIDIChannelOnRemote = (id: PatcherInstanceRecord["id"],
if (!param) return;

const meta: ParameterMetaJsonMap = cloneJSON(param.meta);
meta.midi = (meta.midi || {});
meta.midi.chan = channel;
meta.midi = { ...mapping };

const message = {
address: `${param.path}/meta`,
Expand All @@ -598,27 +598,14 @@ export const setParameterMIDIChannelOnRemote = (id: PatcherInstanceRecord["id"],
oscQueryBridge.sendPacket(writePacket(message));
};

export const setParameterMIDIControlOnRemote = (id: PatcherInstanceRecord["id"], paramId: ParameterRecord["id"], control: number): AppThunk =>
(_dispatch, getState) => {
const state = getState();
const instance = getPatcherInstance(state, id);
if (!instance) return;

const param = getPatcherInstanceParameter(state, paramId);
if (!param) return;

const meta: ParameterMetaJsonMap = cloneJSON(param.meta);
meta.midi = (meta.midi || {});
meta.midi.ctrl = control;

const message = {
address: `${param.path}/meta`,
args: [
{ type: "s", value: JSON.stringify(meta) }
]
};
export const setParameterMIDIMappingOnRemoteFromDisplayValue = (id: PatcherInstanceRecord["id"], paramId: ParameterRecord["id"], value: string): AppThunk =>
(dispatch) => {
const parsed = parseMIDIMappingDisplayValue(value);
if (!parsed) {
return void dispatch(showNotification({ title: "Invalid MIDI Mapping", message: `"${value}" is not a valid MIDI mapping format`, level: NotificationLevel.error }));
}

oscQueryBridge.sendPacket(writePacket(message));
dispatch(setParameterMIDIMappingOnRemote(id, paramId, parsed.type, parsed.mapping));
};

export const setInstanceMessagePortMetaOnRemote = (_instance: PatcherInstanceRecord, port: MessagePortRecord, value: string): AppThunk =>
Expand Down
98 changes: 94 additions & 4 deletions src/components/elements/editableTableCell.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import { NumberInput, Table } from "@mantine/core";
import { FC, memo, useCallback, useEffect, useState } from "react";
import { NumberInput, Table, TextInput } from "@mantine/core";
import { ChangeEvent, FC, KeyboardEvent, memo, useCallback, useEffect, useState } from "react";
import classes from "./elements.module.css";

export type EditableTableNumberCellProps = {
className?: string;
min: number;
max: number;
name: string;
onUpdate: (val: number) => void;
prefix?: string;
value: number;
};

export const EditableTableNumberCell: FC<EditableTableNumberCellProps> = memo(function WrappedEditableMIDIField({
export const EditableTableNumberCell: FC<EditableTableNumberCellProps> = memo(function WrappedEditableNumberField({
className = "",
min,
max,
name,
onUpdate,
prefix,
value
}) {
const [isEditing, setIsEditing] = useState<boolean>(false);
Expand All @@ -37,6 +40,19 @@ export const EditableTableNumberCell: FC<EditableTableNumberCellProps> = memo(fu
onUpdate(currentValue);
}, [setIsEditing, value, currentValue, onUpdate]);

const onKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>): void => {
if (e.key === "Escape") {
setIsEditing(false);
setCurrentValue(value);
return void e.preventDefault();
} else if (e.key === "Enter") {
setIsEditing(false);
if (currentValue === value) return;
onUpdate(currentValue);
return void e.preventDefault();
}
}, [setIsEditing, setCurrentValue, value, currentValue, onUpdate]);

useEffect(() => {
setCurrentValue(value);
}, [value, setCurrentValue]);
Expand All @@ -47,16 +63,90 @@ export const EditableTableNumberCell: FC<EditableTableNumberCellProps> = memo(fu
isEditing ? (
<NumberInput
autoFocus
className={ classes.editableTableCellInput }
variant="unstyled"
onBlur={ onBlur }
onChange={ onChange }
onKeyDown={ onKeyDown }
name={ name }
min={ min }
max={ max }
prefix={ prefix }
size="xs"
value={ currentValue }
/>
) : `${prefix || ""}${value}`
}

</Table.Td>
);
});

export type EditableTableTextCellProps = {
className?: string;
name: string;
onUpdate: (val: string) => void;
value: string;
};

export const EditableTableTextCell: FC<EditableTableTextCellProps> = memo(function WrappedEditableTextField({
className = "",
name,
onUpdate,
value
}) {
const [isEditing, setIsEditing] = useState<boolean>(false);
const [currentValue, setCurrentValue] = useState<string>(value);

const onTriggerEdit = useCallback(() => {
if (isEditing) return;
setIsEditing(true);
setCurrentValue(value);
}, [isEditing, setIsEditing, setCurrentValue, value]);

const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setCurrentValue(e.target.value);
}, [setCurrentValue]);

const onBlur = useCallback(() => {
setIsEditing(false);
if (currentValue === value) return;
onUpdate(currentValue);
}, [setIsEditing, value, currentValue, onUpdate]);

const onKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>): void => {
if (e.key === "Escape") {
setIsEditing(false);
setCurrentValue(value);
return void e.preventDefault();
} else if (e.key === "Enter") {
setIsEditing(false);
if (currentValue === value) return;
onUpdate(currentValue);
return void e.preventDefault();
}
}, [setIsEditing, setCurrentValue, value, currentValue, onUpdate]);

useEffect(() => {
setCurrentValue(value);
}, [value, setCurrentValue]);

return (
<Table.Td className={ className } onClick={ onTriggerEdit } py={ 0 } >
{
isEditing ? (
<TextInput
autoFocus
className={ classes.editableTableCellInput }
variant="unstyled"
onBlur={ onBlur }
onChange={ onChange }
onKeyDown={ onKeyDown }
name={ name }
size="xs"
value={ currentValue }
/>
) : value
) : value
}

</Table.Td>
Expand Down
4 changes: 4 additions & 0 deletions src/components/elements/elements.module.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.icon {
width: 1.3em;
}

.editableTableCellInput {
border-bottom: 1px solid var(--mantine-primary-color-filled);
}
61 changes: 38 additions & 23 deletions src/components/midi/mappedParameterItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,47 @@ import { modals } from "@mantine/modals";
import Link from "next/link";
import { useRouter } from "next/router";
import classes from "./midi.module.css";
import { formatParamValueForDisplay } from "../../lib/util";
import { EditableTableNumberCell } from "../elements/editableTableCell";
import { formatMIDIMappingToDisplay, formatParamValueForDisplay } from "../../lib/util";
import { EditableTableTextCell } from "../elements/editableTableCell";
import { MIDIMetaMappingType } from "../../lib/constants";
import { MIDIMetaMapping } from "../../lib/types";

export type MIDISourceProps = {
mappingType: MIDIMetaMappingType;
midiMapping: MIDIMetaMapping;
onUpdateMapping: (value: string) => void;
};


const MIDISource: FC<MIDISourceProps> = memo(function WrappedMIDISource({
mappingType,
midiMapping,
onUpdateMapping
}) {

return (
<EditableTableTextCell
className={ classes.midiSourceColumn }
name="midi_source"
onUpdate={ onUpdateMapping }
value={ formatMIDIMappingToDisplay(mappingType, midiMapping) }
/>
);
});

export type MIDIMappedParamProps = {
instance: PatcherInstanceRecord;
param: ParameterRecord;
onClearMIDIMapping: (instance: PatcherInstanceRecord, param: ParameterRecord) => void;
onUpdateMIDIChannel: (instance: PatcherInstanceRecord, param: ParameterRecord, channel: number) => void;
onUpdateMIDIControl: (instance: PatcherInstanceRecord, param: ParameterRecord, control: number) => void;
onUpdateMIDIMapping: (instance: PatcherInstanceRecord, param: ParameterRecord, value: string) => void;
};


const MIDIMappedParameter: FC<MIDIMappedParamProps> = memo(function WrappedMIDIMappedParam({
instance,
param,
onClearMIDIMapping,
onUpdateMIDIChannel,
onUpdateMIDIControl
onUpdateMIDIMapping
}) {

const { query: restQuery } = useRouter();
Expand All @@ -44,26 +68,17 @@ const MIDIMappedParameter: FC<MIDIMappedParamProps> = memo(function WrappedMIDIM
});
}, [param, instance, onClearMIDIMapping]);

const onUpdateChannel = useCallback((channel: number) => {
onUpdateMIDIChannel(instance, param, channel);
}, [onUpdateMIDIChannel, instance, param]);

const onUpdateControl = useCallback((control: number) => {
onUpdateMIDIControl(instance, param, control);
}, [onUpdateMIDIControl, instance, param]);
const onUpdateMapping = useCallback((value: string) => {
onUpdateMIDIMapping(instance, param, value);
}, [instance, param, onUpdateMIDIMapping]);

return (
<Table.Tr>
{
param.meta.midi?.chan === undefined
? <Table.Td className={ classes.midiChannelColumn } />
: <EditableTableNumberCell min={ 1 } max={ 16 } value={ param.meta.midi.chan } name="midi_channel" className={ classes.midiChannelColumn } onUpdate={ onUpdateChannel } />
}
{
param.meta.midi?.ctrl === undefined
? <Table.Td className={ classes.midiControlColumn } />
: <EditableTableNumberCell min={ 0 } max={ 127 } value={ param.meta.midi.ctrl } name="midi_control" className={ classes.midiControlColumn } onUpdate={ onUpdateControl } />
}
<MIDISource
mappingType={ param.midiMappingType as MIDIMetaMappingType }
midiMapping={ param.meta.midi as MIDIMetaMapping }
onUpdateMapping={ onUpdateMapping }
/>
<Table.Td className={ classes.parameterNameColumn } >{ param.name }</Table.Td>
<Table.Td className={ classes.patcherInstanceColumn } >
<span className={ classes.patcherInstanceIndex } >{ instance.index }</span>
Expand Down
27 changes: 7 additions & 20 deletions src/components/midi/mappedParameterList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ export type MIDIMappedParameterListProps = {
parameters: ImmuOrderedSet<ParameterRecord>;
patcherInstances: ImmuMap<PatcherInstanceRecord["index"], PatcherInstanceRecord>;
onClearParameterMIDIMapping: (instance: PatcherInstanceRecord, param: ParameterRecord) => void;
onUpdateParameterMIDIChannel: (instance: PatcherInstanceRecord, param: ParameterRecord, channel: number) => void;
onUpdateParameterMIDIControl: (instance: PatcherInstanceRecord, param: ParameterRecord, control: number) => void;
onUpdateParameterMIDIMapping: (instance: PatcherInstanceRecord, param: ParameterRecord, value: string) => void;
onSort: (sortAttr: MIDIMappedParameterSortAttr) => void;
sortAttr: MIDIMappedParameterSortAttr;
sortOrder: SortOrder;
Expand All @@ -23,8 +22,7 @@ const MIDIMappedParameterList: FC<MIDIMappedParameterListProps> = memo(function
patcherInstances,
parameters,
onClearParameterMIDIMapping,
onUpdateParameterMIDIChannel,
onUpdateParameterMIDIControl,
onUpdateParameterMIDIMapping,
onSort,
sortAttr,
sortOrder
Expand All @@ -35,24 +33,14 @@ const MIDIMappedParameterList: FC<MIDIMappedParameterListProps> = memo(function
<Table.Thead>
<Table.Tr>
<TableHeaderCell
className={ classes.midiChannelColumnHeader }
className={ classes.midiSourceColumnHeader }
fz="xs"
onSort={ onSort }
sortKey={ MIDIMappedParameterSortAttr.MIDIChannel }
sortKey={ MIDIMappedParameterSortAttr.MIDISource }
sortOrder={ sortOrder }
sorted={ sortAttr === MIDIMappedParameterSortAttr.MIDIChannel }
sorted={ sortAttr === MIDIMappedParameterSortAttr.MIDISource }
>
Channel
</TableHeaderCell>
<TableHeaderCell
className={ classes.midiControlColumnHeader }
fz="xs"
onSort={ onSort }
sortKey={ MIDIMappedParameterSortAttr.MIDIControl }
sortOrder={ sortOrder }
sorted={ sortAttr === MIDIMappedParameterSortAttr.MIDIControl }
>
Control
Source
</TableHeaderCell>
<TableHeaderCell
className={ classes.parameterNameColumnHeader }
Expand Down Expand Up @@ -91,8 +79,7 @@ const MIDIMappedParameterList: FC<MIDIMappedParameterListProps> = memo(function
instance={ pInstance }
param={ p }
onClearMIDIMapping={ onClearParameterMIDIMapping }
onUpdateMIDIChannel={ onUpdateParameterMIDIChannel }
onUpdateMIDIControl={ onUpdateParameterMIDIControl }
onUpdateMIDIMapping={ onUpdateParameterMIDIMapping }
/>
);
})
Expand Down
Loading
Loading