zds-pickers 4.0.8 → 4.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/index.cjs.js +12 -12
  2. package/dist/index.cjs.js.map +1 -1
  3. package/dist/index.es.js +1265 -1890
  4. package/dist/index.es.js.map +1 -1
  5. package/dist/types/midi/export.d.ts +8 -9
  6. package/dist/types/pickers/StatusPicker.d.ts +2 -2
  7. package/lib/hooks/useComponentSize.js +40 -0
  8. package/lib/hooks/useStateWithDynamicDefault.js +9 -0
  9. package/lib/index.js +21 -0
  10. package/lib/midi/ccValues.js +134 -0
  11. package/lib/midi/export.js +27 -0
  12. package/lib/midi/export.ts +2 -3
  13. package/lib/other/DefaultTooltip.js +3 -0
  14. package/lib/other/OctavePlayer.js +48 -0
  15. package/lib/other/SVGText.js +109 -0
  16. package/lib/other/SoundFontProvider.js +87 -0
  17. package/lib/pickers/CCPicker.js +6 -0
  18. package/lib/pickers/ChannelMappingPicker.js +50 -0
  19. package/lib/pickers/ChannelPicker.js +21 -0
  20. package/lib/pickers/Knob.js +49 -0
  21. package/lib/pickers/KnobPicker.js +29 -0
  22. package/lib/pickers/LatchPicker.js +9 -0
  23. package/lib/pickers/MappingPicker.js +37 -0
  24. package/lib/pickers/NotePicker.js +45 -0
  25. package/lib/pickers/PianoPicker.js +27 -0
  26. package/lib/pickers/PolarityPicker.js +11 -0
  27. package/lib/pickers/ResponseCurve.js +154 -0
  28. package/lib/pickers/ResponseCurvePicker.js +19 -0
  29. package/lib/pickers/Select.js +79 -0
  30. package/lib/pickers/StatusPicker.js +5 -0
  31. package/lib/pickers/ValuePicker.js +23 -0
  32. package/lib/pickers/knobSkin10.js +93 -0
  33. package/lib/rotaryKnob/InternalInput.js +22 -0
  34. package/lib/rotaryKnob/Types.js +1 -0
  35. package/lib/rotaryKnob/helpers/DrawCircle.js +9 -0
  36. package/lib/rotaryKnob/helpers/DrawLine.js +9 -0
  37. package/lib/rotaryKnob/helpers/HelpersOverlay.js +26 -0
  38. package/lib/rotaryKnob/helpers/KnobVisualHelpers.js +52 -0
  39. package/lib/rotaryKnob/index.js +244 -0
  40. package/lib/rotaryKnob/knobdefaultskin.js +56 -0
  41. package/lib/rotaryKnob/utils.js +106 -0
  42. package/lib/utils.js +21 -0
  43. package/package.json +2 -3
  44. package/tsconfig.json +1 -1
  45. package/vite.config.ts +3 -19
@@ -1,12 +1,12 @@
1
1
  import type { Option } from 'lib/pickers/Select';
2
2
  declare const Statuses: {
3
- noteOff: number;
4
- noteOn: number;
5
- aftertouch: number;
6
- controlChange: number;
7
- programChange: number;
8
- channelPressure: number;
9
- pitchWheel: number;
3
+ readonly noteOff: 8;
4
+ readonly noteOn: 9;
5
+ readonly aftertouch: 10;
6
+ readonly controlChange: 11;
7
+ readonly programChange: 12;
8
+ readonly channelPressure: 13;
9
+ readonly pitchWheel: 14;
10
10
  };
11
11
  type Status = (typeof Statuses)[keyof typeof Statuses];
12
12
  declare const MASK_CHANNEL = 15;
@@ -18,5 +18,4 @@ declare const extractStatusAndChannel: (status: number) => {
18
18
  status: Status;
19
19
  channel: number;
20
20
  };
21
- export { combineStatusWithChannel, extractStatusAndChannel, getStatusLabel, MASK_CHANNEL, MASK_STATUS, statusOptions, Statuses, };
22
- export type { Status };
21
+ export { combineStatusWithChannel, extractStatusAndChannel, getStatusLabel, MASK_CHANNEL, MASK_STATUS, statusOptions, Statuses, type Status, };
@@ -5,8 +5,8 @@ type StatusPickerProps = SelectProps<Status> & {
5
5
  statuses: Option<Status>[];
6
6
  };
7
7
  type StatusPickerRef = SelectInstance<Option<Status>, false, GroupBase<Option<Status>>>;
8
- declare const StatusPicker: import("react").ForwardRefExoticComponent<SelectProps<number> & {
8
+ declare const StatusPicker: import("react").ForwardRefExoticComponent<SelectProps<Status> & {
9
9
  statuses: Option<Status>[];
10
- } & import("react").RefAttributes<SelectInstance<Option<number>, false, GroupBase<Option<number>>>>>;
10
+ } & import("react").RefAttributes<SelectInstance<Option<Status>, false, GroupBase<Option<Status>>>>>;
11
11
  export default StatusPicker;
12
12
  export type { StatusPickerProps, StatusPickerRef };
@@ -0,0 +1,40 @@
1
+ import { useCallback, useLayoutEffect, useState } from 'react';
2
+ const getSize = (el) => el
3
+ ? {
4
+ width: el.offsetWidth,
5
+ height: el.offsetHeight,
6
+ }
7
+ : {
8
+ width: 0,
9
+ height: 0,
10
+ };
11
+ const useComponentSize = (ref) => {
12
+ const [componentSize, setComponentSize] = useState(getSize(ref ? ref.current : null));
13
+ const handleResize = useCallback(() => {
14
+ if (ref.current) {
15
+ setComponentSize(getSize(ref.current));
16
+ }
17
+ }, [ref]);
18
+ // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
19
+ useLayoutEffect(() => {
20
+ if (!ref.current)
21
+ return;
22
+ handleResize();
23
+ if (typeof ResizeObserver === 'function') {
24
+ const resizeObserver = new ResizeObserver(() => {
25
+ handleResize();
26
+ });
27
+ resizeObserver.observe(ref.current);
28
+ return () => {
29
+ resizeObserver.disconnect();
30
+ // resizeObserver = null
31
+ };
32
+ }
33
+ window.addEventListener('resize', handleResize);
34
+ return () => {
35
+ window.removeEventListener('resize', handleResize);
36
+ };
37
+ }, [ref.current]);
38
+ return componentSize;
39
+ };
40
+ export default useComponentSize;
@@ -0,0 +1,9 @@
1
+ import { useEffect, useState } from 'react';
2
+ const useStateWithDynamicDefault = (defaultVal) => {
3
+ const [state, setState] = useState(defaultVal);
4
+ useEffect(() => {
5
+ setState(defaultVal);
6
+ }, [defaultVal]);
7
+ return [state, setState];
8
+ };
9
+ export default useStateWithDynamicDefault;
package/lib/index.js ADDED
@@ -0,0 +1,21 @@
1
+ // Components
2
+ export { default as ChannelMappingPicker } from './pickers/ChannelMappingPicker';
3
+ export { default as ChannelPicker } from './pickers/ChannelPicker';
4
+ export { default as DefaultTooltip } from './other/DefaultTooltip';
5
+ export { default as Knob } from './pickers/Knob';
6
+ export { default as KnobPicker } from './pickers/KnobPicker';
7
+ export { default as LatchPicker } from './pickers/LatchPicker';
8
+ export { default as MappingPicker } from './pickers/MappingPicker';
9
+ export { default as NotePicker } from './pickers/NotePicker';
10
+ export { default as OctavePlayer } from './other/OctavePlayer';
11
+ export { default as PianoPicker } from './pickers/PianoPicker';
12
+ export { default as PolarityPicker } from './pickers/PolarityPicker';
13
+ export { default as ResponseCurvePicker } from './pickers/ResponseCurvePicker';
14
+ export { default as Select } from './pickers/Select';
15
+ export { default as StatusPicker } from './pickers/StatusPicker';
16
+ export { default as SVGText } from './other/SVGText';
17
+ export { default as ValuePicker } from './pickers/ValuePicker';
18
+ // MIDI Values & Functions
19
+ export { combineStatusWithChannel, extractStatusAndChannel, getStatusLabel, MASK_CHANNEL, MASK_STATUS, Statuses, } from './midi/export';
20
+ export { default as ResponseCurve, RESPONSE_CURVES, } from './pickers/ResponseCurve';
21
+ export { default as ccValues } from './midi/ccValues';
@@ -0,0 +1,134 @@
1
+ const CC_NAMES = [
2
+ 'Bank Select',
3
+ 'Modulation Wheel',
4
+ 'Breath Controller',
5
+ '',
6
+ 'Foot Control',
7
+ 'Portamento Time',
8
+ 'Data Entry Slider',
9
+ 'Volume',
10
+ 'Balance',
11
+ '',
12
+ 'Pan',
13
+ 'Expression',
14
+ 'Effect Control 1',
15
+ 'Effect Control 2',
16
+ '',
17
+ '',
18
+ '',
19
+ '',
20
+ '',
21
+ '',
22
+ '',
23
+ '',
24
+ '',
25
+ '',
26
+ '',
27
+ '',
28
+ '',
29
+ '',
30
+ '',
31
+ '',
32
+ '',
33
+ '',
34
+ 'Controller 0',
35
+ 'Controller 1',
36
+ 'Controller 2',
37
+ 'Controller 3',
38
+ 'Controller 4',
39
+ 'Controller 5',
40
+ 'Controller 6',
41
+ 'Controller 7',
42
+ 'Controller 8',
43
+ 'Controller 9',
44
+ 'Controller 10',
45
+ 'Controller 11',
46
+ 'Controller 12',
47
+ 'Controller 13',
48
+ 'Controller 14',
49
+ 'Controller 15',
50
+ 'Controller 16',
51
+ 'Controller 17',
52
+ 'Controller 18',
53
+ 'Controller 19',
54
+ 'Controller 20',
55
+ 'Controller 21',
56
+ 'Controller 22',
57
+ 'Controller 23',
58
+ 'Controller 24',
59
+ 'Controller 25',
60
+ 'Controller 26',
61
+ 'Controller 27',
62
+ 'Controller 28',
63
+ 'Controller 29',
64
+ 'Controller 30',
65
+ 'Controller 31',
66
+ 'Sustain',
67
+ 'Portamento',
68
+ 'Sostenuto',
69
+ 'Soft Pedal',
70
+ 'Legato Pedal',
71
+ 'Sustain 2',
72
+ 'Sound Variation',
73
+ 'Resonance/Timbre',
74
+ 'Sound Release Time',
75
+ 'Sound Attack Time',
76
+ 'Frequency Cutoff',
77
+ 'Sound Control 6',
78
+ 'Sound Control 7',
79
+ 'Sound Control 8',
80
+ 'Sound Control 9',
81
+ 'Sound Control 10',
82
+ 'Decay or General 1',
83
+ 'Hi-Pass or General 2',
84
+ 'General 3',
85
+ 'General 4',
86
+ 'Portamento Amount',
87
+ '',
88
+ '',
89
+ '',
90
+ '',
91
+ '',
92
+ '',
93
+ 'Reverb Level',
94
+ 'Tremolo Depth',
95
+ 'Chorus Level',
96
+ 'Detune',
97
+ 'Phaser Depth',
98
+ 'Data Entry +1',
99
+ 'Data Entry -1',
100
+ 'Non-registered LSB',
101
+ 'Non-registered MSB',
102
+ 'Registered msb',
103
+ 'Registered lsb',
104
+ '',
105
+ '',
106
+ '',
107
+ '',
108
+ '',
109
+ '',
110
+ '',
111
+ '',
112
+ 'Shift 1',
113
+ 'Shift 2',
114
+ 'Shift 3',
115
+ 'Shift 4',
116
+ 'Shift 5',
117
+ 'Shift 6',
118
+ '',
119
+ '',
120
+ '',
121
+ '',
122
+ 'All Sound Off',
123
+ 'All Controllers Off',
124
+ 'Local Mode',
125
+ 'All Notes Off',
126
+ 'Omni Mode Off',
127
+ 'Omni Mode On',
128
+ 'Mono Mode On',
129
+ 'Poly Mode',
130
+ ].map((label, value) => ({
131
+ label: String(label).length ? `${value} - ${label}` : String(value),
132
+ value,
133
+ }));
134
+ export default CC_NAMES;
@@ -0,0 +1,27 @@
1
+ const Statuses = {
2
+ noteOff: 8, // 1000
3
+ noteOn: 9, // 1001
4
+ aftertouch: 10, // 1010
5
+ controlChange: 11, // 1011
6
+ programChange: 12, // 1100
7
+ channelPressure: 13, // 1101
8
+ pitchWheel: 14, // 1110
9
+ };
10
+ const MASK_CHANNEL = 15; // 00001111
11
+ const MASK_STATUS = 240; // 01110000
12
+ const statusOptions = [
13
+ { value: Statuses.noteOn, label: 'Note On' },
14
+ { value: Statuses.noteOff, label: 'Note Stack' },
15
+ { value: Statuses.controlChange, label: 'Control Change' },
16
+ { value: Statuses.programChange, label: 'Program Change' },
17
+ { value: Statuses.aftertouch, label: 'Aftertouch' },
18
+ { value: Statuses.channelPressure, label: 'Channel Pressure' },
19
+ { value: Statuses.pitchWheel, label: 'Pitch Wheel' },
20
+ ];
21
+ const getStatusLabel = (val) => statusOptions.filter(({ value }) => value === val)[0]?.label || '???';
22
+ const combineStatusWithChannel = (channel, status) => channel | (status << 4);
23
+ const extractStatusAndChannel = (status) => ({
24
+ status: (((status & MASK_STATUS) >> 4) | 8),
25
+ channel: status & MASK_CHANNEL,
26
+ });
27
+ export { combineStatusWithChannel, extractStatusAndChannel, getStatusLabel, MASK_CHANNEL, MASK_STATUS, statusOptions, Statuses, };
@@ -8,7 +8,7 @@ const Statuses = {
8
8
  programChange: 12, // 1100
9
9
  channelPressure: 13, // 1101
10
10
  pitchWheel: 14, // 1110
11
- }
11
+ } as const
12
12
 
13
13
  type Status = (typeof Statuses)[keyof typeof Statuses]
14
14
 
@@ -49,6 +49,5 @@ export {
49
49
  MASK_STATUS,
50
50
  statusOptions,
51
51
  Statuses,
52
+ type Status,
52
53
  }
53
-
54
- export type { Status }
@@ -0,0 +1,3 @@
1
+ import { Fragment, createElement } from 'react';
2
+ const DefaultTooltip = ({ children }) => createElement(Fragment, null, children);
3
+ export default DefaultTooltip;
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { /* Midi, */ Note } from 'tonal';
3
+ import { Piano as ReactPiano } from 'zds-react-piano';
4
+ import SoundfontProvider from './SoundFontProvider';
5
+ const baseNotes = [
6
+ 'C',
7
+ 'C#',
8
+ 'D',
9
+ 'D#',
10
+ 'E',
11
+ 'F',
12
+ 'F#',
13
+ 'G',
14
+ 'G#',
15
+ 'A',
16
+ 'A#',
17
+ 'B',
18
+ ];
19
+ // const { midiToFreq, midiToNoteName } = Midi
20
+ // console.log(Note.names(), Note.midi('C4'))
21
+ // const firstNote = 21
22
+ // const lastNote = 108
23
+ const numNotes = 12;
24
+ const whiteNotes = numNotes * 0.7; // approx
25
+ const audioContext = new window.AudioContext();
26
+ const OctavePlayer = ({ octave, height, width, ...rest }) => {
27
+ const keyWidth = width / whiteNotes + 2;
28
+ const keyWidthToHeight = keyWidth / height;
29
+ const firstNote = Note.midi(baseNotes[0] + String(octave)) || 0;
30
+ const lastNote = Note.midi(baseNotes[baseNotes.length - 1] + String(octave)) || 0;
31
+ return (_jsx("div", { children: _jsx(ReactPiano, { keyWidthToHeight: keyWidthToHeight, noteRange: { first: firstNote, last: lastNote }, width: width, ...rest }) }));
32
+ };
33
+ const WithProvider = ({ format, height, hostname, instrumentName, octave = 4, onChange, onClick, onDoubleClick, onKeyMouseEnter, onKeyMouseLeave, soundfont, Tooltip, width, }) => (_jsx(SoundfontProvider, { audioContext,
34
+ format,
35
+ hostname,
36
+ instrumentName,
37
+ onChange,
38
+ soundfont, render: ({ isLoading, playNote, stopNote, }) => (_jsxs(_Fragment, { children: [_jsx("div", { children: _jsxs("h2", { children: ["Octave: ", octave] }) }), _jsx(OctavePlayer, { disabled: isLoading, height,
39
+ octave,
40
+ onClick,
41
+ onDoubleClick,
42
+ onKeyMouseEnter,
43
+ onKeyMouseLeave,
44
+ playNote,
45
+ stopNote,
46
+ Tooltip,
47
+ width })] })) }));
48
+ export default WithProvider;
@@ -0,0 +1,109 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ const MEASUREMENT_ELEMENT_ID = '__react_svg_text_measurement_id';
4
+ const calculateWordsByLines = (text, wordWidths, maxWidth, maxHeight) => {
5
+ const { lineHeight, spaceWidth, wordsWithComputedWidth } = wordWidths;
6
+ return text.split(/\s+/).reduce((result, word) => {
7
+ const wordWidth = wordsWithComputedWidth[word];
8
+ const currentLine = result[result.length - 1];
9
+ if (currentLine && currentLine.width + wordWidth + spaceWidth < maxWidth) {
10
+ // Word can be added to an existing line
11
+ currentLine.words.push(word);
12
+ currentLine.width += wordWidth + spaceWidth;
13
+ }
14
+ else {
15
+ // Add first word to line or word is too long to scaleToFit on existing line
16
+ const newLine = {
17
+ words: [word],
18
+ width: wordWidth,
19
+ showLine: maxHeight
20
+ ? lineHeight * (result.length + 1) < maxHeight
21
+ : true,
22
+ };
23
+ result.push(newLine);
24
+ }
25
+ return result;
26
+ }, []);
27
+ };
28
+ const calculateWordWidths = (style, textNode, text) => {
29
+ if (style && textNode) {
30
+ // biome-ignore lint/complexity/noForEach: <explanation>
31
+ Array.from(style).forEach(key => textNode.style.setProperty(key, style.getPropertyValue(key), style.getPropertyPriority(key)));
32
+ const wordArray = [...new Set(String(text).split(/\s+/))];
33
+ const wordsWithComputedWidth = wordArray.reduce((wordMap, word) => {
34
+ textNode.textContent = word;
35
+ // biome-ignore lint/performance/noAccumulatingSpread: <explanation>
36
+ return { ...wordMap, [word]: textNode.getBBox().width };
37
+ }, {});
38
+ textNode.textContent = '\u00A0';
39
+ const spaceWidth = textNode?.getComputedTextLength?.() || 8;
40
+ const lineHeight = textNode.getBBox().height;
41
+ textNode.setAttribute('style', '');
42
+ return { wordsWithComputedWidth, spaceWidth, lineHeight };
43
+ }
44
+ return undefined;
45
+ };
46
+ const Text = (props) => {
47
+ const { className = '', dx = 0, dy = 0, maxHeight = 1000, maxWidth = 1000,
48
+ // textAnchor = 'start',
49
+ text = '', verticalAnchor = 'start', x = 0, y = 0, transform = '', ...rest } = props;
50
+ const [wordWidths, setWordWidths] = useState();
51
+ const [textLines, setTextLines] = useState([]);
52
+ const [style, setComputedStyle] = useState();
53
+ const measureRef = useRef({});
54
+ const displayedLines = useMemo(() => {
55
+ const result = textLines.filter(({ showLine }) => showLine);
56
+ return result.length
57
+ ? result
58
+ : text
59
+ .split(/\n/)
60
+ .map(line => ({ words: [line], width: 0, showLine: true }));
61
+ }, [textLines, text]);
62
+ const ref = useCallback((node) => {
63
+ setComputedStyle(node !== null ? window.getComputedStyle(node) : undefined);
64
+ }, []);
65
+ useEffect(() => {
66
+ const el = document.getElementById(MEASUREMENT_ELEMENT_ID);
67
+ if (el === null) {
68
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
69
+ svg.setAttribute('id', MEASUREMENT_ELEMENT_ID);
70
+ document.body.appendChild(svg);
71
+ svg.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'text'));
72
+ measureRef.current = svg;
73
+ return () => {
74
+ document.body.removeChild(svg);
75
+ };
76
+ }
77
+ measureRef.current = el;
78
+ }, []);
79
+ useEffect(() => {
80
+ const wordWithWidths = calculateWordWidths(style, measureRef.current, text);
81
+ setWordWidths(wordWithWidths);
82
+ }, [text, style]);
83
+ useEffect(() => {
84
+ if (maxWidth && wordWidths) {
85
+ const lines = text
86
+ .split(/\n/)
87
+ .flatMap(line => calculateWordsByLines(line, wordWidths, maxWidth, maxHeight));
88
+ setTextLines(lines);
89
+ }
90
+ }, [maxHeight, maxWidth, wordWidths, text]);
91
+ const startDy = useMemo(() => {
92
+ if (wordWidths?.lineHeight) {
93
+ switch (verticalAnchor) {
94
+ case 'start':
95
+ return wordWidths.lineHeight;
96
+ case 'middle':
97
+ return -(((displayedLines.length - 1) * wordWidths.lineHeight) / 2);
98
+ default:
99
+ return -(displayedLines.length - 1) * wordWidths.lineHeight;
100
+ }
101
+ }
102
+ return 0;
103
+ }, [displayedLines.length, verticalAnchor, wordWidths?.lineHeight]);
104
+ return (_jsx("text", { className: className, ref: ref, dx: dx, dy: dy, x: x, y: y,
105
+ // textAnchor={textAnchor}
106
+ transform: transform, ...rest, children: _jsxs(_Fragment, { children: [displayedLines.map((line, idx) => (_jsx("tspan", { dx: x + dx, dy: idx === 0 ? startDy + dy : wordWidths?.lineHeight || 0, x: 0, children: line.words.join(' ') }, idx))), textLines.length && displayedLines.length !== textLines.length && (_jsxs(_Fragment, { children: [_jsx("tspan", { children: "..." }), _jsx("title", { children: text })] }))] }) }));
107
+ };
108
+ export default Text;
109
+ export { MEASUREMENT_ELEMENT_ID };
@@ -0,0 +1,87 @@
1
+ // See https://github.com/danigb/soundfont-player
2
+ // for more documentation on prop options.
3
+ import { useCallback, useEffect, useState } from 'react';
4
+ import Soundfont from 'soundfont-player';
5
+ // import '../temp/acoustic_grand_piano-mp3'
6
+ const isDefined = (item) => item !== undefined && item !== null && !Number.isNaN(item);
7
+ const SoundfontProvider = (props) => {
8
+ const { audioContext, format, hostname, instrumentName, onChange, render, soundfont, } = props;
9
+ const [isLoading, setIsLoading] = useState(true);
10
+ const [activeAudioNodes, setActiveAudioNodes] = useState({});
11
+ const [instrument, setInstrument] = useState(null);
12
+ const loadInstrument = useCallback((nm) => {
13
+ // Re-trigger loading state
14
+ setInstrument(null);
15
+ if (isDefined(hostname)) {
16
+ // pull from an url
17
+ Soundfont.instrument(audioContext, nm, {
18
+ format,
19
+ soundfont,
20
+ nameToUrl: (name, sf, fmt) => `${hostname}/${sf}/${name}-${fmt}.js`,
21
+ }).then(newInstrument => {
22
+ setInstrument(newInstrument);
23
+ });
24
+ }
25
+ else if (isDefined(instrumentName)) {
26
+ // the host app has already injected the soundfont into global space
27
+ Soundfont.instrument(audioContext, instrumentName).then(newInstrument => {
28
+ setInstrument(newInstrument);
29
+ });
30
+ }
31
+ else {
32
+ // sound must be disabled
33
+ setIsLoading(false);
34
+ }
35
+ }, [audioContext, format, hostname, instrumentName, soundfont]);
36
+ useEffect(() => {
37
+ loadInstrument(instrumentName);
38
+ }, [instrumentName, loadInstrument]);
39
+ useEffect(() => {
40
+ if (isDefined(instrumentName)) {
41
+ setIsLoading(!instrument);
42
+ }
43
+ }, [instrument, instrumentName]);
44
+ const playNote = (midiNumber) => {
45
+ if (instrument) {
46
+ audioContext.resume().then(() => {
47
+ const audioNode = instrument.play(String(midiNumber));
48
+ setActiveAudioNodes(prev => ({ ...prev, [midiNumber]: audioNode }));
49
+ });
50
+ }
51
+ };
52
+ const stopNote = (midiNumber) => {
53
+ audioContext.resume().then(() => {
54
+ if (!activeAudioNodes[midiNumber]) {
55
+ return;
56
+ }
57
+ const audioNode = activeAudioNodes[midiNumber];
58
+ audioNode.stop();
59
+ setActiveAudioNodes(prev => {
60
+ const { [midiNumber]: _, ...rest } = prev;
61
+ return rest;
62
+ });
63
+ });
64
+ onChange?.(midiNumber);
65
+ };
66
+ // Clear any residual notes that don't get called with stopNote
67
+ const stopAllNotes = () => {
68
+ audioContext.resume().then(() => {
69
+ const nodes = Object.values(activeAudioNodes);
70
+ for (const node of nodes) {
71
+ if (node) {
72
+ node.stop();
73
+ }
74
+ }
75
+ setActiveAudioNodes(nodes);
76
+ });
77
+ };
78
+ return render({
79
+ isLoading,
80
+ playNote,
81
+ stopNote,
82
+ stopAllNotes,
83
+ });
84
+ };
85
+ // https://gleitz.github.io/midi-js-soundfonts/MusyngKite/acoustic_grand_piano-mp3.js
86
+ // https://gleitz.github.io/midi-js-soundfonts/FluidR3_GM/.js
87
+ export default SoundfontProvider;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef } from 'react';
3
+ import ccValues from '../midi/ccValues';
4
+ import Select from './Select';
5
+ const CCPicker = forwardRef((props, ref) => (_jsx(Select, { ...props, options: ccValues, ref: ref })));
6
+ export default CCPicker;
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import cl from 'classnames';
3
+ import { forwardRef, useCallback, useEffect, useState } from 'react';
4
+ import { arraySequence } from '../utils';
5
+ import Select from './Select';
6
+ const defaultOptions = arraySequence(16).map(value => ({
7
+ value,
8
+ label: value,
9
+ }));
10
+ const ChannelMappingPicker = forwardRef(({ channels, className, ...rest }, ref) => {
11
+ const [options, setOptions] = useState([]);
12
+ const formatOptionLabel = useCallback(({ value, label }, { context }) => {
13
+ if (value === 'separator') {
14
+ return _jsx("span", { className: "zds-mappings-user-mapping", children: label });
15
+ }
16
+ if (typeof value !== 'number')
17
+ return null;
18
+ const mapping = channels[value];
19
+ return mapping ? (_jsxs("span", { className: "mapping-entry", children: [_jsxs("b", { children: [value + 1, context === 'value' && ' -'] }), _jsx("span", { className: "mapping-entry-label", children: mapping.label })] })) : (_jsxs("span", { className: "mapping-entry empty-mapping-entry", children: [_jsxs("b", { children: [value + 1, context === 'value' && ' -'] }), _jsx("span", { className: "mapping-entry-label", children: "no mapping" })] }));
20
+ }, [channels]);
21
+ useEffect(() => {
22
+ const result = defaultOptions.sort((a, b) => {
23
+ if (typeof a.value === 'number' &&
24
+ typeof b.value === 'number' &&
25
+ channels[a.value] &&
26
+ !channels[b.value]) {
27
+ return -1;
28
+ }
29
+ if (typeof a.value === 'number' &&
30
+ typeof b.value === 'number' &&
31
+ !channels[a.value] &&
32
+ channels[b.value]) {
33
+ return 1;
34
+ }
35
+ if (typeof a.value === 'number' && typeof b.value === 'number') {
36
+ return a.value - b.value;
37
+ }
38
+ return 0;
39
+ });
40
+ const numFilled = result.filter(({ value }) => typeof value === 'number' && !!channels[value]).length;
41
+ const separatorExists = result.some(({ value }) => value === 'separator');
42
+ if (numFilled && numFilled < 16 && !separatorExists) {
43
+ // insert a separator after the count of filled items
44
+ result.splice(numFilled, 0, { label: _jsx("hr", {}), value: 'separator' });
45
+ }
46
+ setOptions(result);
47
+ }, [channels]);
48
+ return (_jsx(Select, { ...rest, className: cl(className, 'channel-mapping-picker'), formatOptionLabel: formatOptionLabel, options: options, ref: ref }));
49
+ });
50
+ export default ChannelMappingPicker;
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import cl from 'classnames';
3
+ import { forwardRef } from 'react';
4
+ import { arraySequence } from '../utils';
5
+ import Select from './Select';
6
+ const options = arraySequence(16).map(value => ({
7
+ value: value,
8
+ label: `Channel ${value + 1}`,
9
+ }));
10
+ const formatOptionLabel = (option, { context }) => context === 'value' ? option.label : Number.parseInt(String(option.value)) + 1; // menu, e.g. the list of options
11
+ const filterOptions = (option, input) => {
12
+ // console.log(option, input)
13
+ if (input) {
14
+ return String(option.label).toLowerCase().includes(input.toLowerCase());
15
+ }
16
+ return true;
17
+ };
18
+ const ChannelPicker = forwardRef(({ className, ...rest }, ref) => {
19
+ return (_jsx(Select, { ...rest, className: cl(className, 'channel-picker'), filterOptions: filterOptions, formatOptionLabel: formatOptionLabel, options: options, ref: ref }));
20
+ });
21
+ export default ChannelPicker;
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback } from 'react';
3
+ import useStateWithDynamicDefault from '../hooks/useStateWithDynamicDefault';
4
+ import RotaryKnob from '../rotaryKnob';
5
+ import { assertRange } from '../utils';
6
+ import knobSkin10 from './knobSkin10';
7
+ const Knob = (props) => {
8
+ const { disabled = false, max = 127, min = 0, onChange, value: initialValue = 0, wheelEnabled = false, wheelSensitivity = 0.1, ...rest } = props;
9
+ const [value, setValue] = useStateWithDynamicDefault(initialValue);
10
+ const handleChange = (val) => {
11
+ if (disabled)
12
+ return;
13
+ const newValue = assertRange(Number.parseInt(String(val), 10), max, min);
14
+ setValue(newValue);
15
+ if (onChange) {
16
+ onChange(newValue);
17
+ }
18
+ };
19
+ const handleWheel = useCallback((event) => {
20
+ if (!wheelEnabled || disabled)
21
+ return;
22
+ event.preventDefault();
23
+ const delta = -event.deltaY * wheelSensitivity;
24
+ const newValue = assertRange(value + delta, max, min);
25
+ setValue(newValue);
26
+ if (onChange) {
27
+ onChange(newValue);
28
+ }
29
+ }, [
30
+ wheelEnabled,
31
+ disabled,
32
+ wheelSensitivity,
33
+ value,
34
+ max,
35
+ min,
36
+ onChange,
37
+ setValue,
38
+ ]);
39
+ return (_jsx(RotaryKnob, { clampMax: 320, clampMin: 40, className: "zds-pickers__knob-container", disabled: disabled, onChange: handleChange, onWheel: wheelEnabled ? handleWheel : undefined, preciseMode: false, rotateDegrees: 180,
40
+ /**
41
+ * To see all skins:
42
+ * http://react-rotary-knob-skins-preview.surge.sh/
43
+ *
44
+ * NOTE: we've copied the one we use into this package because the skins
45
+ * pack package does not work in Electron.
46
+ */
47
+ skin: knobSkin10, style: { opacity: disabled ? 0.4 : 1 }, unlockDistance: 0, defaultValue: value, max, min, value, ...rest }));
48
+ };
49
+ export default Knob;