note-listener 1.0.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.
Files changed (2) hide show
  1. package/package.json +17 -0
  2. package/src/index.js +161 -0
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "note-listener",
3
+ "version": "1.0.0",
4
+ "description": "simple pitch detection library for instruments using pitchy",
5
+ "main": "src/index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "keywords": [],
10
+ "author": "",
11
+ "license": "ISC",
12
+ "type": "module",
13
+ "dependencies": {
14
+ "pitchy": "^4.1.0"
15
+ }
16
+ }
17
+
package/src/index.js ADDED
@@ -0,0 +1,161 @@
1
+ import { PitchDetector } from "pitchy";
2
+
3
+ /* ====== Utilities (unchanged) ====== */
4
+
5
+ function freqToNote(freq) {
6
+ const A4 = 440;
7
+ const notes = [
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 n = Math.round(12 * Math.log2(freq / A4) + 69);
23
+ const name = notes[n % 12];
24
+ const octave = Math.floor(n / 12) - 1;
25
+
26
+ return name + octave;
27
+ }
28
+
29
+ function rms(buffer) {
30
+ let sum = 0;
31
+ for (let i = 0; i < buffer.length; i++) {
32
+ sum += buffer[i] * buffer[i];
33
+ }
34
+ return Math.sqrt(sum / buffer.length);
35
+ }
36
+
37
+ function getGuitar() {
38
+ return navigator.mediaDevices.getUserMedia({
39
+ audio: {
40
+ echoCancellation: false,
41
+ autoGainControl: false,
42
+ noiseSuppression: false,
43
+ latency: 0,
44
+ },
45
+ });
46
+ }
47
+
48
+ /* ====== PUBLIC API ====== */
49
+
50
+ export async function createPitchListener({
51
+ onNote,
52
+ minEnergy = 0.03,
53
+ clarityThreshold = 0.88,
54
+ minFreq = 82,
55
+ maxFreq = 1319,
56
+ } = {}) {
57
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
58
+ throw new Error("Browser does not support microphone access.");
59
+ }
60
+ if (!window.AudioContext && !window.webkitAudioContext) {
61
+ throw new Error("Browser does not support Web Audio API.");
62
+ }
63
+
64
+ const context = new AudioContext();
65
+ let guitar;
66
+ try {
67
+ guitar = await getGuitar();
68
+ } catch (e) {
69
+ console.warn("Microphone access not granted:", e.message);
70
+ return {
71
+ start() {
72
+ console.warn("Cannot start, no mic access.");
73
+ },
74
+ stop() {},
75
+ };
76
+ }
77
+
78
+ if (context.state === "suspended") {
79
+ await context.resume();
80
+ }
81
+
82
+ const source = context.createMediaStreamSource(guitar);
83
+
84
+ const filter = context.createBiquadFilter();
85
+ filter.type = "highpass";
86
+ filter.frequency.value = 70;
87
+
88
+ const analyser = context.createAnalyser();
89
+ analyser.fftSize = 2048;
90
+
91
+ source.connect(filter);
92
+ filter.connect(analyser);
93
+
94
+ const buffer = new Float32Array(analyser.fftSize);
95
+ const detector = PitchDetector.forFloat32Array(analyser.fftSize);
96
+
97
+ // variables for control and smoothing
98
+
99
+ const noteBuffer = [];
100
+ const bufferSize = 5;
101
+ let lastNote = null;
102
+ let running = false;
103
+
104
+ // helper function for smoothing
105
+
106
+ function mostCommon(arr) {
107
+ const counts = {};
108
+ arr.forEach((n) => (counts[n] = (counts[n] || 0) + 1));
109
+ let max = 0,
110
+ winner = null;
111
+ for (const n in counts) {
112
+ if (counts[n] > max) {
113
+ max = counts[n];
114
+ winner = n;
115
+ }
116
+ }
117
+ return winner;
118
+ }
119
+
120
+ function read() {
121
+ if (!running) return;
122
+
123
+ analyser.getFloatTimeDomainData(buffer);
124
+
125
+ const energy = rms(buffer);
126
+ if (energy >= minEnergy) {
127
+ const [freq, clarity] = detector.findPitch(buffer, context.sampleRate);
128
+
129
+ if (clarity >= clarityThreshold && freq >= minFreq && freq <= maxFreq) {
130
+ const note = freqToNote(freq);
131
+
132
+ noteBuffer.push(note);
133
+ if (noteBuffer.length > bufferSize) noteBuffer.shift();
134
+
135
+ const smoothedNote = mostCommon(noteBuffer);
136
+
137
+ if (smoothedNote !== lastNote) {
138
+ lastNote = smoothedNote;
139
+ onNote(smoothedNote, freq, clarity);
140
+ }
141
+ }
142
+ }
143
+
144
+ requestAnimationFrame(read);
145
+ }
146
+
147
+ return {
148
+ start() {
149
+ running = true;
150
+ read();
151
+ },
152
+ stop() {
153
+ running = false;
154
+ if (context && context.state !== "closed") {
155
+ context
156
+ .close()
157
+ .catch((err) => console.warn("Error closing audio context:", err));
158
+ }
159
+ },
160
+ };
161
+ }