Browse Source

play sound on mouseover

+ add a form feature
mrkvon 1 year ago
parent
commit
9e7bf0e8bb
5 changed files with 129 additions and 19 deletions
  1. 15
    3
      src/App.tsx
  2. 26
    1
      src/ChordForm.tsx
  3. 53
    0
      src/SoundButton.tsx
  4. 34
    15
      src/chords.tsx
  5. 1
    0
      src/play.tsx

+ 15
- 3
src/App.tsx View File

@@ -1,15 +1,22 @@
1 1
 import React, { useState, ReactElement } from 'react';
2 2
 import FingerBoard from './FingerBoard';
3 3
 import ChordForm from './ChordForm';
4
-import { chord2fingering } from './chords';
4
+import { chord2fingering, chord2frequencies } from './chords';
5
+import SoundButton from './SoundButton';
5 6
 
6 7
 type ChordSelected = {
7 8
   chord: string;
8 9
   selected: boolean;
9 10
 };
10 11
 
12
+const initialChords = [
13
+  { chord: 'dmin7', selected: true },
14
+  { chord: 'g7', selected: false },
15
+  { chord: 'cmaj7', selected: false },
16
+];
17
+
11 18
 function App(): ReactElement {
12
-  const [chords, setChords] = useState([] as ChordSelected[]);
19
+  const [chords, setChords] = useState(initialChords as ChordSelected[]);
13 20
 
14 21
   const selectedChord = chords.find(({ selected }) => selected);
15 22
   const fingers = selectedChord ? chord2fingering(selectedChord.chord) : [];
@@ -42,7 +49,12 @@ function App(): ReactElement {
42 49
         <ul>
43 50
           {chords.map(({ chord }, i) => (
44 51
             <li key={i}>
45
-              <button onMouseOver={() => handleSelectChord(i)}>{chord}</button>
52
+              <SoundButton
53
+                frequencies={chord2frequencies(chord)}
54
+                onMouseOver={() => handleSelectChord(i)}
55
+              >
56
+                {chord}
57
+              </SoundButton>
46 58
               <button onClick={() => handleRemoveChord(i)}>x</button>
47 59
               {/*i > 0 && <button onClick={() => handleUp(i)}>up</button>}
48 60
               {i < chords.length - 1 && (

+ 26
- 1
src/ChordForm.tsx View File

@@ -1,5 +1,9 @@
1 1
 import React, { useState, ReactElement, FormEvent, ChangeEvent } from 'react';
2
-import { chord2fingering } from './chords';
2
+import { chord2fingering, chords, notes, accidentals } from './chords';
3
+
4
+const availableChords = Object.keys(chords);
5
+const availableNotes = Object.keys(notes);
6
+const availableAccidentals = Object.keys(accidentals);
3 7
 
4 8
 const ChordForm = ({
5 9
   onSubmit,
@@ -32,6 +36,27 @@ const ChordForm = ({
32 36
       {chord && error && <div>{error}</div>}
33 37
       <input value={chord} onChange={handleChange} />
34 38
       <input type="submit" value="submit" disabled={!chord || !!error} />
39
+      <div>
40
+        {availableNotes.map(note => (
41
+          <a onClick={() => setChord(note)} key={note}>
42
+            {note}
43
+          </a>
44
+        ))}
45
+      </div>
46
+      <div>
47
+        {availableAccidentals.map(acc => (
48
+          <a onClick={() => setChord(value => value + acc)} key={acc}>
49
+            {acc}
50
+          </a>
51
+        ))}
52
+      </div>
53
+      <div>
54
+        {availableChords.map(chord => (
55
+          <a onClick={() => setChord(value => value + chord)} key={chord}>
56
+            {chord}
57
+          </a>
58
+        ))}
59
+      </div>
35 60
     </form>
36 61
   );
37 62
 };

+ 53
- 0
src/SoundButton.tsx View File

@@ -0,0 +1,53 @@
1
+import React, { useState, useEffect, ReactNode } from 'react';
2
+
3
+const SoundButton = ({
4
+  frequencies,
5
+  onMouseOver = () => null,
6
+  children,
7
+  ...rest
8
+}: {
9
+  frequencies: number[];
10
+  onMouseOver?: () => void;
11
+  children: ReactNode;
12
+}) => {
13
+  const [play, setPlay] = useState(false);
14
+
15
+  useEffect(() => {
16
+    const context = play ? new AudioContext() : null;
17
+    const o: OscillatorNode[] = [];
18
+    if (play && context) {
19
+      frequencies.forEach(f => {
20
+        const os = context.createOscillator();
21
+        const g = context.createGain();
22
+        os.type = 'triangle';
23
+        os.frequency.value = f;
24
+        os.connect(g);
25
+        g.gain.value = 0.25;
26
+        g.connect(context.destination);
27
+        os.start();
28
+        o.push(os);
29
+      });
30
+    }
31
+    return () => {
32
+      o.forEach(o => o && o.stop());
33
+      context && context.close();
34
+    };
35
+  }, [play, frequencies]);
36
+
37
+  const handleMouseOver = () => {
38
+    setPlay(true);
39
+    onMouseOver();
40
+  };
41
+
42
+  return (
43
+    <button
44
+      onMouseOver={handleMouseOver}
45
+      onMouseOut={() => setPlay(false)}
46
+      {...rest}
47
+    >
48
+      {children}
49
+    </button>
50
+  );
51
+};
52
+
53
+export default SoundButton;

+ 34
- 15
src/chords.tsx View File

@@ -1,6 +1,6 @@
1 1
 import { FingerCoordinate } from './types';
2 2
 
3
-const chords: { [name: string]: number[] } = {
3
+export const chords: { [name: string]: number[] } = {
4 4
   maj: [0, 4, 7],
5 5
   min: [0, 3, 7],
6 6
   '+': [0, 4, 8],
@@ -11,6 +11,22 @@ const chords: { [name: string]: number[] } = {
11 11
   min7: [0, 3, 7, 10],
12 12
 };
13 13
 
14
+export const notes: { [name: string]: number } = {
15
+  c: 0,
16
+  d: 2,
17
+  e: 4,
18
+  f: 5,
19
+  g: 7,
20
+  a: 9,
21
+  b: 10,
22
+  h: 11,
23
+};
24
+
25
+export const accidentals = {
26
+  '#': 1,
27
+  b: 11,
28
+};
29
+
14 30
 type NumberChord = {
15 31
   base: number;
16 32
   tones: number[];
@@ -46,9 +62,6 @@ const chord2numbers = (chord: string): NumberChord => {
46 62
   const note = chord.slice(0, separator);
47 63
   const chordType = chord.slice(separator);
48 64
 
49
-  // eslint-disable-next-line no-console
50
-  console.log(chordType, chordType in chords);
51
-
52 65
   if (!(chordType in chords)) {
53 66
     throw new Error("chord type doesn't exist");
54 67
   }
@@ -69,17 +82,6 @@ export const isChord = (chord: string): boolean => {
69 82
 };
70 83
 
71 84
 function note2number(note: string): number {
72
-  const notes: { [name: string]: number } = {
73
-    c: 0,
74
-    d: 2,
75
-    e: 4,
76
-    f: 5,
77
-    g: 7,
78
-    a: 9,
79
-    b: 10,
80
-    h: 11,
81
-  };
82
-
83 85
   if (!Object.keys(notes).includes(note[0]))
84 86
     throw new Error('invalid note name (c, d, e, f, g, a, b, h)');
85 87
 
@@ -96,3 +98,20 @@ function note2number(note: string): number {
96 98
 
97 99
   throw new Error('invalid note name');
98 100
 }
101
+
102
+export const chord2frequencies = (chord: string): number[] => {
103
+  const { base, tones } = chord2numbers(chord);
104
+
105
+  return [
106
+    tone2frequency(base) / 2,
107
+    ...tones.map(tone => tone2frequency((base + tone) % 12)),
108
+  ];
109
+};
110
+
111
+const tone2frequency = (tone: number): number => {
112
+  tone = tone >= 7 ? tone - 12 : tone;
113
+  // a === 9 === 440
114
+  const c = 440 * 2 ** (-9 / 12);
115
+
116
+  return c * 2 ** (tone / 12);
117
+};

+ 1
- 0
src/play.tsx View File

@@ -0,0 +1 @@
1
+export default async () => {};