>({});
19 |
20 | useMappedKeyboardKeys(keysRef);
21 |
22 | return (
23 |
24 | {allNotes
25 | .filter((_, i) => i >= 24 && i < 64) // just keep the middle notes, 88 is a lot
26 | .map(({ note, octave }) => (
27 |
36 | (keysRef.current[`${note}${octave}`] = el)
37 | }
38 | />
39 | ))}
40 |
41 | );
42 | }
43 |
44 | export const KeyboardMap = {
45 | a: "g3",
46 | w: "g#3",
47 | s: "a3",
48 | e: "a#3",
49 | d: "b3",
50 | f: "c4",
51 | t: "c#4",
52 | g: "d4",
53 | y: "d#4",
54 | h: "e4",
55 | j: "f4",
56 | i: "f#4",
57 | k: "g4",
58 | o: "g#4",
59 | l: "a4",
60 | p: "a#4",
61 | ";": "b4",
62 | "'": "c5",
63 | } as const;
64 | type MappedKeyboardKey = keyof typeof KeyboardMap;
65 |
66 | // map the middle and top rows of the computer keyboard to the middle piano keys (f => middle C)
67 | function useMappedKeyboardKeys(
68 | keysRef: React.MutableRefObject>
69 | ) {
70 | useEffect(() => {
71 | function keyPress(event: KeyboardEvent) {
72 | const key = event.key as MappedKeyboardKey;
73 | if (key in KeyboardMap && KeyboardMap[key] in keysRef.current) {
74 | const el = keysRef.current[KeyboardMap[key]];
75 | el.focus();
76 | el.click();
77 | }
78 | }
79 | document.body.addEventListener("keypress", keyPress);
80 | return () => document.body.removeEventListener("keypress", keyPress);
81 | }, [keysRef]);
82 | }
83 |
84 | export const Notes = [
85 | "a",
86 | "a#",
87 | "b",
88 | "c",
89 | "c#",
90 | "d",
91 | "d#",
92 | "e",
93 | "f",
94 | "f#",
95 | "g",
96 | "g#",
97 | ] as const;
98 | export type Note = typeof Notes[number];
99 |
100 | export const allNotes = Array(88)
101 | .fill(null)
102 | .map((_, i) => ({
103 | note: Notes[i % 12],
104 | octave: Math.floor((i + 9) / 12),
105 | }));
106 | export const noteIndices = Object.fromEntries(
107 | allNotes.map(({ note, octave }, i) => [`${note}${octave}`, i])
108 | );
109 |
110 | type KeyProps = {
111 | note: Note;
112 | octave: number;
113 | onPlay?(note: string, el: HTMLButtonElement): void;
114 | active?: boolean;
115 | };
116 | const Key = forwardRef(function Key(
117 | { note, octave, onPlay, active },
118 | ref
119 | ) {
120 | const localRef = useRef(null);
121 | useEffect(() => {
122 | if (localRef.current && document.activeElement === localRef.current)
123 | localRef.current.blur();
124 | }, []);
125 |
126 | function onClickOrTouch(e: React.MouseEvent | React.TouchEvent) {
127 | const el = e.target as HTMLButtonElement;
128 | onPlay?.(`${note}${octave}`, el);
129 | setTimeout(() => {
130 | if (document.activeElement === el) el.blur();
131 | }, 0);
132 | }
133 |
134 | return (
135 |