zds-pickers 4.0.24 → 4.1.0
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.
- package/README.md +199 -2
- package/dist/index.cjs.js +40 -15
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +3108 -10452
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/other/OctavePlayer.js +13 -12
- package/dist/other/SoundFontProvider.js +64 -4
- package/dist/pickers/KeyPicker.js +20 -0
- package/dist/pickers/PianoPicker.js +2 -1
- package/dist/pickers/knobSkin10.js +18 -0
- package/dist/rotaryKnob/knobdefaultskin.js +13 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/other/OctavePlayer.d.ts +5 -3
- package/dist/types/other/SoundFontProvider.d.ts +13 -0
- package/dist/types/pickers/KeyPicker.d.ts +15 -0
- package/dist/types/pickers/PianoPicker.d.ts +3 -1
- package/lib/index.ts +2 -0
- package/lib/other/OctavePlayer.tsx +22 -31
- package/lib/other/SoundFontProvider.tsx +85 -6
- package/lib/pickers/KeyPicker.tsx +56 -0
- package/lib/pickers/PianoPicker.tsx +4 -0
- package/lib/pickers/knobSkin10.tsx +18 -0
- package/lib/rotaryKnob/knobdefaultskin.ts +13 -0
- package/lib/temp/acoustic_grand_piano-mp3.js +14 -1
- package/package.json +18 -20
- package/stories/assets/sampleStyles.css +16 -0
- package/stories/picker.stories.tsx +61 -25
- package/vite.config.ts +18 -9
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export * from './other/SvgText';
|
|
|
7
7
|
export * from './pickers/CCPicker';
|
|
8
8
|
export * from './pickers/ChannelMappingPicker';
|
|
9
9
|
export * from './pickers/ChannelPicker';
|
|
10
|
+
export * from './pickers/KeyPicker';
|
|
10
11
|
export * from './pickers/Knob';
|
|
11
12
|
export * from './pickers/KnobPicker';
|
|
12
13
|
export * from './pickers/LatchPicker';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { /* Midi, */ Note } from 'tonal';
|
|
3
3
|
import { Piano as ReactPiano } from 'zds-react-piano';
|
|
4
4
|
import { SoundfontProvider } from './SoundFontProvider';
|
|
@@ -30,19 +30,20 @@ const BaseOctavePlayer = ({ octave, height, width, ...rest }) => {
|
|
|
30
30
|
const lastNote = Note.midi(baseNotes[baseNotes.length - 1] + String(octave)) || 0;
|
|
31
31
|
return (_jsx("div", { children: _jsx(ReactPiano, { keyWidthToHeight: keyWidthToHeight, noteRange: { first: firstNote, last: lastNote }, width: width, ...rest }) }));
|
|
32
32
|
};
|
|
33
|
-
const WithProvider = ({ format, height, hostname, instrumentName, octave = 4, onChange, onClick, onDoubleClick, onKeyMouseEnter, onKeyMouseLeave, soundfont, Tooltip, width, }) => (_jsx(SoundfontProvider, { audioContext,
|
|
33
|
+
const WithProvider = ({ format, height, hostname, instrumentName, octave = 4, onChange, onClick, onDoubleClick, onKeyMouseEnter, onKeyMouseLeave, selectedNotes, soundfont, Tooltip, width, }) => (_jsx(SoundfontProvider, { audioContext,
|
|
34
34
|
format,
|
|
35
35
|
hostname,
|
|
36
36
|
instrumentName,
|
|
37
37
|
onChange,
|
|
38
|
-
soundfont, render: ({ isLoading, playNote, stopNote
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
soundfont, render: ({ isLoading, playNote, stopNote }) => (_jsx(BaseOctavePlayer, { disabled: isLoading, height,
|
|
39
|
+
octave,
|
|
40
|
+
onClick,
|
|
41
|
+
onDoubleClick,
|
|
42
|
+
onKeyMouseEnter,
|
|
43
|
+
onKeyMouseLeave,
|
|
44
|
+
playNote,
|
|
45
|
+
selectedNotes,
|
|
46
|
+
stopNote,
|
|
47
|
+
Tooltip,
|
|
48
|
+
width })) }));
|
|
48
49
|
export { WithProvider as OctavePlayer };
|
|
@@ -2,7 +2,30 @@
|
|
|
2
2
|
// for more documentation on prop options.
|
|
3
3
|
import { useCallback, useEffect, useState } from 'react';
|
|
4
4
|
import Soundfont from 'soundfont-player';
|
|
5
|
-
|
|
5
|
+
import { Note } from 'tonal';
|
|
6
|
+
// Ensure MIDI global exists before importing soundfont
|
|
7
|
+
if (typeof window !== 'undefined') {
|
|
8
|
+
if (!window.MIDI) {
|
|
9
|
+
window.MIDI = { Soundfont: {} };
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
// Also ensure it's available globally (not just on window)
|
|
13
|
+
if (typeof globalThis !== 'undefined') {
|
|
14
|
+
if (!globalThis.MIDI) {
|
|
15
|
+
globalThis.MIDI = window.MIDI || { Soundfont: {} };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Also set it on the global object for Node.js compatibility
|
|
19
|
+
if (typeof global !== 'undefined') {
|
|
20
|
+
if (!global.MIDI) {
|
|
21
|
+
global.MIDI = window.MIDI || { Soundfont: {} };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Import bundled soundfont to avoid CSP issues in Electron apps
|
|
25
|
+
// NOTE: This adds ~2.2MB to the bundle size. Currently all consumers need piano,
|
|
26
|
+
// but if future web apps don't need audio, consider making this import conditional
|
|
27
|
+
// or implementing lazy loading to reduce bundle size for audio-free use cases.
|
|
28
|
+
import '../temp/acoustic_grand_piano-mp3';
|
|
6
29
|
const isDefined = (item) => item !== undefined && item !== null && !Number.isNaN(item);
|
|
7
30
|
const SoundfontProvider = (props) => {
|
|
8
31
|
const { audioContext, format, hostname, instrumentName, onChange, render, soundfont, } = props;
|
|
@@ -24,9 +47,46 @@ const SoundfontProvider = (props) => {
|
|
|
24
47
|
}
|
|
25
48
|
else if (instrumentName) {
|
|
26
49
|
// the host app has already injected the soundfont into global space
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
50
|
+
// Try to create a player directly from our bundled soundfont data
|
|
51
|
+
const soundfontData = window.MIDI?.Soundfont?.[instrumentName];
|
|
52
|
+
if (soundfontData) {
|
|
53
|
+
// MIDI note number to note name conversion using Tonal
|
|
54
|
+
const midiToNoteName = (midiNumber) => {
|
|
55
|
+
const num = typeof midiNumber === 'string'
|
|
56
|
+
? Number.parseInt(midiNumber, 10)
|
|
57
|
+
: midiNumber;
|
|
58
|
+
return Note.fromMidi(num);
|
|
59
|
+
};
|
|
60
|
+
// Create a simple player that can play notes from our bundled data
|
|
61
|
+
const player = {
|
|
62
|
+
play: (note, velocity) => {
|
|
63
|
+
// Convert MIDI note number to note name if needed
|
|
64
|
+
const noteName = midiToNoteName(note);
|
|
65
|
+
const noteData = soundfontData[noteName];
|
|
66
|
+
if (noteData) {
|
|
67
|
+
// Create audio from the data URL
|
|
68
|
+
const audio = new Audio(noteData);
|
|
69
|
+
audio.volume = (velocity || 127) / 127;
|
|
70
|
+
audio.play();
|
|
71
|
+
// Return a compatible object with stop method
|
|
72
|
+
return {
|
|
73
|
+
stop: () => {
|
|
74
|
+
audio.pause();
|
|
75
|
+
audio.currentTime = 0;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
},
|
|
81
|
+
stop: () => {
|
|
82
|
+
// Simple stop implementation
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
setInstrument(player);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
setIsLoading(false);
|
|
89
|
+
}
|
|
30
90
|
}
|
|
31
91
|
else {
|
|
32
92
|
// sound must be disabled
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import { OctavePlayer } from '../other/OctavePlayer';
|
|
4
|
+
import { noSelection } from './Select';
|
|
5
|
+
const KeyPicker = (props) => {
|
|
6
|
+
const { value, onChange, disabled = false, height = 100, width = 300, octave = 4, ...rest } = props;
|
|
7
|
+
const handleKeyClick = useCallback((note) => {
|
|
8
|
+
if (disabled)
|
|
9
|
+
return;
|
|
10
|
+
onChange(note);
|
|
11
|
+
}, [disabled, onChange]);
|
|
12
|
+
// Only highlight the note if it's a valid selection and within the octave range
|
|
13
|
+
// Octave 4 = C4 (60) to B4 (71)
|
|
14
|
+
const octaveStart = 60 + (octave - 4) * 12;
|
|
15
|
+
const octaveEnd = octaveStart + 11;
|
|
16
|
+
const shouldHighlight = value !== noSelection && value >= octaveStart && value <= octaveEnd;
|
|
17
|
+
return (_jsx(OctavePlayer, { ...rest, selectedNotes: shouldHighlight ? [value] : [], disabled: disabled, height: height, width: width, octave: octave, onClick: handleKeyClick }));
|
|
18
|
+
};
|
|
19
|
+
KeyPicker.propTypes = {};
|
|
20
|
+
export { KeyPicker };
|
|
@@ -11,7 +11,7 @@ const BasePianoPicker = ({ height, width, ...rest }) => {
|
|
|
11
11
|
const keyWidthToHeight = keyWidth / height;
|
|
12
12
|
return (_jsx("div", { children: _jsx(ReactPiano, { keyWidthToHeight: keyWidthToHeight, noteRange: { first: firstNote, last: lastNote }, width: width, ...rest }) }));
|
|
13
13
|
};
|
|
14
|
-
const WithProvider = ({ format, height, hostname, instrumentName, onChange, onClick, onDoubleClick, onKeyMouseEnter, onKeyMouseLeave, soundfont, width, }) => (_jsx(SoundfontProvider, { audioContext,
|
|
14
|
+
const WithProvider = ({ format, height, hostname, instrumentName, onChange, onClick, onDoubleClick, onKeyMouseEnter, onKeyMouseLeave, selectedNotes, soundfont, width, }) => (_jsx(SoundfontProvider, { audioContext,
|
|
15
15
|
format,
|
|
16
16
|
hostname,
|
|
17
17
|
instrumentName,
|
|
@@ -22,6 +22,7 @@ const WithProvider = ({ format, height, hostname, instrumentName, onChange, onCl
|
|
|
22
22
|
onKeyMouseEnter,
|
|
23
23
|
onKeyMouseLeave,
|
|
24
24
|
playNote,
|
|
25
|
+
selectedNotes,
|
|
25
26
|
stopNote,
|
|
26
27
|
width })) }));
|
|
27
28
|
export { WithProvider as PianoPicker };
|
|
@@ -55,8 +55,26 @@ const knobSkin10 = {
|
|
|
55
55
|
</g>
|
|
56
56
|
<g id="knob" transform="translate(28.574244, 28.574244)">
|
|
57
57
|
<circle id="Oval-5" fill="#322E2E" cx="71.4356094" cy="71.4356094" r="71.4356094"/>
|
|
58
|
+
|
|
59
|
+
<!-- Rotating indicator (existing) -->
|
|
58
60
|
<path d="M72.4209282,3.63066376 L88.6786876,16.869789 C81.585496,18.1548295 76.1662428,18.7973498 72.4209282,18.7973498 C68.6756135,18.7973498 63.2563604,18.1548295 56.1631688,16.869789 L72.4209282,3.63066376 Z" id="Rectangle" fill="#E6D7D7" transform="translate(72.420928, 11.214007) scale(1, -1) translate(-72.420928, -11.214007) "/>
|
|
59
61
|
</g>
|
|
62
|
+
|
|
63
|
+
<!-- Fixed position marks (outside the rotating knob group) -->
|
|
64
|
+
<!-- 12:00 position mark (more prominent) -->
|
|
65
|
+
<line x1="100.0098534" y1="15" x2="100.0098534" y2="35" stroke="#CCCCCC" stroke-width="4" opacity="0.9"/>
|
|
66
|
+
|
|
67
|
+
<!-- Arc fill between end position marks -->
|
|
68
|
+
<!-- <path d="M 35 165 A 65 65 0 0 1 165 165" stroke="#666666" stroke-width="8" fill="none" opacity="0.3"/> -->
|
|
69
|
+
|
|
70
|
+
<!-- End position marks (0 and 127) -->
|
|
71
|
+
<!-- 0 position mark (40 degrees from top, clockwise) - at outer radius beyond knob edge -->
|
|
72
|
+
<!-- This should be at the bottom-left where the value indicator points when at 0 -->
|
|
73
|
+
<line x1="36" y1="164" x2="44" y2="156" stroke="#DDDDDD" stroke-width="4"/>
|
|
74
|
+
|
|
75
|
+
<!-- 127 position mark (320 degrees from top, clockwise) - at outer radius beyond knob edge -->
|
|
76
|
+
<!-- This should be at the bottom-right where the value indicator points when at 127 -->
|
|
77
|
+
<line x1="164" y1="164" x2="156" y2="156" stroke="#DDDDDD" stroke-width="4"/>
|
|
60
78
|
<g id="label">
|
|
61
79
|
<g id="labeltext" fill-opacity="1" fill="#E6D7D7">
|
|
62
80
|
<text id="534f4e02-5541-3607-0966-af0b025d80e0" font-family="Helvetica" font-size="44" font-weight="normal" fill="#E6D7D7" class="text-7" filter="url(#12f618f7-e258-b597-3ed6-242484ddad8f)">
|
|
@@ -46,6 +46,19 @@ export default {
|
|
|
46
46
|
</g>
|
|
47
47
|
<g id="knob" transform="translate(27.431373, 27.431373)">
|
|
48
48
|
<circle id="Oval-5" fill="#322E2E" cx="71" cy="71" r="71"></circle>
|
|
49
|
+
|
|
50
|
+
<!-- Fixed position marks -->
|
|
51
|
+
<!-- 12:00 position mark (less prominent) -->
|
|
52
|
+
<line x1="71" y1="15" x2="71" y2="25" stroke="#666666" stroke-width="1" opacity="0.6"/>
|
|
53
|
+
|
|
54
|
+
<!-- End position marks (0 and 127) -->
|
|
55
|
+
<!-- 0 position mark (40 degrees from top, clockwise) -->
|
|
56
|
+
<line x1="95.5" y1="45.5" x2="88.5" y2="52.5" stroke="#CCCCCC" stroke-width="2"/>
|
|
57
|
+
|
|
58
|
+
<!-- 127 position mark (320 degrees from top, clockwise) -->
|
|
59
|
+
<line x1="46.5" y1="45.5" x2="53.5" y2="52.5" stroke="#CCCCCC" stroke-width="2"/>
|
|
60
|
+
|
|
61
|
+
<!-- Rotating indicator (existing) -->
|
|
49
62
|
<path d="M70.0686275,4.17063061 L86.5686275,65.2689929 C81.3642501,66.811221 75.8642501,67.5735686 70.0686275,67.5560356 C64.2730048,67.5385027 58.7730048,66.7761551 53.5686275,65.2689929 L70.0686275,4.17063061 Z" id="Rectangle-Copy" fill="#E6D7D7" transform="translate(70.068627, 35.863481) scale(1, -1) translate(-70.068627, -35.863481) "></path>
|
|
50
63
|
</g>
|
|
51
64
|
</g>
|
package/dist/types/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from './other/SvgText';
|
|
|
7
7
|
export * from './pickers/CCPicker';
|
|
8
8
|
export * from './pickers/ChannelMappingPicker';
|
|
9
9
|
export * from './pickers/ChannelPicker';
|
|
10
|
+
export * from './pickers/KeyPicker';
|
|
10
11
|
export * from './pickers/Knob';
|
|
11
12
|
export * from './pickers/KnobPicker';
|
|
12
13
|
export * from './pickers/LatchPicker';
|
|
@@ -27,6 +28,7 @@ export type * from './other/SvgText';
|
|
|
27
28
|
export type * from './pickers/CCPicker';
|
|
28
29
|
export type * from './pickers/ChannelMappingPicker';
|
|
29
30
|
export type * from './pickers/ChannelPicker';
|
|
31
|
+
export type * from './pickers/KeyPicker';
|
|
30
32
|
export type * from './pickers/Knob';
|
|
31
33
|
export type * from './pickers/KnobPicker';
|
|
32
34
|
export type * from './pickers/LatchPicker';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PianoProps } from 'lib/pickers/PianoPicker';
|
|
1
2
|
import type Soundfont from 'soundfont-player';
|
|
2
3
|
import type { TooltipProps } from './DefaultTooltip';
|
|
3
4
|
type PianoMidiNote = {
|
|
@@ -6,13 +7,13 @@ type PianoMidiNote = {
|
|
|
6
7
|
midiNumber: number;
|
|
7
8
|
width: number;
|
|
8
9
|
};
|
|
9
|
-
type OctavePlayerProps = {
|
|
10
|
+
type OctavePlayerProps = PianoProps & {
|
|
10
11
|
disabled?: boolean;
|
|
11
12
|
height: number;
|
|
12
13
|
octave: number;
|
|
13
14
|
width: number;
|
|
14
15
|
};
|
|
15
|
-
type WithProviderProps = {
|
|
16
|
+
type WithProviderProps = PianoProps & {
|
|
16
17
|
format?: 'mp3' | 'ogg';
|
|
17
18
|
height: number;
|
|
18
19
|
hostname?: string;
|
|
@@ -23,10 +24,11 @@ type WithProviderProps = {
|
|
|
23
24
|
onDoubleClick?: (note: number) => void;
|
|
24
25
|
onKeyMouseEnter?: (note: PianoMidiNote) => void;
|
|
25
26
|
onKeyMouseLeave?: (note: number) => void;
|
|
27
|
+
selectedNotes?: number[];
|
|
26
28
|
soundfont?: 'MusyngKite' | 'FluidR3_GM';
|
|
27
29
|
Tooltip?: TooltipProps;
|
|
28
30
|
width: number;
|
|
29
31
|
};
|
|
30
|
-
declare const WithProvider: ({ format, height, hostname, instrumentName, octave, onChange, onClick, onDoubleClick, onKeyMouseEnter, onKeyMouseLeave, soundfont, Tooltip, width, }: WithProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
32
|
+
declare const WithProvider: ({ format, height, hostname, instrumentName, octave, onChange, onClick, onDoubleClick, onKeyMouseEnter, onKeyMouseLeave, selectedNotes, soundfont, Tooltip, width, }: WithProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
31
33
|
export { WithProvider as OctavePlayer };
|
|
32
34
|
export type { OctavePlayerProps, PianoMidiNote, WithProviderProps };
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import Soundfont from 'soundfont-player';
|
|
2
|
+
interface MIDIGlobal {
|
|
3
|
+
Soundfont: Record<string, Record<string, string>>;
|
|
4
|
+
}
|
|
5
|
+
declare global {
|
|
6
|
+
interface Window {
|
|
7
|
+
MIDI?: MIDIGlobal;
|
|
8
|
+
}
|
|
9
|
+
var MIDI: MIDIGlobal | undefined;
|
|
10
|
+
namespace globalThis {
|
|
11
|
+
var MIDI: MIDIGlobal | undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
import '../temp/acoustic_grand_piano-mp3';
|
|
2
15
|
type SoundfontProviderProps = {
|
|
3
16
|
audioContext: AudioContext;
|
|
4
17
|
format?: 'mp3' | 'ogg';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type OctavePlayerProps } from '../other/OctavePlayer';
|
|
2
|
+
type KeyPickerProps = Omit<OctavePlayerProps, 'selectedNotes' | 'onClick'> & {
|
|
3
|
+
value: number;
|
|
4
|
+
onChange: (value: number) => void;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
height?: number;
|
|
7
|
+
width?: number;
|
|
8
|
+
octave?: number;
|
|
9
|
+
};
|
|
10
|
+
declare const KeyPicker: {
|
|
11
|
+
(props: KeyPickerProps): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
propTypes: {};
|
|
13
|
+
};
|
|
14
|
+
export { KeyPicker };
|
|
15
|
+
export type { KeyPickerProps };
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { WithProviderProps } from '../other/OctavePlayer';
|
|
2
2
|
type PianoProps = {
|
|
3
|
+
activeNotes?: number[];
|
|
4
|
+
selectedNotes?: number[];
|
|
3
5
|
disabled: boolean;
|
|
4
6
|
height: number;
|
|
5
7
|
width: number;
|
|
6
8
|
};
|
|
7
|
-
declare const WithProvider: ({ format, height, hostname, instrumentName, onChange, onClick, onDoubleClick, onKeyMouseEnter, onKeyMouseLeave, soundfont, width, }: WithProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
declare const WithProvider: ({ format, height, hostname, instrumentName, onChange, onClick, onDoubleClick, onKeyMouseEnter, onKeyMouseLeave, selectedNotes, soundfont, width, }: WithProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
8
10
|
export { WithProvider as PianoPicker };
|
|
9
11
|
export type { PianoProps };
|
package/lib/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from './other/SvgText'
|
|
|
10
10
|
export * from './pickers/CCPicker'
|
|
11
11
|
export * from './pickers/ChannelMappingPicker'
|
|
12
12
|
export * from './pickers/ChannelPicker'
|
|
13
|
+
export * from './pickers/KeyPicker'
|
|
13
14
|
export * from './pickers/Knob'
|
|
14
15
|
export * from './pickers/KnobPicker'
|
|
15
16
|
export * from './pickers/LatchPicker'
|
|
@@ -34,6 +35,7 @@ export type * from './other/SvgText'
|
|
|
34
35
|
export type * from './pickers/CCPicker'
|
|
35
36
|
export type * from './pickers/ChannelMappingPicker'
|
|
36
37
|
export type * from './pickers/ChannelPicker'
|
|
38
|
+
export type * from './pickers/KeyPicker'
|
|
37
39
|
export type * from './pickers/Knob'
|
|
38
40
|
export type * from './pickers/KnobPicker'
|
|
39
41
|
export type * from './pickers/LatchPicker'
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PianoProps } from 'lib/pickers/PianoPicker'
|
|
1
2
|
import type Soundfont from 'soundfont-player'
|
|
2
3
|
import { /* Midi, */ Note } from 'tonal'
|
|
3
4
|
import { Piano as ReactPiano } from 'zds-react-piano'
|
|
@@ -38,7 +39,7 @@ const whiteNotes = numNotes * 0.7 // approx
|
|
|
38
39
|
|
|
39
40
|
const audioContext = new window.AudioContext()
|
|
40
41
|
|
|
41
|
-
type OctavePlayerProps = {
|
|
42
|
+
type OctavePlayerProps = PianoProps & {
|
|
42
43
|
disabled?: boolean
|
|
43
44
|
height: number
|
|
44
45
|
octave: number
|
|
@@ -70,7 +71,7 @@ const BaseOctavePlayer = ({
|
|
|
70
71
|
)
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
type WithProviderProps = {
|
|
74
|
+
type WithProviderProps = PianoProps & {
|
|
74
75
|
format?: 'mp3' | 'ogg'
|
|
75
76
|
height: number
|
|
76
77
|
hostname?: string
|
|
@@ -81,6 +82,7 @@ type WithProviderProps = {
|
|
|
81
82
|
onDoubleClick?: (note: number) => void
|
|
82
83
|
onKeyMouseEnter?: (note: PianoMidiNote) => void
|
|
83
84
|
onKeyMouseLeave?: (note: number) => void
|
|
85
|
+
selectedNotes?: number[]
|
|
84
86
|
soundfont?: 'MusyngKite' | 'FluidR3_GM'
|
|
85
87
|
Tooltip?: TooltipProps
|
|
86
88
|
width: number
|
|
@@ -97,6 +99,7 @@ const WithProvider = ({
|
|
|
97
99
|
onDoubleClick,
|
|
98
100
|
onKeyMouseEnter,
|
|
99
101
|
onKeyMouseLeave,
|
|
102
|
+
selectedNotes,
|
|
100
103
|
soundfont,
|
|
101
104
|
Tooltip,
|
|
102
105
|
width,
|
|
@@ -110,35 +113,23 @@ const WithProvider = ({
|
|
|
110
113
|
onChange,
|
|
111
114
|
soundfont,
|
|
112
115
|
}}
|
|
113
|
-
render={({
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
octave,
|
|
131
|
-
onClick,
|
|
132
|
-
onDoubleClick,
|
|
133
|
-
onKeyMouseEnter,
|
|
134
|
-
onKeyMouseLeave,
|
|
135
|
-
playNote,
|
|
136
|
-
stopNote,
|
|
137
|
-
Tooltip,
|
|
138
|
-
width,
|
|
139
|
-
}}
|
|
140
|
-
/>
|
|
141
|
-
</>
|
|
116
|
+
render={({ isLoading, playNote, stopNote }) => (
|
|
117
|
+
<BaseOctavePlayer
|
|
118
|
+
disabled={isLoading}
|
|
119
|
+
{...{
|
|
120
|
+
height,
|
|
121
|
+
octave,
|
|
122
|
+
onClick,
|
|
123
|
+
onDoubleClick,
|
|
124
|
+
onKeyMouseEnter,
|
|
125
|
+
onKeyMouseLeave,
|
|
126
|
+
playNote,
|
|
127
|
+
selectedNotes,
|
|
128
|
+
stopNote,
|
|
129
|
+
Tooltip,
|
|
130
|
+
width,
|
|
131
|
+
}}
|
|
132
|
+
/>
|
|
142
133
|
)}
|
|
143
134
|
/>
|
|
144
135
|
)
|
|
@@ -2,7 +2,49 @@
|
|
|
2
2
|
// for more documentation on prop options.
|
|
3
3
|
import { useCallback, useEffect, useState } from 'react'
|
|
4
4
|
import Soundfont from 'soundfont-player'
|
|
5
|
-
|
|
5
|
+
import { Note } from 'tonal'
|
|
6
|
+
|
|
7
|
+
// Type for the MIDI global object
|
|
8
|
+
interface MIDIGlobal {
|
|
9
|
+
Soundfont: Record<string, Record<string, string>>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Extend global interfaces to include MIDI
|
|
13
|
+
declare global {
|
|
14
|
+
interface Window {
|
|
15
|
+
MIDI?: MIDIGlobal
|
|
16
|
+
}
|
|
17
|
+
var MIDI: MIDIGlobal | undefined
|
|
18
|
+
namespace globalThis {
|
|
19
|
+
var MIDI: MIDIGlobal | undefined
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// Ensure MIDI global exists before importing soundfont
|
|
23
|
+
if (typeof window !== 'undefined') {
|
|
24
|
+
if (!window.MIDI) {
|
|
25
|
+
window.MIDI = { Soundfont: {} }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Also ensure it's available globally (not just on window)
|
|
30
|
+
if (typeof globalThis !== 'undefined') {
|
|
31
|
+
if (!globalThis.MIDI) {
|
|
32
|
+
globalThis.MIDI = window.MIDI || { Soundfont: {} }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Also set it on the global object for Node.js compatibility
|
|
37
|
+
if (typeof global !== 'undefined') {
|
|
38
|
+
if (!global.MIDI) {
|
|
39
|
+
global.MIDI = window.MIDI || { Soundfont: {} }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Import bundled soundfont to avoid CSP issues in Electron apps
|
|
44
|
+
// NOTE: This adds ~2.2MB to the bundle size. Currently all consumers need piano,
|
|
45
|
+
// but if future web apps don't need audio, consider making this import conditional
|
|
46
|
+
// or implementing lazy loading to reduce bundle size for audio-free use cases.
|
|
47
|
+
import '../temp/acoustic_grand_piano-mp3'
|
|
6
48
|
|
|
7
49
|
const isDefined = <T,>(item: T) =>
|
|
8
50
|
item !== undefined && item !== null && !Number.isNaN(item)
|
|
@@ -56,11 +98,48 @@ const SoundfontProvider = (props: SoundfontProviderProps) => {
|
|
|
56
98
|
})
|
|
57
99
|
} else if (instrumentName) {
|
|
58
100
|
// the host app has already injected the soundfont into global space
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
101
|
+
// Try to create a player directly from our bundled soundfont data
|
|
102
|
+
const soundfontData = window.MIDI?.Soundfont?.[instrumentName]
|
|
103
|
+
if (soundfontData) {
|
|
104
|
+
// MIDI note number to note name conversion using Tonal
|
|
105
|
+
const midiToNoteName = (midiNumber: number | string): string => {
|
|
106
|
+
const num =
|
|
107
|
+
typeof midiNumber === 'string'
|
|
108
|
+
? Number.parseInt(midiNumber, 10)
|
|
109
|
+
: midiNumber
|
|
110
|
+
return Note.fromMidi(num)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Create a simple player that can play notes from our bundled data
|
|
114
|
+
const player = {
|
|
115
|
+
play: (note: string | number, velocity?: number) => {
|
|
116
|
+
// Convert MIDI note number to note name if needed
|
|
117
|
+
const noteName = midiToNoteName(note)
|
|
118
|
+
const noteData = soundfontData[noteName]
|
|
119
|
+
if (noteData) {
|
|
120
|
+
// Create audio from the data URL
|
|
121
|
+
const audio = new Audio(noteData)
|
|
122
|
+
audio.volume = (velocity || 127) / 127
|
|
123
|
+
audio.play()
|
|
124
|
+
|
|
125
|
+
// Return a compatible object with stop method
|
|
126
|
+
return {
|
|
127
|
+
stop: () => {
|
|
128
|
+
audio.pause()
|
|
129
|
+
audio.currentTime = 0
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null
|
|
134
|
+
},
|
|
135
|
+
stop: () => {
|
|
136
|
+
// Simple stop implementation
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
setInstrument(player as Soundfont.Player)
|
|
140
|
+
} else {
|
|
141
|
+
setIsLoading(false)
|
|
142
|
+
}
|
|
64
143
|
} else {
|
|
65
144
|
// sound must be disabled
|
|
66
145
|
setIsLoading(false)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useCallback } from 'react'
|
|
2
|
+
import { OctavePlayer, type OctavePlayerProps } from '../other/OctavePlayer'
|
|
3
|
+
import { noSelection } from './Select'
|
|
4
|
+
|
|
5
|
+
type KeyPickerProps = Omit<OctavePlayerProps, 'selectedNotes' | 'onClick'> & {
|
|
6
|
+
value: number
|
|
7
|
+
onChange: (value: number) => void
|
|
8
|
+
disabled?: boolean
|
|
9
|
+
height?: number
|
|
10
|
+
width?: number
|
|
11
|
+
octave?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const KeyPicker = (props: KeyPickerProps) => {
|
|
15
|
+
const {
|
|
16
|
+
value,
|
|
17
|
+
onChange,
|
|
18
|
+
disabled = false,
|
|
19
|
+
height = 100,
|
|
20
|
+
width = 300,
|
|
21
|
+
octave = 4,
|
|
22
|
+
...rest
|
|
23
|
+
} = props
|
|
24
|
+
|
|
25
|
+
const handleKeyClick = useCallback(
|
|
26
|
+
(note: number) => {
|
|
27
|
+
if (disabled) return
|
|
28
|
+
onChange(note)
|
|
29
|
+
},
|
|
30
|
+
[disabled, onChange],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
// Only highlight the note if it's a valid selection and within the octave range
|
|
34
|
+
// Octave 4 = C4 (60) to B4 (71)
|
|
35
|
+
const octaveStart = 60 + (octave - 4) * 12
|
|
36
|
+
const octaveEnd = octaveStart + 11
|
|
37
|
+
const shouldHighlight =
|
|
38
|
+
value !== noSelection && value >= octaveStart && value <= octaveEnd
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<OctavePlayer
|
|
42
|
+
{...rest}
|
|
43
|
+
selectedNotes={shouldHighlight ? [value] : []}
|
|
44
|
+
disabled={disabled}
|
|
45
|
+
height={height}
|
|
46
|
+
width={width}
|
|
47
|
+
octave={octave}
|
|
48
|
+
onClick={handleKeyClick}
|
|
49
|
+
/>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
KeyPicker.propTypes = {}
|
|
54
|
+
|
|
55
|
+
export { KeyPicker }
|
|
56
|
+
export type { KeyPickerProps }
|
|
@@ -11,6 +11,8 @@ const whiteNotes = numNotes * 0.7 // approx
|
|
|
11
11
|
const audioContext = new window.AudioContext()
|
|
12
12
|
|
|
13
13
|
type PianoProps = {
|
|
14
|
+
activeNotes?: number[]
|
|
15
|
+
selectedNotes?: number[]
|
|
14
16
|
disabled: boolean
|
|
15
17
|
height: number
|
|
16
18
|
width: number
|
|
@@ -42,6 +44,7 @@ const WithProvider = ({
|
|
|
42
44
|
onDoubleClick,
|
|
43
45
|
onKeyMouseEnter,
|
|
44
46
|
onKeyMouseLeave,
|
|
47
|
+
selectedNotes,
|
|
45
48
|
soundfont,
|
|
46
49
|
width,
|
|
47
50
|
}: WithProviderProps) => (
|
|
@@ -64,6 +67,7 @@ const WithProvider = ({
|
|
|
64
67
|
onKeyMouseEnter,
|
|
65
68
|
onKeyMouseLeave,
|
|
66
69
|
playNote,
|
|
70
|
+
selectedNotes,
|
|
67
71
|
stopNote,
|
|
68
72
|
width,
|
|
69
73
|
}}
|
|
@@ -55,8 +55,26 @@ const knobSkin10 = {
|
|
|
55
55
|
</g>
|
|
56
56
|
<g id="knob" transform="translate(28.574244, 28.574244)">
|
|
57
57
|
<circle id="Oval-5" fill="#322E2E" cx="71.4356094" cy="71.4356094" r="71.4356094"/>
|
|
58
|
+
|
|
59
|
+
<!-- Rotating indicator (existing) -->
|
|
58
60
|
<path d="M72.4209282,3.63066376 L88.6786876,16.869789 C81.585496,18.1548295 76.1662428,18.7973498 72.4209282,18.7973498 C68.6756135,18.7973498 63.2563604,18.1548295 56.1631688,16.869789 L72.4209282,3.63066376 Z" id="Rectangle" fill="#E6D7D7" transform="translate(72.420928, 11.214007) scale(1, -1) translate(-72.420928, -11.214007) "/>
|
|
59
61
|
</g>
|
|
62
|
+
|
|
63
|
+
<!-- Fixed position marks (outside the rotating knob group) -->
|
|
64
|
+
<!-- 12:00 position mark (more prominent) -->
|
|
65
|
+
<line x1="100.0098534" y1="15" x2="100.0098534" y2="35" stroke="#CCCCCC" stroke-width="4" opacity="0.9"/>
|
|
66
|
+
|
|
67
|
+
<!-- Arc fill between end position marks -->
|
|
68
|
+
<!-- <path d="M 35 165 A 65 65 0 0 1 165 165" stroke="#666666" stroke-width="8" fill="none" opacity="0.3"/> -->
|
|
69
|
+
|
|
70
|
+
<!-- End position marks (0 and 127) -->
|
|
71
|
+
<!-- 0 position mark (40 degrees from top, clockwise) - at outer radius beyond knob edge -->
|
|
72
|
+
<!-- This should be at the bottom-left where the value indicator points when at 0 -->
|
|
73
|
+
<line x1="36" y1="164" x2="44" y2="156" stroke="#DDDDDD" stroke-width="4"/>
|
|
74
|
+
|
|
75
|
+
<!-- 127 position mark (320 degrees from top, clockwise) - at outer radius beyond knob edge -->
|
|
76
|
+
<!-- This should be at the bottom-right where the value indicator points when at 127 -->
|
|
77
|
+
<line x1="164" y1="164" x2="156" y2="156" stroke="#DDDDDD" stroke-width="4"/>
|
|
60
78
|
<g id="label">
|
|
61
79
|
<g id="labeltext" fill-opacity="1" fill="#E6D7D7">
|
|
62
80
|
<text id="534f4e02-5541-3607-0966-af0b025d80e0" font-family="Helvetica" font-size="44" font-weight="normal" fill="#E6D7D7" class="text-7" filter="url(#12f618f7-e258-b597-3ed6-242484ddad8f)">
|
|
@@ -46,6 +46,19 @@ export default {
|
|
|
46
46
|
</g>
|
|
47
47
|
<g id="knob" transform="translate(27.431373, 27.431373)">
|
|
48
48
|
<circle id="Oval-5" fill="#322E2E" cx="71" cy="71" r="71"></circle>
|
|
49
|
+
|
|
50
|
+
<!-- Fixed position marks -->
|
|
51
|
+
<!-- 12:00 position mark (less prominent) -->
|
|
52
|
+
<line x1="71" y1="15" x2="71" y2="25" stroke="#666666" stroke-width="1" opacity="0.6"/>
|
|
53
|
+
|
|
54
|
+
<!-- End position marks (0 and 127) -->
|
|
55
|
+
<!-- 0 position mark (40 degrees from top, clockwise) -->
|
|
56
|
+
<line x1="95.5" y1="45.5" x2="88.5" y2="52.5" stroke="#CCCCCC" stroke-width="2"/>
|
|
57
|
+
|
|
58
|
+
<!-- 127 position mark (320 degrees from top, clockwise) -->
|
|
59
|
+
<line x1="46.5" y1="45.5" x2="53.5" y2="52.5" stroke="#CCCCCC" stroke-width="2"/>
|
|
60
|
+
|
|
61
|
+
<!-- Rotating indicator (existing) -->
|
|
49
62
|
<path d="M70.0686275,4.17063061 L86.5686275,65.2689929 C81.3642501,66.811221 75.8642501,67.5735686 70.0686275,67.5560356 C64.2730048,67.5385027 58.7730048,66.7761551 53.5686275,65.2689929 L70.0686275,4.17063061 Z" id="Rectangle-Copy" fill="#E6D7D7" transform="translate(70.068627, 35.863481) scale(1, -1) translate(-70.068627, -35.863481) "></path>
|
|
50
63
|
</g>
|
|
51
64
|
</g>
|