zds-pickers 4.3.0 → 4.5.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/dist/index.cjs.js +5 -5
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +696 -2452
- package/dist/index.es.js.map +1 -1
- package/dist/other/SoundFontProvider.js +38 -6
- package/dist/pickers/KeyPicker.js +2 -2
- package/dist/types/pickers/KeyPicker.d.ts +2 -0
- package/package.json +4 -3
|
@@ -29,7 +29,17 @@ const midiToNoteName = (midiNumber) => {
|
|
|
29
29
|
: midiNumber;
|
|
30
30
|
return Note.fromMidi(num);
|
|
31
31
|
};
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Key-up release, in seconds — the same 0.3 s soundfont-player's own envelope
|
|
34
|
+
* uses on the remote path. Pausing the element outright cut the sample
|
|
35
|
+
* mid-waveform, which clicked on every key-up and made a glissando sound
|
|
36
|
+
* like a row of switches.
|
|
37
|
+
*/
|
|
38
|
+
const RELEASE_SECONDS = 0.3;
|
|
39
|
+
// setTargetAtTime is exponential, so it needs a time constant rather than a
|
|
40
|
+
// duration: five of them leave <1% of the level by the time the element stops.
|
|
41
|
+
const RELEASE_TIME_CONSTANT = RELEASE_SECONDS / 5;
|
|
42
|
+
const createBundledPlayer = (audioContext, soundfontData) => {
|
|
33
43
|
const player = {
|
|
34
44
|
play: (note, velocity) => {
|
|
35
45
|
const noteName = midiToNoteName(note);
|
|
@@ -37,13 +47,29 @@ const createBundledPlayer = (soundfontData) => {
|
|
|
37
47
|
if (!noteData) {
|
|
38
48
|
return { stop: () => { } };
|
|
39
49
|
}
|
|
50
|
+
// Still streamed by an <audio> element — no decode, so no first-note
|
|
51
|
+
// latency — but routed through a gain node so the release can ramp.
|
|
40
52
|
const audio = new Audio(noteData);
|
|
41
|
-
|
|
53
|
+
const source = audioContext.createMediaElementSource(audio);
|
|
54
|
+
const gain = audioContext.createGain();
|
|
55
|
+
gain.gain.value = (velocity ?? 127) / 127;
|
|
56
|
+
source.connect(gain).connect(audioContext.destination);
|
|
42
57
|
audio.play();
|
|
58
|
+
let stopped = false;
|
|
43
59
|
return {
|
|
44
60
|
stop: () => {
|
|
45
|
-
|
|
46
|
-
|
|
61
|
+
if (stopped)
|
|
62
|
+
return;
|
|
63
|
+
stopped = true;
|
|
64
|
+
const now = audioContext.currentTime;
|
|
65
|
+
gain.gain.cancelScheduledValues(now);
|
|
66
|
+
gain.gain.setValueAtTime(gain.gain.value, now);
|
|
67
|
+
gain.gain.setTargetAtTime(0, now, RELEASE_TIME_CONSTANT);
|
|
68
|
+
window.setTimeout(() => {
|
|
69
|
+
audio.pause();
|
|
70
|
+
source.disconnect();
|
|
71
|
+
gain.disconnect();
|
|
72
|
+
}, RELEASE_SECONDS * 1000);
|
|
47
73
|
},
|
|
48
74
|
};
|
|
49
75
|
},
|
|
@@ -54,7 +80,7 @@ const createBundledPlayer = (soundfontData) => {
|
|
|
54
80
|
const loadBundledOrRemoteInstrument = (audioContext, instrumentName, format, soundfont, hostname) => {
|
|
55
81
|
const soundfontData = window.MIDI?.Soundfont?.[instrumentName];
|
|
56
82
|
if (soundfontData) {
|
|
57
|
-
return Promise.resolve(createBundledPlayer(soundfontData));
|
|
83
|
+
return Promise.resolve(createBundledPlayer(audioContext, soundfontData));
|
|
58
84
|
}
|
|
59
85
|
// biome-ignore lint/suspicious/noConsole: intentional diagnostics for missing bundled soundfont
|
|
60
86
|
console.warn(`[zds-pickers] Bundled soundfont "${instrumentName}" not found; falling back to remote load.`);
|
|
@@ -72,7 +98,9 @@ const loadBundledOrRemoteInstrument = (audioContext, instrumentName, format, sou
|
|
|
72
98
|
};
|
|
73
99
|
const SoundfontProvider = (props) => {
|
|
74
100
|
const { audioContext, format, hostname, instrumentName, onChange, render, soundfont, } = props;
|
|
75
|
-
|
|
101
|
+
// No instrument means silent, not loading — starting true would leave a
|
|
102
|
+
// muted keyboard disabled forever, since nothing ever loads to clear it.
|
|
103
|
+
const [isLoading, setIsLoading] = useState(Boolean(instrumentName));
|
|
76
104
|
const [activeAudioNodes, setActiveAudioNodes] = useState({});
|
|
77
105
|
const [instrument, setInstrument] = useState(null);
|
|
78
106
|
const loadInstrument = useCallback((nm) => {
|
|
@@ -111,7 +139,11 @@ const SoundfontProvider = (props) => {
|
|
|
111
139
|
useEffect(() => {
|
|
112
140
|
if (instrumentName) {
|
|
113
141
|
loadInstrument(instrumentName);
|
|
142
|
+
return;
|
|
114
143
|
}
|
|
144
|
+
// Unset after a load (muting): drop the loaded player so playNote no-ops.
|
|
145
|
+
setInstrument(null);
|
|
146
|
+
setIsLoading(false);
|
|
115
147
|
}, [instrumentName, loadInstrument]);
|
|
116
148
|
useEffect(() => {
|
|
117
149
|
if (isDefined(instrumentName)) {
|
|
@@ -24,7 +24,7 @@ const makeNoteNameLabelRenderer = (noteLabels) => ({ isAccidental, isActive, mid
|
|
|
24
24
|
}), children: pitchName }));
|
|
25
25
|
};
|
|
26
26
|
const KeyPicker = (props) => {
|
|
27
|
-
const { value, onChange, disabled = false, height = 100, noteLabels, showNoteNames = false, width = 300, octave = 4, ...rest } = props;
|
|
27
|
+
const { value, onChange, disabled = false, height = 100, muted = false, noteLabels, showNoteNames = false, width = 300, octave = 4, ...rest } = props;
|
|
28
28
|
const renderNoteLabel = useMemo(() => makeNoteNameLabelRenderer(noteLabels), [noteLabels]);
|
|
29
29
|
const handleKeyClick = useCallback((note) => {
|
|
30
30
|
if (disabled)
|
|
@@ -36,6 +36,6 @@ const KeyPicker = (props) => {
|
|
|
36
36
|
const octaveStart = 60 + (octave - 4) * 12;
|
|
37
37
|
const octaveEnd = octaveStart + 11;
|
|
38
38
|
const shouldHighlight = value !== noSelection && value >= octaveStart && value <= octaveEnd;
|
|
39
|
-
return (_jsx(OctavePlayer, { ...rest, className: showNoteNames ? 'ReactPiano--showNoteNames' : undefined, selectedNotes: shouldHighlight ? [value] : [], disabled: disabled, height: height, renderNoteLabel: showNoteNames ? renderNoteLabel : undefined, width: width, octave: octave, onClick: handleKeyClick, instrumentName:
|
|
39
|
+
return (_jsx(OctavePlayer, { ...rest, className: showNoteNames ? 'ReactPiano--showNoteNames' : undefined, selectedNotes: shouldHighlight ? [value] : [], disabled: disabled, height: height, renderNoteLabel: showNoteNames ? renderNoteLabel : undefined, width: width, octave: octave, onClick: handleKeyClick, instrumentName: muted ? undefined : 'acoustic_grand_piano' }));
|
|
40
40
|
};
|
|
41
41
|
export { KeyPicker };
|
|
@@ -4,6 +4,8 @@ type KeyPickerProps = Omit<PianoProviderProps, 'className' | 'instrumentName' |
|
|
|
4
4
|
onChange: (value: number) => void;
|
|
5
5
|
disabled?: boolean;
|
|
6
6
|
height?: number;
|
|
7
|
+
/** Silences the piano sample a key click otherwise plays. */
|
|
8
|
+
muted?: boolean;
|
|
7
9
|
/**
|
|
8
10
|
* Chroma-indexed note names (index 0 = C) overriding the default flat
|
|
9
11
|
* spelling — lets a caller label keys for a specific key/scale, where both
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zds-pickers",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"main": "./dist/index.cjs.js",
|
|
5
5
|
"module": "./dist/index.es.js",
|
|
6
6
|
"types": "./dist/types/index.d.ts",
|
|
@@ -55,9 +55,10 @@
|
|
|
55
55
|
"react-dom": "18.3.1",
|
|
56
56
|
"react-select": "5.10.1",
|
|
57
57
|
"storybook": "10.5.5",
|
|
58
|
+
"tonal": "6.4.3",
|
|
58
59
|
"typescript": "5.8.2",
|
|
59
60
|
"vite": "^8.0.16",
|
|
60
|
-
"vitest": "4.1.
|
|
61
|
+
"vitest": "4.1.11",
|
|
61
62
|
"zds-mappings": "1.4.9"
|
|
62
63
|
},
|
|
63
64
|
"dependencies": {
|
|
@@ -66,7 +67,6 @@
|
|
|
66
67
|
"d3-selection": "3.0.0",
|
|
67
68
|
"react-svgmt": "2.0.2",
|
|
68
69
|
"soundfont-player": "0.12.0",
|
|
69
|
-
"tonal": "6.4.2",
|
|
70
70
|
"zds-react-piano": "4.1.0"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"react": ">=18.0.0",
|
|
75
75
|
"react-dom": ">=18.0.0",
|
|
76
76
|
"react-select": ">=5.10.0",
|
|
77
|
+
"tonal": ">=6.4.2",
|
|
77
78
|
"zds-mappings": "^1.4.9"
|
|
78
79
|
},
|
|
79
80
|
"scripts": {
|