listen for midi binding control range.

This commit is contained in:
Robin E. R. Davies
2025-06-26 10:24:48 -04:00
parent 2974033128
commit 07af383211
20 changed files with 591 additions and 522 deletions
+8
View File
@@ -25,6 +25,9 @@ export default class MidiBinding {
this.bindingType = input.bindingType;
this.note = input.note;
this.control = input.control;
this.minControlValue = input.minControlValue?? 0;
this.maxControlValue = input.maxControlValue?? 127;
this.rotaryScale = input.rotaryScale?? 1;
this.minValue = input.minValue;
this.maxValue = input.maxValue;
this.linearControlType = input.linearControlType;
@@ -53,6 +56,9 @@ export default class MidiBinding {
&& (this.note === other.note)
&& (this.control === other.control)
&& (this.minValue === other.minValue)
&& (this.minControlValue === other.minControlValue)
&& (this.maxControlValue === other.maxControlValue)
&& (this.rotaryScale === other.rotaryScale)
&& (this.maxValue === other.maxValue)
&& (this.linearControlType === other.linearControlType)
&& (this.switchControlType === other.switchControlType)
@@ -73,6 +79,8 @@ export default class MidiBinding {
bindingType: number = MidiBinding.BINDING_TYPE_NONE;
note: number = 12*4+24; // C4.
control: number = 1;
minControlValue: number = 0;
maxControlValue: number = 127;
minValue: number = 0;
maxValue: number = 1;
rotaryScale: number = 1;
+311 -66
View File
@@ -18,11 +18,12 @@
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import { Component } from 'react';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import { ListenHandle, MidiMessage, PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import { Theme } from '@mui/material/styles';
import WithStyles from './WithStyles';
import { withStyles } from "tss-react/mui";
import {createStyles} from './WithStyles';
import { createStyles } from './WithStyles';
import Snackbar from '@mui/material/Snackbar';
import { UiPlugin } from './Lv2Plugin';
@@ -47,7 +48,7 @@ const styles = (theme: Theme) => createStyles({
}
});
enum MidiControlType {
export enum MidiControlType {
None,
Select,
Dial,
@@ -56,18 +57,57 @@ enum MidiControlType {
MomentarySwitch
}
export function getMidiControlType(uiPlugin: UiPlugin | undefined, symbol: string): MidiControlType {
if (!uiPlugin) return MidiControlType.None;
let port = uiPlugin.getControl(symbol);
if (!port) return MidiControlType.None;
if (symbol === "__bypass") {
return MidiControlType.Toggle;
}
if (!port) return MidiControlType.None;
if (port.mod_momentaryOffByDefault || port.mod_momentaryOnByDefault) {
return MidiControlType.MomentarySwitch;
}
if (port.trigger_property) {
return MidiControlType.Trigger;
}
if (port.trigger_property) {
return MidiControlType.Trigger;
}
if (port.isAbToggle() || port.isOnOffSwitch()) {
return MidiControlType.Toggle;
}
if (port.isSelect()) {
return MidiControlType.Select;
}
return MidiControlType.Dial;
}
export function canBindToNote(controlType: MidiControlType): boolean {
return controlType === MidiControlType.Toggle
|| controlType === MidiControlType.Trigger
|| controlType === MidiControlType.MomentarySwitch
}
interface MidiBindingViewProps extends WithStyles<typeof styles> {
instanceId: number;
listen: boolean;
midiBinding: MidiBinding;
uiPlugin: UiPlugin;
midiControlType: MidiControlType;
onChange: (instanceId: number, newBinding: MidiBinding) => void;
onListen: (instanceId: number, key: string, listenForControl: boolean) => void;
}
interface MidiBindingViewState {
midiControlType: MidiControlType;
listenSymbol: string;
listenForRangeSymbol: string;
listenForRangeMax: number;
listenForRangeMin: number;
listenSnackbarOpen: boolean;
}
@@ -83,7 +123,11 @@ const MidiBindingView =
super(props);
this.model = PiPedalModelFactory.getInstance();
this.state = {
midiControlType: this.getControlType()
listenSymbol: "",
listenForRangeSymbol: "",
listenForRangeMax: 127,
listenForRangeMin: 0,
listenSnackbarOpen: false
};
}
@@ -139,6 +183,16 @@ const MidiBindingView =
newBinding.maxValue = value;
this.props.onChange(this.props.instanceId, newBinding);
}
handleCtlMinChange(value: number): void {
let newBinding = this.props.midiBinding.clone();
newBinding.minControlValue = Math.round(value);
this.props.onChange(this.props.instanceId, newBinding);
}
handleCtlMaxChange(value: number): void {
let newBinding = this.props.midiBinding.clone();
newBinding.maxControlValue = Math.round(value);
this.props.onChange(this.props.instanceId, newBinding);
}
handleScaleChange(value: number): void {
let newBinding = this.props.midiBinding.clone();
newBinding.rotaryScale = value;
@@ -158,10 +212,23 @@ const MidiBindingView =
return result;
}
mounted: boolean = false;
componentDidMount() {
super.componentDidMount?.();
this.mounted = true;
}
componentWillUnmount() {
this.mounted = false;
this.cancelListenForControl();
super.componentWillUnmount?.();
}
validateSwitchControlType(midiBinding: MidiBinding) {
// :-(
let controlType = this.state.midiControlType;
let controlType = this.props.midiControlType;
if (controlType === MidiControlType.Toggle
&& midiBinding.bindingType === MidiBinding.BINDING_TYPE_NOTE) {
if (midiBinding.switchControlType !== MidiBinding.TRIGGER_ON_RISING_EDGE // Toggle on Note On.
@@ -181,41 +248,154 @@ const MidiBindingView =
}
getControlType(): MidiControlType {
if (this.props.midiBinding.symbol === "__bypass") {
return MidiControlType.Toggle;
}
if (!this.props.uiPlugin) return MidiControlType.None;
let port = this.props.uiPlugin.getControl(this.props.midiBinding.symbol);
if (!port) return MidiControlType.None;
if (port.mod_momentaryOffByDefault || port.mod_momentaryOnByDefault) {
return MidiControlType.MomentarySwitch;
}
if (port.trigger_property) {
return MidiControlType.Trigger;
}
if (port.trigger_property) {
return MidiControlType.Trigger;
}
if (port.isAbToggle() || port.isOnOffSwitch()) {
return MidiControlType.Toggle;
}
if (port.isSelect()) {
return MidiControlType.Select;
}
return MidiControlType.Dial;
return this.props.midiControlType;
}
///////////////////////////////////////
listenTimeoutHandle?: number;
listenHandle?: ListenHandle;
cancelListenForControl() {
this.stopListenTimeout();
if (this.listenHandle) {
this.model.cancelListenForMidiEvent(this.listenHandle)
this.listenHandle = undefined;
}
this.setState({ listenSymbol: "", listenForRangeSymbol: "" });
}
handleListenForRangeSucceeded(instanceId: number, symbol: string, midiMessage: MidiMessage) {
if (!midiMessage.isControl()) return;
let binding = this.props.midiBinding;
if (binding.control != midiMessage.cc1) {
return;
}
let value = midiMessage.cc2;
let min = this.state.listenForRangeMin;
let max = this.state.listenForRangeMax;
let update = false;
if (value < min) {
min = value;
update = true;
}
if (value > max) {
max = value;
update = true;
}
if (!update) {
return; // No change, so ignore.
}
let newBinding = binding.clone();
newBinding.minControlValue = min;
newBinding.maxControlValue = max;
this.props.onChange(this.props.instanceId, newBinding);
this.setState({ listenForRangeMin: min, listenForRangeMax: max });
this.startListenTimeout(20000);
}
handleListenSucceeded(instanceId: number, symbol: string, midiMessage: MidiMessage) {
if (instanceId !== this.props.instanceId) {
return;
}
if (symbol !== this.props.midiBinding.symbol) {
return;
}
let binding = this.props.midiBinding;
let newBinding = binding.clone();
if (midiMessage.isNote()) {
if (!canBindToNote(this.props.midiControlType)) {
return;
}
newBinding.bindingType = MidiBinding.BINDING_TYPE_NOTE
newBinding.note = midiMessage.cc1;
} else if (midiMessage.isControl()) {
newBinding.bindingType = MidiBinding.BINDING_TYPE_CONTROL
newBinding.control = midiMessage.cc1;
} else {
return;
}
this.props.onChange(this.props.instanceId, newBinding);
this.cancelListenForControl();
}
handleListenForControlRange(instanceId: number, symbol: string): void {
if (this.state.listenForRangeSymbol !== "") {
this.cancelListenForControl();
return;
}
this.cancelListenForControl();
this.setState({
listenSymbol: "",
listenForRangeSymbol: symbol,
listenForRangeMin: 127,
listenForRangeMax: 0,
listenSnackbarOpen: true
});
this.startListenTimeout(20000);
this.listenHandle = this.model.listenForMidiEvent(
(midiMessage) => {
this.handleListenForRangeSucceeded(instanceId, symbol, midiMessage);
});
}
stopListenTimeout(): void {
if (this.listenTimeoutHandle) {
clearTimeout(this.listenTimeoutHandle);
}
}
startListenTimeout(timeout: number): void {
this.stopListenTimeout();
this.listenTimeoutHandle = setTimeout(() => {
this.cancelListenForControl();
this.listenTimeoutHandle = undefined;
}, timeout);
}
handleListen(): void {
if (this.state.listenSymbol !== "") {
this.cancelListenForControl();
return;
}
this.cancelListenForControl();
this.setState({
listenSymbol: this.props.midiBinding.symbol,
listenForRangeSymbol: "",
listenSnackbarOpen: true
});
let instanceId = this.props.instanceId;
let symbol = this.props.midiBinding.symbol;
this.startListenTimeout(8000);
this.listenHandle = this.model.listenForMidiEvent(
(midiMessage) => {
this.handleListenSucceeded(instanceId, symbol, midiMessage);
});
}
///////////////////////////////////////
render() {
const classes = withStyles.getClasses(this.props);
let midiBinding = this.props.midiBinding;
let uiPlugin = this.props.uiPlugin;
if (!uiPlugin) {
if (this.props.midiControlType === MidiControlType.None) {
return (<div />);
}
let controlType = this.state.midiControlType;
let controlType = this.props.midiControlType;
let showLinearRange =
controlType === MidiControlType.Dial &&
@@ -238,9 +418,7 @@ const MidiBindingView =
value={midiBinding.bindingType}
>
<MenuItem value={0}>None</MenuItem>
{(controlType === MidiControlType.Toggle
|| controlType === MidiControlType.Trigger
|| controlType === MidiControlType.MomentarySwitch) && (
{(canBindToNote(this.props.midiControlType)) && (
<MenuItem value={1}>Note</MenuItem>
)}
<MenuItem value={2}>Control</MenuItem>
@@ -262,15 +440,15 @@ const MidiBindingView =
</Select>
<IconButtonEx
tooltip="Listen for MIDI input"
onBlur={() => {
this.cancelListenForControl();
}}
onClick={() => {
if (this.props.listen) {
this.props.onListen(-2, "", false)
} else {
this.props.onListen(this.props.instanceId, this.props.midiBinding.symbol, false)
}
this.handleListen()
}}
size="large">
{this.props.listen ? (
{this.state.listenSymbol !== "" ? (
<MicOutlinedIcon />
) : (
<MicNoneOutlinedIcon />
@@ -296,15 +474,15 @@ const MidiBindingView =
</Select>
<IconButtonEx
tooltip="Listen for MIDI input"
onBlur={() => {
this.cancelListenForControl();
}}
onClick={() => {
if (this.props.listen) {
this.props.onListen(-2, "", false)
} else {
this.props.onListen(this.props.instanceId, this.props.midiBinding.symbol, true)
}
this.handleListen()
}}
size="large">
{this.props.listen ? (
{this.state.listenSymbol !== "" ? (
<MicOutlinedIcon />
) : (
<MicNoneOutlinedIcon />
@@ -314,6 +492,27 @@ const MidiBindingView =
</div>
)
}
{midiBinding.bindingType === MidiBinding.BINDING_TYPE_NONE &&
(
<IconButtonEx
tooltip="Listen for MIDI input"
onBlur={() => {
this.cancelListenForControl();
}}
onClick={() => {
this.handleListen()
}}
size="large">
{this.state.listenSymbol !== "" ? (
<MicOutlinedIcon />
) : (
<MicNoneOutlinedIcon />
)}
</IconButtonEx>
)
}
{
((controlType === MidiControlType.Toggle && midiBinding.bindingType === MidiBinding.BINDING_TYPE_NOTE) &&
(
@@ -378,25 +577,58 @@ const MidiBindingView =
<MenuItem value={MidiBinding.CIRCULAR_CONTROL_TYPE}>Rotary</MenuItem>
</Select>
</div>
)
}
{
showLinearRange && (
)}
{showLinearRange && (
<div style={{ display: "flex", flexFlow: "row wrap", alignItems: "center" }}>
<div className={classes.controlDiv}>
<Typography display="inline">Min:&nbsp;</Typography>
<NumericInput defaultValue={midiBinding.minValue} ariaLabel='min'
min={0} max={1} onChange={(value) => { this.handleMinChange(value); }}
<Typography display="inline" noWrap>Min Ctl:&nbsp;</Typography>
<NumericInput integer value={midiBinding.minControlValue} ariaLabel='min ctl val'
min={0} max={127} step={1} onChange={(value) => { this.handleCtlMinChange(value); }}
/>
</div>
)
<div style={{ display: "flex", flexFlow: "row nowrap", alignItems: "center" }}>
<div className={classes.controlDiv}>
<Typography display="inline" noWrap>Max Ctl:&nbsp;</Typography>
<NumericInput value={midiBinding.maxControlValue} ariaLabel='max ctl val'
integer={true}
min={0} max={127} step={1} onChange={(value) => { this.handleCtlMaxChange(value); }}
/>
</div>
<IconButtonEx
tooltip="Listen for control range"
onBlur={() => {
this.cancelListenForControl();
}}
onClick={() => {
this.handleListenForControlRange(this.props.instanceId, this.props.midiBinding.symbol);
}}
size="large">
{this.state.listenForRangeSymbol !== "" ? (
<MicOutlinedIcon />
) : (
<MicNoneOutlinedIcon />
)}
</IconButtonEx>
</div>
</div>
)
}
{
showLinearRange && (
<div className={classes.controlDiv}>
<Typography display="inline">Max:&nbsp;</Typography>
<NumericInput defaultValue={midiBinding.maxValue} ariaLabel='max'
min={0} max={1} onChange={(value) => { this.handleMaxChange(value); }} />
<div style={{ display: "flex", flexFlow: "row wrap", alignItems: "center" }}>
<div className={classes.controlDiv}>
<Typography noWrap display="inline">Min Val:&nbsp;</Typography>
<NumericInput value={midiBinding.minValue} ariaLabel='min'
min={0} max={1} onChange={(value) => { this.handleMinChange(value); }}
/>
</div>
<div className={classes.controlDiv}>
<Typography noWrap display="inline">Max Val:&nbsp;</Typography>
<NumericInput value={midiBinding.maxValue} ariaLabel='max'
min={0} max={1} onChange={(value) => { this.handleMaxChange(value); }} />
</div>
</div>
)
}
@@ -404,11 +636,24 @@ const MidiBindingView =
canRotaryScale && (
<div className={classes.controlDiv}>
<Typography display="inline">Scale:&nbsp;</Typography>
<NumericInput defaultValue={midiBinding.maxValue} ariaLabel='scale'
<NumericInput value={midiBinding.maxValue} ariaLabel='scale'
min={-100} max={100} onChange={(value) => { this.handleScaleChange(value); }} />
</div>
)
}
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={this.state.listenSnackbarOpen}
autoHideDuration={1500}
onClose={() => this.setState({ listenSnackbarOpen: false })}
message="Listening for MIDI input"
/>
</div>
);
+10 -88
View File
@@ -20,7 +20,7 @@
import React, { SyntheticEvent } from 'react';
import DialogEx from './DialogEx';
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
import { PiPedalModel, PiPedalModelFactory, ListenHandle } from './PiPedalModel';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import Typography from '@mui/material/Typography';
import { Theme } from '@mui/material/styles';
import WithStyles from './WithStyles';
@@ -32,8 +32,7 @@ import Toolbar from '@mui/material/Toolbar';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import IconButtonEx from './IconButtonEx';
import MidiBinding from './MidiBinding';
import MidiBindingView from './MidiBindingView';
import Snackbar from '@mui/material/Snackbar';
import MidiBindingView, { getMidiControlType } from './MidiBindingView';
import { PortGroup, UiPlugin, makeSplitUiPlugin } from './Lv2Plugin';
import { css } from '@emotion/react';
@@ -81,9 +80,6 @@ export interface MidiBindingDialogProps extends WithStyles<typeof styles> {
}
export interface MidiBindingDialogState {
listenInstanceId: number;
listenSymbol: string;
listenSnackbarOpen: boolean;
}
@@ -98,7 +94,10 @@ export const MidiBindingDialog =
this.state = {
listenInstanceId: -2,
listenSymbol: "",
listenSnackbarOpen: false
listenForRangeSymbol: "",
listenSnackbarOpen: false,
listenForRangeMin: 127,
listenForRangeMax: 0
};
this.model = PiPedalModelFactory.getInstance();
this.handleClose = this.handleClose.bind(this);
@@ -108,64 +107,9 @@ export const MidiBindingDialog =
hasHooks: boolean = false;
handleClose() {
this.cancelListenForControl();
this.props.onClose();
}
listenTimeoutHandle?: number;
listenHandle?: ListenHandle;
cancelListenForControl() {
if (this.listenTimeoutHandle) {
clearTimeout(this.listenTimeoutHandle);
this.listenTimeoutHandle = undefined;
}
if (this.listenHandle) {
this.model.cancelListenForMidiEvent(this.listenHandle)
this.listenHandle = undefined;
}
this.setState({ listenInstanceId: -2, listenSymbol: "" });
}
handleListenSucceeded(instanceId: number, symbol: string, isNote: boolean, noteOrControl: number) {
this.cancelListenForControl();
let pedalboard = this.model.pedalboard.get();
let item = pedalboard.getItem(instanceId);
if (!item) return;
let binding = item.getMidiBinding(symbol);
let newBinding = binding.clone();
if (isNote) {
newBinding.bindingType = MidiBinding.BINDING_TYPE_NOTE;
newBinding.note = noteOrControl;
} else {
newBinding.bindingType = MidiBinding.BINDING_TYPE_CONTROL;
newBinding.control = noteOrControl;
}
this.model.setMidiBinding(instanceId, newBinding);
}
handleListenForControl(instanceId: number, symbol: string, listenForControl: boolean): void {
this.cancelListenForControl();
this.setState({ listenInstanceId: instanceId, listenSymbol: symbol, listenSnackbarOpen: true });
this.listenTimeoutHandle = setTimeout(() => {
this.cancelListenForControl();
}, 8000);
this.listenHandle = this.model.listenForMidiEvent(listenForControl,
(isNote: boolean, noteOrControl: number) => {
this.handleListenSucceeded(instanceId, symbol, isNote, noteOrControl);
});
}
onWindowSizeChanged(width: number, height: number): void {
}
@@ -176,10 +120,11 @@ export const MidiBindingDialog =
this.mounted = true;
}
componentWillUnmount() {
super.componentWillUnmount();
this.mounted = false;
super.componentWillUnmount();
}
componentDidUpdate() {
@@ -255,15 +200,7 @@ export const MidiBindingDialog =
</td>
<td className={classes.bindingTd}>
<MidiBindingView instanceId={item.instanceId} midiBinding={item.getMidiBinding("__bypass")}
uiPlugin={plugin}
onListen={(instanceId: number, symbol: string, listenForControl: boolean) => {
if (instanceId === -2) {
this.cancelListenForControl();
} else {
this.handleListenForControl(instanceId, symbol, listenForControl);
}
}}
listen={item.instanceId === this.state.listenInstanceId && this.state.listenSymbol === "__bypass"}
midiControlType={ getMidiControlType(plugin, "__bypass") }
onChange={(instanceId: number, newItem: MidiBinding) => this.handleItemChanged(instanceId, newItem)}
/>
</td>
@@ -305,13 +242,8 @@ export const MidiBindingDialog =
</td>
<td className={classes.bindingTd}>
<MidiBindingView instanceId={item.instanceId}
uiPlugin={plugin}
midiControlType={getMidiControlType(plugin, symbol)}
midiBinding={item.getMidiBinding(symbol)}
listen={item.instanceId === this.state.listenInstanceId && this.state.listenSymbol === symbol}
onListen={(instanceId: number, symbol: string, listenForControl: boolean) => {
this.handleListenForControl(instanceId, symbol, listenForControl);
}}
onChange={(instanceId: number, newItem: MidiBinding) => this.handleItemChanged(instanceId, newItem)}
/>
</td>
@@ -379,16 +311,6 @@ export const MidiBindingDialog =
</table>
</div>
</div>
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={this.state.listenSnackbarOpen}
autoHideDuration={1500}
onClose={() => this.setState({ listenSnackbarOpen: false })}
message="Listening for MIDI input"
/>
</DialogEx >
);
}
+42 -18
View File
@@ -35,14 +35,18 @@ const styles = ({ palette }: Theme) => createStyles({
interface NumericInputProps extends WithStyles<typeof styles> {
ariaLabel: string;
defaultValue: number;
value: number;
min: number;
max: number;
step?: number;
integer?: boolean;
onChange: (value: number) => void;
}
interface NumericInputState {
stateValue: string;
error: boolean;
focused: boolean;
}
export const NumericInput =
@@ -54,22 +58,28 @@ export const NumericInput =
constructor(props: NumericInputProps) {
super(props);
this.state = {
error: false
stateValue: this.toDisplayValue(props.value),
error: false,
focused: false
};
}
changed: boolean = false;
handleChange(event: any): void {
handleChange(event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void {
this.changed = true;
let strValue = event.target.value;
this.setState({ stateValue: strValue });
try {
let value = Number(strValue);
if (isNaN(value))
{
this.setState({ error: true });
} else if (this.props.integer === true && !Number.isInteger(value)) {
this.setState({ error: true });
} else if (value >= this.props.min && value <= this.props.max) {
this.setState({ error: false });
this.props.onChange(value);
} else {
this.setState({ error: true });
}
@@ -80,6 +90,10 @@ export const NumericInput =
}
toDisplayValue(value: number): string {
if (this.props.integer === true)
{
return Math.round(value).toFixed(0);
}
if (value <= -1000 || value >= 1000)
{
return value.toFixed(0);
@@ -96,7 +110,7 @@ export const NumericInput =
}
}
apply(input: HTMLInputElement) {
apply(input: HTMLInputElement| HTMLTextAreaElement) {
if (this.changed) {
let strValue = input.value;
let value = Number(strValue);
@@ -104,28 +118,34 @@ export const NumericInput =
{
if (value < this.props.min) value = this.props.min;
if (value > this.props.max) value = this.props.max;
if (this.props.integer === true) value = Math.round(value);
input.value = this.toDisplayValue(value);
this.props.onChange(value);
} else {
input.value = this.toDisplayValue(this.props.defaultValue);
input.value = this.toDisplayValue(this.props.value);
this.props.onChange(this.props.value);
}
this.changed = false;
this.setState({ error: false });
}
}
handleLostFocus(e: any) {
this.apply(e.currentTarget as HTMLInputElement);
handleFocus(e: React.FocusEvent<HTMLInputElement| HTMLTextAreaElement>) {
this.changed = false;
this.setState({ focused: true });
e.currentTarget.select();
this.setState({ stateValue: this.toDisplayValue(this.props.value) });
}
handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
handleBlur(e: any) {
this.apply(e.currentTarget as HTMLInputElement);
this.setState({ focused: false });
}
handleKeyDown(e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) {
if (e.key === "Enter")
{
this.apply(e.currentTarget as HTMLInputElement);
} else if (e.key === "Escape")
{
e.currentTarget.value = this.toDisplayValue(this.props.defaultValue);
this.changed = false;
}
this.setState({ stateValue: this.toDisplayValue(this.props.value) });
this.changed = false;
}
}
@@ -135,11 +155,15 @@ export const NumericInput =
inputProps={{
'aria-label': this.props.ariaLabel,
style: { textAlign: 'right' },
"onBlur": (e: any) => this.handleLostFocus(e),
"onKeyDown": (e: any) => this.handleKeyDown(e)
"min": this.props.min,
"max": this.props.max,
"step:": this.props.step
}}
onChange={(event) => this.handleChange(event)}
defaultValue={this.toDisplayValue(this.props.defaultValue)}
onFocus={(e) => {this.handleFocus(e);}}
onBlur= {(e) => { this.handleBlur(e); }}
onKeyDown={(e) => {this.handleKeyDown(e);}}
onChange={(e) => this.handleChange(e)}
value={this.state.focused ? this.state.stateValue: this.toDisplayValue(this.props.value)}
error={this.state.error}
/>
+39 -15
View File
@@ -158,14 +158,31 @@ interface ControlValueChangeItem {
};
export class MidiMessage {
constructor(cc0: number, cc1: number, cc2: number) {
this.cc0 = cc0;
this.cc1 = cc1;
this.cc2 = cc2;
}
cc0: number;
cc1: number;
cc2: number;
isNote() {
return (this.cc0 & 0xF0) == 0x90 && this.cc2 !== 0;
}
isControl() {
return (this.cc0 & 0xF0) == 0xB0;
}
};
class MidiEventListener {
constructor(handle: number, callback: (isNote: boolean, noteOrControl: number) => void) {
constructor(handle: number, callback: (message: MidiMessage) => void) {
this.handle = handle;
this.callback = callback;
}
handle: number;
callback: (isNote: boolean, noteOrControl: number) => void;
callback: (midiMessage: MidiMessage) => void;
};
export type PatchPropertyListener = (instanceId: number, propertyUri: string, atomObject: any) => void;
@@ -679,10 +696,13 @@ export class PiPedalModel //implements PiPedalModel
}
} else if (message === "onNotifyMidiListener") {
let clientHandle = body.clientHandle as number;
let isNote = body.isNote as boolean;
let noteOrControl = body.noteOrControl as number;
this.handleNotifyMidiListener(clientHandle, isNote, noteOrControl);
let notifyBody = body as { clientHandle: number, cc0: number, cc1: number, cc2: number };
let clientHandle = notifyBody.clientHandle as number;
this.handleNotifyMidiListener(
clientHandle,
notifyBody.cc0,
notifyBody.cc1,
notifyBody.cc2);
} else if (message === "onNotifyPathPatchPropertyChanged") {
let instanceId = body.instanceId as number;
let propertyUri = body.propertyUri as string;
@@ -2355,12 +2375,12 @@ export class PiPedalModel //implements PiPedalModel
private monitorPatchPropertyListeners: PatchPropertyListenerItem[] = [];
nextListenHandle = 1;
listenForMidiEvent(listenForControl: boolean, onComplete: (isNote: boolean, noteOrControl: number) => void): ListenHandle {
listenForMidiEvent(onComplete: (midiMessage: MidiMessage) => void): ListenHandle {
let handle = this.nextListenHandle++;
this.midiListeners.push(new MidiEventListener(handle, onComplete));
this.webSocket?.send("listenForMidiEvent", { listenForControls: listenForControl, handle: handle });
this.webSocket?.send("listenForMidiEvent", { handle: handle });
return {
_handle: handle
};
@@ -2418,12 +2438,16 @@ export class PiPedalModel //implements PiPedalModel
}
handleNotifyMidiListener(clientHandle: number, isNote: boolean, noteOrControl: number) {
handleNotifyMidiListener(clientHandle: number, cc0: number, cc1: number, cc2: number): void {
let midiMessage = new MidiMessage(cc0, cc1, cc2);
if (!midiMessage.isNote() && !midiMessage.isControl()) {
return;
}
for (let i = 0; i < this.midiListeners.length; ++i) {
let listener = this.midiListeners[i];
if (listener.handle === clientHandle) {
listener.callback(isNote, noteOrControl);
listener.callback(midiMessage);
}
}
}
@@ -2464,7 +2488,7 @@ export class PiPedalModel //implements PiPedalModel
let link = window.document.createElement("A") as HTMLAnchorElement;
link.href = url;
link.target = "_blank";
link.download = "download.piPreset";
link.download = "download.piPreset";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
@@ -2705,8 +2729,8 @@ export class PiPedalModel //implements PiPedalModel
});
}
copyFilePropertyFile(
oldRelativePath: string,
newRelativePath: string,
oldRelativePath: string,
newRelativePath: string,
uiFileProperty: UiFileProperty,
overwrite: boolean
): Promise<string> {
@@ -3167,7 +3191,7 @@ export class PiPedalModel //implements PiPedalModel
item.title = title;
this.pedalboard.set(newPedalboard);
// notify the server.
// xxx;
this.webSocket?.send("setPedalboardItemTitle", { instanceId: instanceId, title: title });
}
};
+10 -189
View File
@@ -23,200 +23,21 @@
*/
import { Component } from 'react';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import { Theme } from '@mui/material/styles';
import WithStyles from './WithStyles';
import { withStyles } from "tss-react/mui";
import {createStyles} from './WithStyles';
import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select';
import MidiBinding from './MidiBinding';
import Utility from './Utility';
import MicNoneOutlinedIcon from '@mui/icons-material/MicNoneOutlined';
import MicOutlinedIcon from '@mui/icons-material/MicOutlined';
import IconButtonEx from './IconButtonEx';
const styles = (theme: Theme) => createStyles({
controlDiv: { flex: "0 0 auto", marginRight: 12, verticalAlign: "center", height: 48, paddingTop: 8, paddingBottom: 8 },
controlDiv2: {
flex: "0 0 auto", marginRight: 12, verticalAlign: "center",
height: 48, paddingTop: 0, paddingBottom: 0, whiteSpace: "nowrap"
}
});
interface SystemMidiBindingViewProps extends WithStyles<typeof styles> {
import MidiBindingView, {MidiControlType} from './MidiBindingView';
interface SystemMidiBindingViewProps {
instanceId: number;
listen: boolean;
midiBinding: MidiBinding;
onChange: (instanceId: number, newBinding: MidiBinding) => void;
onListen: (instanceId: number, key: string, listenForControl: boolean) => void;
}
interface SystemMidiBindingViewState {
function SystemMidiBindingView(props: SystemMidiBindingViewProps) {
return (
<MidiBindingView
instanceId={props.instanceId}
midiBinding={props.midiBinding}
onChange={props.onChange}
midiControlType={MidiControlType.Trigger}
/>)
}
const SystemMidiBindingView =
withStyles(
class extends Component<SystemMidiBindingViewProps, SystemMidiBindingViewState> {
model: PiPedalModel;
constructor(props: SystemMidiBindingViewProps) {
super(props);
this.model = PiPedalModelFactory.getInstance();
this.state = {
};
}
handleTypeChange(e: any, extra: any) {
let newValue = parseInt(e.target.value);
let newBinding = this.props.midiBinding.clone();
newBinding.bindingType = newValue;
this.props.onChange(this.props.instanceId, newBinding);
}
handleNoteChange(e: any, extra: any) {
let newValue = parseInt(e.target.value);
let newBinding = this.props.midiBinding.clone();
newBinding.note = newValue;
this.props.onChange(this.props.instanceId, newBinding);
}
handleControlChange(e: any, extra: any) {
let newValue = parseInt(e.target.value);
let newBinding = this.props.midiBinding.clone();
newBinding.control = newValue;
this.props.onChange(this.props.instanceId, newBinding);
}
handleLatchControlTypeChange(e: any, extra: any) {
let newValue = parseInt(e.target.value);
let newBinding = this.props.midiBinding.clone();
newBinding.switchControlType = newValue;
this.props.onChange(this.props.instanceId, newBinding);
}
handleLinearControlTypeChange(e: any, extra: any) {
let newValue = parseInt(e.target.value);
let newBinding = this.props.midiBinding.clone();
newBinding.linearControlType = newValue;
this.props.onChange(this.props.instanceId, newBinding);
}
handleMinChange(value: number): void {
let newBinding = this.props.midiBinding.clone();
newBinding.minValue = value;
this.props.onChange(this.props.instanceId, newBinding);
}
handleMaxChange(value: number): void {
let newBinding = this.props.midiBinding.clone();
newBinding.maxValue = value;
this.props.onChange(this.props.instanceId, newBinding);
}
handleScaleChange(value: number): void {
let newBinding = this.props.midiBinding.clone();
newBinding.rotaryScale = value;
this.props.onChange(this.props.instanceId, newBinding);
}
generateMidiSelects(): React.ReactNode[] {
let result: React.ReactNode[] = [];
for (let i = 0; i < 127; ++i) {
result.push(
<MenuItem value={i}>{Utility.midiNoteName(i)}</MenuItem>
)
}
return result;
}
generateControlSelects(): React.ReactNode[] {
return Utility.validMidiControllers.map((control) => (
<MenuItem key={control.value} value={control.value}>{control.displayName}</MenuItem>
)
);
}
render() {
const classes = withStyles.getClasses(this.props);
let midiBinding = this.props.midiBinding;
return (
<div style={{ display: "flex", flexDirection: "row", flexWrap: "wrap", justifyContent: "left", alignItems: "center", minHeight: 48 }}>
<div className={classes.controlDiv} >
<Select variant="standard"
style={{ width: 80, }}
onChange={(e, extra) => this.handleTypeChange(e, extra)}
value={midiBinding.bindingType}
>
<MenuItem value={0}>None</MenuItem>
<MenuItem value={1}>Note</MenuItem>
<MenuItem value={2}>Control</MenuItem>
</Select>
</div>
{
(midiBinding.bindingType === MidiBinding.BINDING_TYPE_NOTE) &&
(
<div className={classes.controlDiv} >
<Select variant="standard"
style={{ width: 80, verticalAlign: 'center' }}
onChange={(e, extra) => this.handleNoteChange(e, extra)}
value={midiBinding.note}
>
{
this.generateMidiSelects()
}
</Select>
</div>
)
}
{
(midiBinding.bindingType === MidiBinding.BINDING_TYPE_CONTROL) &&
(
<div className={classes.controlDiv} >
<Select variant="standard"
style={{ width: 80 }}
onChange={(e, extra) => this.handleControlChange(e, extra)}
value={midiBinding.control}
renderValue={(value) => { return "CC-" + value }}
>
{
this.generateControlSelects()
}
</Select>
</div>
)
}
<IconButtonEx
tooltip="Listen for MIDI input"
onClick={() => {
if (this.props.listen) {
this.props.onListen(-2, "", false)
} else {
this.props.onListen(this.props.instanceId, this.props.midiBinding.symbol, false)
}
}}
size="large">
{this.props.listen ? (
<MicOutlinedIcon />
) : (
<MicNoneOutlinedIcon />
)}
</IconButtonEx>
</div>
);
}
},
styles
);
export default SystemMidiBindingView;
+1 -66
View File
@@ -27,7 +27,7 @@ import React, { SyntheticEvent } from 'react';
import { css } from '@emotion/react';
import DialogEx from './DialogEx';
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
import { PiPedalModel, PiPedalModelFactory, ListenHandle } from './PiPedalModel';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import Typography from '@mui/material/Typography';
import { Theme } from '@mui/material/styles';
import WithStyles from './WithStyles';
@@ -182,66 +182,9 @@ export const SystemMidiBindingDialog =
hasHooks: boolean = false;
handleClose() {
this.cancelListenForControl();
this.props.onClose();
}
listenTimeoutHandle?: number;
listenHandle?: ListenHandle;
cancelListenForControl() {
if (this.listenTimeoutHandle) {
clearTimeout(this.listenTimeoutHandle);
this.listenTimeoutHandle = undefined;
}
if (this.listenHandle) {
this.model.cancelListenForMidiEvent(this.listenHandle)
this.listenHandle = undefined;
}
this.setState({ listenInstanceId: -2, listenSymbol: "" });
}
handleListenSucceeded(instanceId: number, symbol: string, isNote: boolean, noteOrControl: number) {
this.cancelListenForControl();
for (var binding of this.state.systemMidiBindings) {
if (binding.instanceId === instanceId) {
let newBinding = binding.midiBinding.clone();
if (isNote) {
newBinding.bindingType = MidiBinding.BINDING_TYPE_NOTE;
newBinding.note = noteOrControl;
} else {
newBinding.bindingType = MidiBinding.BINDING_TYPE_CONTROL;
newBinding.control = noteOrControl;
}
this.model.setSystemMidiBinding(instanceId, newBinding);
return;
}
}
}
handleListenForControl(instanceId: number, symbol: string, listenForControl: boolean): void {
this.cancelListenForControl();
this.setState({ listenInstanceId: instanceId, listenSymbol: symbol, listenSnackbarOpen: true });
this.listenTimeoutHandle = setTimeout(() => {
this.cancelListenForControl();
}, 8000);
this.listenHandle = this.model.listenForMidiEvent(listenForControl,
(isNote: boolean, noteOrControl: number) => {
this.handleListenSucceeded(instanceId, symbol, isNote, noteOrControl);
});
}
onWindowSizeChanged(width: number, height: number): void {
}
@@ -293,14 +236,6 @@ export const SystemMidiBindingDialog =
</td>
<td className={classes.bindingTd}>
<SystemMidiBindingView instanceId={item.instanceId} midiBinding={item.midiBinding}
onListen={(instanceId: number, symbol: string, listenForControl: boolean) => {
if (instanceId === -2) {
this.cancelListenForControl();
} else {
this.handleListenForControl(instanceId, symbol, listenForControl);
}
}}
listen={item.instanceId === this.state.listenInstanceId && this.state.listenSymbol === item.midiBinding.symbol}
onChange={(instanceId: number, newItem: MidiBinding) => this.handleItemChanged(instanceId, newItem)}
/>
</td>
+21 -16
View File
@@ -48,9 +48,8 @@ function ToolTipEx(props: ToolTipExProps) {
setTimeoutInstance(timeoutInstance + 1); // make useeffect run.
}
function stopTimeout() {
if (timeout > 0) {
startTimeout(0);
}
setTimeout(0);
setTimeoutInstance(0);
}
function handleMouseEnter(event: React.MouseEvent<HTMLDivElement>) {
setOpen(false);
@@ -60,6 +59,7 @@ function ToolTipEx(props: ToolTipExProps) {
function handleMouseLeave(event: React.MouseEvent<HTMLDivElement>) {
setOpen(false);
stopTimeout();
setLongPressLeaving(false);
}
function handlePointerDownCapture(event: React.PointerEvent<HTMLDivElement>) {
@@ -81,7 +81,7 @@ function ToolTipEx(props: ToolTipExProps) {
if (valueTooltip === undefined) // no timeout if there's a value tooltip
{
if (t > 0) {
// console.log("ToolTipEx: starting timeout for ", t);
console.log("ToolTipEx: starting timeout for ", t);
handle = window.setTimeout(() => {
setOpen(true);
},t);
@@ -89,7 +89,7 @@ function ToolTipEx(props: ToolTipExProps) {
}
return () => {
if (handle !== null) {
// console.log("ToolTipEx: clearing timeout for ", t);
console.log("ToolTipEx: clearing timeout for ", t);
window.clearTimeout(handle);
}
};
@@ -179,41 +179,46 @@ function ToolTipEx(props: ToolTipExProps) {
stopTimeout(); // Reset hover timeout on click
}
return (
<div
<div style={{ display: 'inline-block' }}
onClickCapture={(e) => {
// console.log("ToolTipEx: onClickCapture");
console.log("ToolTipEx: onClickCapture");
handleClickCapture(e);
setTimeout(0);
setOpen(false);
setIsLongPress(false);
setLongPressLeaving(true);
}}
onMouseEnter={(e)=> {
// console.log("ToolTipEx: onMouseEnter");
handleMouseEnter(e);
console.log("ToolTipEx: onMouseEnter");
if (!longPressLeaving) {// Don't handle mouse enter if we're in a long press leaving state
handleMouseEnter(e);
}
}}
onMouseLeave={(e)=> {
// console.log("ToolTipEx: onMouseLeave");
console.log("ToolTipEx: onMouseLeave");
handleMouseLeave(e);
}}
onPointerCancelCapture={(e) => {
// console.log("ToolTipEx: onPointerCancelCapture");
console.log("ToolTipEx: onPointerCancelCapture");
handlePointerCancel(e);
}}
onContextMenuCapture={(e) => {
// console.log("ToolTipEx: onContextMenuCapture");
console.log("ToolTipEx: onContextMenuCapture");
handleConextMenu(e);
return false;
}}
onPointerDownCapture={(e) => {
// console.log("ToolTipEx: onPointerDownCapture");
console.log("ToolTipEx: onPointerDownCapture");
handlePointerDownCapture(e);
}}
onPointerMoveCapture={(e) => {
// console.log("ToolTipEx: onPointerMoveCapture");
console.log("ToolTipEx: onPointerMoveCapture");
handlePointerMoveCapture(e);
}}
onPointerUpCapture={(e) => {
// console.log("ToolTipEx: onPointerUpCapture");
console.log("ToolTipEx: onPointerUpCapture");
handlePointerUpCapture(e);
}}
>