zds-pickers 4.0.3 → 4.0.6

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 (101) hide show
  1. package/.editorconfig +19 -0
  2. package/.github/workflows/codeql-analysis.yml +67 -0
  3. package/.nvmrc +1 -0
  4. package/.prettierignore +0 -0
  5. package/.prettierrc +13 -0
  6. package/.storybook/main.ts +42 -0
  7. package/.storybook/manager.ts +12 -0
  8. package/.storybook/preview.tsx +26 -0
  9. package/.vscode/extensions.json +32 -0
  10. package/.vscode/javascript.code-snippets +229 -0
  11. package/.vscode/settings.json +358 -0
  12. package/biome.jsonc +158 -0
  13. package/dist/index.cjs.js +15 -32
  14. package/dist/index.es.js +4144 -10129
  15. package/dist/types/hooks/useComponentSize.d.ts +5 -0
  16. package/dist/types/hooks/useStateWithDynamicDefault.d.ts +2 -0
  17. package/dist/types/index.d.ts +266 -2
  18. package/dist/types/main.d.ts +19 -0
  19. package/dist/types/midi/ccValues.d.ts +3 -0
  20. package/dist/types/midi/export.d.ts +22 -0
  21. package/dist/types/other/DefaultTooltip.d.ts +10 -0
  22. package/dist/types/other/OctavePlayer.d.ts +20 -0
  23. package/dist/types/other/SVGText.d.ts +17 -0
  24. package/dist/types/other/SoundFontProvider.d.ts +17 -0
  25. package/dist/types/pickers/CCPicker.d.ts +7 -0
  26. package/dist/types/pickers/ChannelMappingPicker.d.ts +14 -0
  27. package/dist/types/pickers/ChannelPicker.d.ts +8 -0
  28. package/dist/types/pickers/Knob.d.ts +12 -0
  29. package/dist/types/pickers/KnobPicker.d.ts +27 -0
  30. package/dist/types/pickers/LatchPicker.d.ts +8 -0
  31. package/dist/types/pickers/MappingPicker.d.ts +11 -0
  32. package/dist/types/pickers/NotePicker.d.ts +20 -0
  33. package/dist/types/pickers/PianoPicker.d.ts +9 -0
  34. package/dist/types/pickers/PolarityPicker.d.ts +14 -0
  35. package/dist/types/pickers/ResponseCurve.d.ts +40 -0
  36. package/dist/types/pickers/ResponseCurvePicker.d.ts +15 -0
  37. package/dist/types/pickers/Select.d.ts +29 -0
  38. package/dist/types/pickers/StatusPicker.d.ts +12 -0
  39. package/dist/types/pickers/ValuePicker.d.ts +15 -0
  40. package/dist/types/pickers/knobSkin10.d.ts +14 -0
  41. package/dist/types/rotaryKnob/InternalInput.d.ts +18 -0
  42. package/dist/types/rotaryKnob/Types.d.ts +5 -0
  43. package/dist/types/rotaryKnob/helpers/DrawCircle.d.ts +18 -0
  44. package/dist/types/rotaryKnob/helpers/DrawLine.d.ts +15 -0
  45. package/dist/types/rotaryKnob/helpers/HelpersOverlay.d.ts +11 -0
  46. package/dist/types/rotaryKnob/helpers/KnobVisualHelpers.d.ts +30 -0
  47. package/dist/types/rotaryKnob/index.d.ts +49 -0
  48. package/dist/types/rotaryKnob/knobdefaultskin.d.ts +6 -0
  49. package/dist/types/rotaryKnob/utils.d.ts +49 -0
  50. package/dist/types/utils.d.ts +9 -0
  51. package/lib/@types/nebiru-react-rotary-knob.d.ts +20 -0
  52. package/lib/@types/react-svgmt.d.ts +74 -0
  53. package/lib/@types/zds-mappings.d.ts +55 -0
  54. package/lib/@types/zds-react-piano.d.ts +17 -0
  55. package/lib/hooks/useComponentSize.ts +52 -0
  56. package/lib/hooks/useStateWithDynamicDefault.ts +13 -0
  57. package/lib/main.ts +34 -0
  58. package/lib/midi/ccValues.ts +139 -0
  59. package/lib/midi/export.ts +54 -0
  60. package/lib/other/DefaultTooltip.ts +14 -0
  61. package/lib/other/OctavePlayer.tsx +141 -0
  62. package/lib/other/SVGText.tsx +221 -0
  63. package/lib/other/SoundFontProvider.tsx +130 -0
  64. package/lib/pickers/CCPicker.tsx +28 -0
  65. package/lib/pickers/ChannelMappingPicker.tsx +135 -0
  66. package/lib/pickers/ChannelPicker.tsx +76 -0
  67. package/lib/pickers/Knob.tsx +93 -0
  68. package/lib/pickers/KnobPicker.tsx +93 -0
  69. package/lib/pickers/LatchPicker.tsx +33 -0
  70. package/lib/pickers/MappingPicker.tsx +68 -0
  71. package/lib/pickers/NotePicker.tsx +95 -0
  72. package/lib/pickers/PianoPicker.tsx +77 -0
  73. package/lib/pickers/PolarityPicker.tsx +39 -0
  74. package/lib/pickers/ResponseCurve.tsx +304 -0
  75. package/lib/pickers/ResponseCurvePicker.tsx +74 -0
  76. package/lib/pickers/Select.tsx +248 -0
  77. package/lib/pickers/StatusPicker.tsx +28 -0
  78. package/lib/pickers/ValuePicker.tsx +67 -0
  79. package/lib/pickers/knobSkin10.tsx +94 -0
  80. package/lib/rotaryKnob/InternalInput.tsx +43 -0
  81. package/lib/rotaryKnob/Types.ts +6 -0
  82. package/lib/rotaryKnob/helpers/DrawCircle.tsx +41 -0
  83. package/lib/rotaryKnob/helpers/DrawLine.tsx +32 -0
  84. package/lib/rotaryKnob/helpers/HelpersOverlay.tsx +43 -0
  85. package/lib/rotaryKnob/helpers/KnobVisualHelpers.tsx +138 -0
  86. package/lib/rotaryKnob/index.tsx +417 -0
  87. package/lib/rotaryKnob/knobdefaultskin.ts +56 -0
  88. package/lib/rotaryKnob/utils.ts +112 -0
  89. package/lib/temp/acoustic_grand_piano-mp3.js +94 -0
  90. package/lib/utils.ts +41 -0
  91. package/package.json +3 -7
  92. package/stories/assets/sampleStyles.css +349 -0
  93. package/stories/assets/utils.js +7 -0
  94. package/stories/picker.stories.tsx +313 -0
  95. package/tsconfig.app.json +26 -0
  96. package/tsconfig.json +34 -0
  97. package/tsconfig.node.json +24 -0
  98. package/vite.config.ts +45 -0
  99. package/dist/index.cjs.js.map +0 -1
  100. package/dist/index.d.ts +0 -244
  101. package/dist/index.es.js.map +0 -1
@@ -0,0 +1,54 @@
1
+ import type { Option } from 'lib/pickers/Select'
2
+
3
+ const Statuses = {
4
+ noteOff: 8, // 1000
5
+ noteOn: 9, // 1001
6
+ aftertouch: 10, // 1010
7
+ controlChange: 11, // 1011
8
+ programChange: 12, // 1100
9
+ channelPressure: 13, // 1101
10
+ pitchWheel: 14, // 1110
11
+ }
12
+
13
+ type Status = (typeof Statuses)[keyof typeof Statuses]
14
+
15
+ const MASK_CHANNEL = 15 // 00001111
16
+ const MASK_STATUS = 240 // 01110000
17
+
18
+ const statusOptions: Option<Status>[] = [
19
+ { value: Statuses.noteOn, label: 'Note On' },
20
+ { value: Statuses.noteOff, label: 'Note Stack' },
21
+ { value: Statuses.controlChange, label: 'Control Change' },
22
+ { value: Statuses.programChange, label: 'Program Change' },
23
+ { value: Statuses.aftertouch, label: 'Aftertouch' },
24
+ { value: Statuses.channelPressure, label: 'Channel Pressure' },
25
+ { value: Statuses.pitchWheel, label: 'Pitch Wheel' },
26
+ ]
27
+
28
+ const getStatusLabel = (val: Status) =>
29
+ statusOptions.filter(({ value }) => value === val)[0]?.label || '???'
30
+
31
+ const combineStatusWithChannel = (channel: number, status: Status) =>
32
+ channel | (status << 4)
33
+
34
+ const extractStatusAndChannel = (
35
+ status: number,
36
+ ): {
37
+ status: Status
38
+ channel: number
39
+ } => ({
40
+ status: (((status & MASK_STATUS) >> 4) | 8) as Status,
41
+ channel: status & MASK_CHANNEL,
42
+ })
43
+
44
+ export {
45
+ combineStatusWithChannel,
46
+ extractStatusAndChannel,
47
+ getStatusLabel,
48
+ MASK_CHANNEL,
49
+ MASK_STATUS,
50
+ statusOptions,
51
+ Statuses,
52
+ }
53
+
54
+ export type { Status }
@@ -0,0 +1,14 @@
1
+ import { Fragment, createElement } from 'react'
2
+
3
+ type TooltipProps = {
4
+ children?: React.ReactNode
5
+ placement?: 'left' | 'right' | 'top' | 'bottom'
6
+ title: React.ReactNode
7
+ }
8
+
9
+ const DefaultTooltip = ({ children }: React.PropsWithoutRef<TooltipProps>) =>
10
+ createElement(Fragment, null, children)
11
+
12
+ export default DefaultTooltip
13
+
14
+ export type { TooltipProps }
@@ -0,0 +1,141 @@
1
+ import type Soundfont from 'soundfont-player'
2
+ import { /* Midi, */ Note } from 'tonal'
3
+ import { Piano as ReactPiano } from 'zds-react-piano'
4
+ import type { TooltipProps } from './DefaultTooltip'
5
+ import SoundfontProvider from './SoundFontProvider'
6
+
7
+ const baseNotes = [
8
+ 'C',
9
+ 'C#',
10
+ 'D',
11
+ 'D#',
12
+ 'E',
13
+ 'F',
14
+ 'F#',
15
+ 'G',
16
+ 'G#',
17
+ 'A',
18
+ 'A#',
19
+ 'B',
20
+ ]
21
+
22
+ // const { midiToFreq, midiToNoteName } = Midi
23
+
24
+ // console.log(Note.names(), Note.midi('C4'))
25
+
26
+ // const firstNote = 21
27
+ // const lastNote = 108
28
+ const numNotes = 12
29
+
30
+ const whiteNotes = numNotes * 0.7 // approx
31
+
32
+ const audioContext = new window.AudioContext()
33
+
34
+ type OctavePlayerProps = {
35
+ disabled?: boolean
36
+ height: number
37
+ octave: number
38
+ width: number
39
+ }
40
+
41
+ const OctavePlayer = ({
42
+ octave,
43
+ height,
44
+ width,
45
+ ...rest
46
+ }: OctavePlayerProps) => {
47
+ const keyWidth = width / whiteNotes + 2
48
+ const keyWidthToHeight = keyWidth / height
49
+
50
+ const firstNote = Note.midi(baseNotes[0] + String(octave)) || 0
51
+ const lastNote =
52
+ Note.midi(baseNotes[baseNotes.length - 1] + String(octave)) || 0
53
+
54
+ return (
55
+ <div>
56
+ <ReactPiano
57
+ keyWidthToHeight={keyWidthToHeight}
58
+ noteRange={{ first: firstNote, last: lastNote }}
59
+ width={width}
60
+ {...rest}
61
+ />
62
+ </div>
63
+ )
64
+ }
65
+
66
+ type WithProviderProps = {
67
+ format?: 'mp3' | 'ogg'
68
+ height: number
69
+ hostname?: string
70
+ instrumentName: Soundfont.InstrumentName
71
+ octave?: number
72
+ onChange?: (note: number) => void
73
+ onClick?: (note: number) => void
74
+ onDoubleClick?: (note: number) => void
75
+ onKeyMouseEnter?: (note: number) => void
76
+ onKeyMouseLeave?: (note: number) => void
77
+ soundfont?: 'MusyngKite' | 'FluidR3_GM'
78
+ Tooltip?: TooltipProps
79
+ width: number
80
+ }
81
+
82
+ const WithProvider = ({
83
+ format,
84
+ height,
85
+ hostname,
86
+ instrumentName,
87
+ octave = 4,
88
+ onChange,
89
+ onClick,
90
+ onDoubleClick,
91
+ onKeyMouseEnter,
92
+ onKeyMouseLeave,
93
+ soundfont,
94
+ Tooltip,
95
+ width,
96
+ }: WithProviderProps) => (
97
+ <SoundfontProvider
98
+ {...{
99
+ audioContext,
100
+ format,
101
+ hostname,
102
+ instrumentName,
103
+ onChange,
104
+ soundfont,
105
+ }}
106
+ render={({
107
+ isLoading,
108
+ playNote,
109
+ stopNote,
110
+ }: {
111
+ isLoading: boolean
112
+ playNote: (midiNumber: number) => void
113
+ stopNote: (midiNumber: number) => void
114
+ }) => (
115
+ <>
116
+ <div>
117
+ <h2>Octave: {octave}</h2>
118
+ </div>
119
+ <OctavePlayer
120
+ disabled={isLoading}
121
+ {...{
122
+ height,
123
+ octave,
124
+ onClick,
125
+ onDoubleClick,
126
+ onKeyMouseEnter,
127
+ onKeyMouseLeave,
128
+ playNote,
129
+ stopNote,
130
+ Tooltip,
131
+ width,
132
+ }}
133
+ />
134
+ </>
135
+ )}
136
+ />
137
+ )
138
+
139
+ export default WithProvider
140
+
141
+ export type { WithProviderProps }
@@ -0,0 +1,221 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
+
3
+ const MEASUREMENT_ELEMENT_ID = '__react_svg_text_measurement_id'
4
+
5
+ type WordsByLine = {
6
+ words: string[]
7
+ width: number
8
+ showLine: boolean
9
+ }
10
+
11
+ const calculateWordsByLines = (
12
+ text: string,
13
+ wordWidths: WordWidths,
14
+ maxWidth: number,
15
+ maxHeight: number,
16
+ ) => {
17
+ const { lineHeight, spaceWidth, wordsWithComputedWidth } = wordWidths
18
+
19
+ return text.split(/\s+/).reduce((result: WordsByLine[], word) => {
20
+ const wordWidth = wordsWithComputedWidth[word]
21
+ const currentLine = result[result.length - 1]
22
+ if (currentLine && currentLine.width + wordWidth + spaceWidth < maxWidth) {
23
+ // Word can be added to an existing line
24
+ currentLine.words.push(word)
25
+ currentLine.width += wordWidth + spaceWidth
26
+ } else {
27
+ // Add first word to line or word is too long to scaleToFit on existing line
28
+ const newLine: WordsByLine = {
29
+ words: [word],
30
+ width: wordWidth,
31
+ showLine: maxHeight
32
+ ? lineHeight * (result.length + 1) < maxHeight
33
+ : true,
34
+ }
35
+ result.push(newLine)
36
+ }
37
+ return result
38
+ }, [])
39
+ }
40
+
41
+ type WordWidths = {
42
+ wordsWithComputedWidth: Record<string, number>
43
+ spaceWidth: number
44
+ lineHeight: number
45
+ }
46
+
47
+ const calculateWordWidths = (
48
+ style?: CSSStyleDeclaration,
49
+ textNode?: SVGGraphicsElement | null,
50
+ text?: string,
51
+ ): WordWidths | undefined => {
52
+ if (style && textNode) {
53
+ // biome-ignore lint/complexity/noForEach: <explanation>
54
+ Array.from(style).forEach(key =>
55
+ textNode.style.setProperty(
56
+ key,
57
+ style.getPropertyValue(key),
58
+ style.getPropertyPriority(key),
59
+ ),
60
+ )
61
+ const wordArray = [...new Set(String(text).split(/\s+/))]
62
+ const wordsWithComputedWidth = wordArray.reduce((wordMap, word) => {
63
+ textNode.textContent = word
64
+ // biome-ignore lint/performance/noAccumulatingSpread: <explanation>
65
+ return { ...wordMap, [word]: textNode.getBBox().width }
66
+ }, {})
67
+
68
+ textNode.textContent = '\u00A0'
69
+
70
+ const spaceWidth =
71
+ (textNode as SVGTextContentElement)?.getComputedTextLength?.() || 8
72
+ const lineHeight = textNode.getBBox().height
73
+
74
+ textNode.setAttribute('style', '')
75
+ return { wordsWithComputedWidth, spaceWidth, lineHeight }
76
+ }
77
+ return undefined
78
+ }
79
+
80
+ type TextProps = Partial<HTMLOrSVGElement> & {
81
+ className?: string
82
+ dx?: number
83
+ dy?: number
84
+ maxHeight?: number
85
+ maxWidth?: number
86
+ transform?: string
87
+ text: string
88
+ textAnchor?: React.CSSProperties['textAnchor']
89
+ verticalAnchor?: 'start' | 'middle' | 'end'
90
+ x?: number
91
+ y?: number
92
+ }
93
+
94
+ const Text = (props: TextProps) => {
95
+ const {
96
+ className = '',
97
+ dx = 0,
98
+ dy = 0,
99
+ maxHeight = 1000,
100
+ maxWidth = 1000,
101
+ // textAnchor = 'start',
102
+ text = '',
103
+ verticalAnchor = 'start',
104
+ x = 0,
105
+ y = 0,
106
+ transform = '',
107
+ ...rest
108
+ } = props
109
+ const [wordWidths, setWordWidths] = useState<WordWidths>()
110
+ const [textLines, setTextLines] = useState<WordsByLine[]>([])
111
+ const [style, setComputedStyle] = useState<CSSStyleDeclaration>()
112
+ const measureRef = useRef<SVGSVGElement | HTMLElement>({} as HTMLElement)
113
+
114
+ const displayedLines: WordsByLine[] = useMemo(() => {
115
+ const result = textLines.filter(({ showLine }) => showLine)
116
+ return result.length
117
+ ? result
118
+ : text
119
+ .split(/\n/)
120
+ .map(line => ({ words: [line], width: 0, showLine: true }))
121
+ }, [textLines, text])
122
+
123
+ const ref: React.LegacyRef<SVGTextElement> = useCallback(
124
+ (node: SVGTextElement | null) => {
125
+ setComputedStyle(
126
+ node !== null ? window.getComputedStyle(node) : undefined,
127
+ )
128
+ },
129
+ [],
130
+ )
131
+
132
+ useEffect(() => {
133
+ const el = document.getElementById(MEASUREMENT_ELEMENT_ID)
134
+ if (el === null) {
135
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
136
+ svg.setAttribute('id', MEASUREMENT_ELEMENT_ID)
137
+ document.body.appendChild(svg)
138
+ svg.appendChild(
139
+ document.createElementNS('http://www.w3.org/2000/svg', 'text'),
140
+ )
141
+ measureRef.current = svg
142
+ return () => {
143
+ document.body.removeChild(svg)
144
+ }
145
+ }
146
+ measureRef.current = el
147
+ }, [])
148
+
149
+ useEffect(() => {
150
+ const wordWithWidths = calculateWordWidths(
151
+ style,
152
+ measureRef.current as SVGGraphicsElement | null,
153
+ text,
154
+ )
155
+
156
+ setWordWidths(wordWithWidths)
157
+ }, [text, style])
158
+
159
+ useEffect(() => {
160
+ if (maxWidth && wordWidths) {
161
+ const lines = text
162
+ .split(/\n/)
163
+ .flatMap(line =>
164
+ calculateWordsByLines(line, wordWidths, maxWidth, maxHeight),
165
+ )
166
+
167
+ setTextLines(lines)
168
+ }
169
+ }, [maxHeight, maxWidth, wordWidths, text])
170
+
171
+ const startDy = useMemo(() => {
172
+ if (wordWidths?.lineHeight) {
173
+ switch (verticalAnchor) {
174
+ case 'start':
175
+ return wordWidths.lineHeight
176
+
177
+ case 'middle':
178
+ return -(((displayedLines.length - 1) * wordWidths.lineHeight) / 2)
179
+
180
+ default:
181
+ return -(displayedLines.length - 1) * wordWidths.lineHeight
182
+ }
183
+ }
184
+ return 0
185
+ }, [displayedLines.length, verticalAnchor, wordWidths?.lineHeight])
186
+
187
+ return (
188
+ <text
189
+ className={className}
190
+ ref={ref}
191
+ dx={dx}
192
+ dy={dy}
193
+ x={x}
194
+ y={y}
195
+ // textAnchor={textAnchor}
196
+ transform={transform}
197
+ {...rest}>
198
+ <>
199
+ {displayedLines.map((line, idx) => (
200
+ <tspan
201
+ dx={x + dx}
202
+ dy={idx === 0 ? startDy + dy : wordWidths?.lineHeight || 0}
203
+ key={idx}
204
+ x={0}>
205
+ {line.words.join(' ')}
206
+ </tspan>
207
+ ))}
208
+ {textLines.length && displayedLines.length !== textLines.length && (
209
+ <>
210
+ <tspan>...</tspan>
211
+ <title>{text}</title>
212
+ </>
213
+ )}
214
+ </>
215
+ </text>
216
+ )
217
+ }
218
+
219
+ export default Text
220
+
221
+ export { MEASUREMENT_ELEMENT_ID }
@@ -0,0 +1,130 @@
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
+
7
+ const isDefined = <T,>(item: T) =>
8
+ item !== undefined && item !== null && !Number.isNaN(item)
9
+
10
+ type SoundfontProviderProps = {
11
+ audioContext: AudioContext
12
+ format?: 'mp3' | 'ogg'
13
+ hostname?: string
14
+ instrumentName: Soundfont.InstrumentName
15
+ onChange?: (midiNumber: number) => void
16
+ render: (props: {
17
+ isLoading: boolean
18
+ playNote: (midiNumber: number) => void
19
+ stopNote: (midiNumber: number) => void
20
+ stopAllNotes: () => void
21
+ }) => React.ReactNode
22
+ soundfont?: 'MusyngKite' | 'FluidR3_GM'
23
+ }
24
+
25
+ const SoundfontProvider = (props: SoundfontProviderProps) => {
26
+ const {
27
+ audioContext,
28
+ format,
29
+ hostname,
30
+ instrumentName,
31
+ onChange,
32
+ render,
33
+ soundfont,
34
+ } = props
35
+
36
+ const [isLoading, setIsLoading] = useState(true)
37
+ const [activeAudioNodes, setActiveAudioNodes] = useState<
38
+ Record<number, Soundfont.Player>
39
+ >({})
40
+ const [instrument, setInstrument] = useState<Soundfont.Player | null>(null)
41
+
42
+ const loadInstrument = useCallback(
43
+ (nm: Soundfont.InstrumentName) => {
44
+ // Re-trigger loading state
45
+ setInstrument(null)
46
+
47
+ if (isDefined(hostname)) {
48
+ // pull from an url
49
+ Soundfont.instrument(audioContext, nm, {
50
+ format,
51
+ soundfont,
52
+ nameToUrl: (name: string, sf: string, fmt: string) =>
53
+ `${hostname}/${sf}/${name}-${fmt}.js`,
54
+ }).then(newInstrument => {
55
+ setInstrument(newInstrument)
56
+ })
57
+ } else if (isDefined(instrumentName)) {
58
+ // the host app has already injected the soundfont into global space
59
+ Soundfont.instrument(audioContext, instrumentName).then(
60
+ newInstrument => {
61
+ setInstrument(newInstrument)
62
+ },
63
+ )
64
+ } else {
65
+ // sound must be disabled
66
+ setIsLoading(false)
67
+ }
68
+ },
69
+ [audioContext, format, hostname, instrumentName, soundfont],
70
+ )
71
+
72
+ useEffect(() => {
73
+ loadInstrument(instrumentName)
74
+ }, [instrumentName, loadInstrument])
75
+
76
+ useEffect(() => {
77
+ if (isDefined(instrumentName)) {
78
+ setIsLoading(!instrument)
79
+ }
80
+ }, [instrument, instrumentName])
81
+
82
+ const playNote = (midiNumber: number) => {
83
+ if (instrument) {
84
+ audioContext.resume().then(() => {
85
+ const audioNode = instrument.play(String(midiNumber))
86
+ setActiveAudioNodes(prev => ({ ...prev, [midiNumber]: audioNode }))
87
+ })
88
+ }
89
+ }
90
+
91
+ const stopNote = (midiNumber: number) => {
92
+ audioContext.resume().then(() => {
93
+ if (!activeAudioNodes[midiNumber]) {
94
+ return
95
+ }
96
+ const audioNode = activeAudioNodes[midiNumber]
97
+ audioNode.stop()
98
+ setActiveAudioNodes(prev => {
99
+ const { [midiNumber]: _, ...rest } = prev
100
+ return rest
101
+ })
102
+ })
103
+ onChange?.(midiNumber)
104
+ }
105
+
106
+ // Clear any residual notes that don't get called with stopNote
107
+ const stopAllNotes = () => {
108
+ audioContext.resume().then(() => {
109
+ const nodes = Object.values(activeAudioNodes)
110
+ for (const node of nodes) {
111
+ if (node) {
112
+ node.stop()
113
+ }
114
+ }
115
+ setActiveAudioNodes(nodes)
116
+ })
117
+ }
118
+
119
+ return render({
120
+ isLoading,
121
+ playNote,
122
+ stopNote,
123
+ stopAllNotes,
124
+ })
125
+ }
126
+
127
+ // https://gleitz.github.io/midi-js-soundfonts/MusyngKite/acoustic_grand_piano-mp3.js
128
+ // https://gleitz.github.io/midi-js-soundfonts/FluidR3_GM/.js
129
+
130
+ export default SoundfontProvider
@@ -0,0 +1,28 @@
1
+ import { forwardRef } from 'react'
2
+ import type { GroupBase, SelectInstance } from 'react-select'
3
+ import ccValues from '../midi/ccValues'
4
+ import Select from './Select'
5
+ import type { Option, SelectProps } from './Select'
6
+
7
+ type CCPickerProps = SelectProps<number>
8
+
9
+ type CCPickerRef = SelectInstance<
10
+ Option<number>,
11
+ false,
12
+ GroupBase<Option<number>>
13
+ >
14
+
15
+ const CCPicker = forwardRef<
16
+ SelectInstance<Option<number>, false, GroupBase<Option<number>>>,
17
+ CCPickerProps
18
+ >((props, ref) => (
19
+ <Select
20
+ {...props}
21
+ options={ccValues}
22
+ ref={ref}
23
+ />
24
+ ))
25
+
26
+ export default CCPicker
27
+
28
+ export type { CCPickerProps, CCPickerRef }