sa2kit 1.4.2 → 1.5.1
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/ConfigService-7MEZXKJ5.js +21 -0
- package/dist/ConfigService-7MEZXKJ5.js.map +1 -0
- package/dist/ConfigService-BV57YYFW.mjs +4 -0
- package/dist/ConfigService-BV57YYFW.mjs.map +1 -0
- package/dist/ConfigService-BxK06xP6.d.mts +262 -0
- package/dist/ConfigService-BxK06xP6.d.ts +262 -0
- package/dist/audioDetection/index.d.mts +449 -0
- package/dist/audioDetection/index.d.ts +449 -0
- package/dist/audioDetection/index.js +1244 -0
- package/dist/audioDetection/index.js.map +1 -0
- package/dist/audioDetection/index.mjs +1227 -0
- package/dist/audioDetection/index.mjs.map +1 -0
- package/dist/chunk-5XUE72Y3.mjs +1001 -0
- package/dist/chunk-5XUE72Y3.mjs.map +1 -0
- package/dist/chunk-DQVPZTVC.js +1009 -0
- package/dist/chunk-DQVPZTVC.js.map +1 -0
- package/dist/chunk-NEPD75MX.mjs +467 -0
- package/dist/chunk-NEPD75MX.mjs.map +1 -0
- package/dist/chunk-OEDY7GI4.js +473 -0
- package/dist/chunk-OEDY7GI4.js.map +1 -0
- package/dist/chunk-TFQF2HDO.mjs +354 -0
- package/dist/chunk-TFQF2HDO.mjs.map +1 -0
- package/dist/chunk-TOC5FSHP.js +358 -0
- package/dist/chunk-TOC5FSHP.js.map +1 -0
- package/dist/imageCrop/index.d.mts +165 -0
- package/dist/imageCrop/index.d.ts +165 -0
- package/dist/imageCrop/index.js +559 -0
- package/dist/imageCrop/index.js.map +1 -0
- package/dist/imageCrop/index.mjs +540 -0
- package/dist/imageCrop/index.mjs.map +1 -0
- package/dist/index.d.mts +139 -0
- package/dist/index.d.ts +139 -0
- package/dist/index.js +670 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +662 -0
- package/dist/index.mjs.map +1 -1
- package/dist/mmd/index.d.mts +113 -2
- package/dist/mmd/index.d.ts +113 -2
- package/dist/mmd/index.js +484 -2
- package/dist/mmd/index.js.map +1 -1
- package/dist/mmd/index.mjs +482 -4
- package/dist/mmd/index.mjs.map +1 -1
- package/dist/testYourself/admin/index.d.mts +58 -0
- package/dist/testYourself/admin/index.d.ts +58 -0
- package/dist/testYourself/admin/index.js +17 -0
- package/dist/testYourself/admin/index.js.map +1 -0
- package/dist/testYourself/admin/index.mjs +4 -0
- package/dist/testYourself/admin/index.mjs.map +1 -0
- package/dist/testYourself/index.d.mts +6 -98
- package/dist/testYourself/index.d.ts +6 -98
- package/dist/testYourself/index.js +90 -334
- package/dist/testYourself/index.js.map +1 -1
- package/dist/testYourself/index.mjs +47 -333
- package/dist/testYourself/index.mjs.map +1 -1
- package/dist/testYourself/server/index.d.mts +1029 -0
- package/dist/testYourself/server/index.d.ts +1029 -0
- package/dist/testYourself/server/index.js +42 -0
- package/dist/testYourself/server/index.js.map +1 -0
- package/dist/testYourself/server/index.mjs +5 -0
- package/dist/testYourself/server/index.mjs.map +1 -0
- package/dist/universalFile/server/index.js +5 -5
- package/dist/universalFile/server/index.mjs +1 -1
- package/package.json +62 -20
|
@@ -0,0 +1,1227 @@
|
|
|
1
|
+
import '../chunk-BJTO5JO5.mjs';
|
|
2
|
+
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
3
|
+
|
|
4
|
+
// src/audioDetection/core/AudioInputService.ts
|
|
5
|
+
var AudioInputService = class {
|
|
6
|
+
constructor(config = {}) {
|
|
7
|
+
this.audioContext = null;
|
|
8
|
+
this.mediaStream = null;
|
|
9
|
+
this.sourceNode = null;
|
|
10
|
+
this.analyserNode = null;
|
|
11
|
+
this.state = "idle";
|
|
12
|
+
this.config = {
|
|
13
|
+
sampleRate: config.sampleRate ?? 44100,
|
|
14
|
+
fftSize: config.fftSize ?? 4096,
|
|
15
|
+
minVolume: config.minVolume ?? 1e-3,
|
|
16
|
+
// 降低默认阈值,使其更敏感
|
|
17
|
+
minConfidence: config.minConfidence ?? 0.5,
|
|
18
|
+
// 降低默认置信度,更容易检测
|
|
19
|
+
smoothing: config.smoothing ?? 0.8,
|
|
20
|
+
frequencyRange: config.frequencyRange ?? { min: 27.5, max: 4186 }
|
|
21
|
+
// A0 到 C8
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 初始化音频输入
|
|
26
|
+
* Initialize audio input
|
|
27
|
+
*/
|
|
28
|
+
async initialize() {
|
|
29
|
+
if (this.state === "active") {
|
|
30
|
+
console.warn("AudioInputService \u5DF2\u7ECF\u5904\u4E8E\u6D3B\u52A8\u72B6\u6001");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
this.setState("initializing");
|
|
35
|
+
this.mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
36
|
+
audio: {
|
|
37
|
+
echoCancellation: false,
|
|
38
|
+
noiseSuppression: false,
|
|
39
|
+
autoGainControl: false,
|
|
40
|
+
sampleRate: this.config.sampleRate
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
this.audioContext = new AudioContext({
|
|
44
|
+
sampleRate: this.config.sampleRate
|
|
45
|
+
});
|
|
46
|
+
this.sourceNode = this.audioContext.createMediaStreamSource(this.mediaStream);
|
|
47
|
+
this.analyserNode = this.audioContext.createAnalyser();
|
|
48
|
+
this.analyserNode.fftSize = this.config.fftSize;
|
|
49
|
+
this.analyserNode.smoothingTimeConstant = this.config.smoothing;
|
|
50
|
+
this.sourceNode.connect(this.analyserNode);
|
|
51
|
+
this.setState("active");
|
|
52
|
+
console.log("AudioInputService \u521D\u59CB\u5316\u6210\u529F");
|
|
53
|
+
} catch (error) {
|
|
54
|
+
this.setState("error");
|
|
55
|
+
console.error("AudioInputService \u521D\u59CB\u5316\u5931\u8D25:", error);
|
|
56
|
+
throw new Error(`\u9EA6\u514B\u98CE\u521D\u59CB\u5316\u5931\u8D25: ${error instanceof Error ? error.message : "\u672A\u77E5\u9519\u8BEF"}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 获取时域数据
|
|
61
|
+
* Get time domain data
|
|
62
|
+
*/
|
|
63
|
+
getTimeDomainData() {
|
|
64
|
+
if (!this.analyserNode) {
|
|
65
|
+
throw new Error("AnalyserNode \u672A\u521D\u59CB\u5316");
|
|
66
|
+
}
|
|
67
|
+
const bufferLength = this.analyserNode.fftSize;
|
|
68
|
+
const dataArray = new Float32Array(bufferLength);
|
|
69
|
+
this.analyserNode.getFloatTimeDomainData(dataArray);
|
|
70
|
+
return dataArray;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 获取频域数据
|
|
74
|
+
* Get frequency domain data
|
|
75
|
+
*/
|
|
76
|
+
getFrequencyData() {
|
|
77
|
+
if (!this.analyserNode) {
|
|
78
|
+
throw new Error("AnalyserNode \u672A\u521D\u59CB\u5316");
|
|
79
|
+
}
|
|
80
|
+
const bufferLength = this.analyserNode.frequencyBinCount;
|
|
81
|
+
const dataArray = new Float32Array(bufferLength);
|
|
82
|
+
this.analyserNode.getFloatFrequencyData(dataArray);
|
|
83
|
+
return dataArray;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 获取当前音量
|
|
87
|
+
* Get current volume
|
|
88
|
+
*/
|
|
89
|
+
getCurrentVolume() {
|
|
90
|
+
const timeDomainData = this.getTimeDomainData();
|
|
91
|
+
let sum = 0;
|
|
92
|
+
for (let i = 0; i < timeDomainData.length; i++) {
|
|
93
|
+
const val = timeDomainData[i];
|
|
94
|
+
if (val !== void 0) {
|
|
95
|
+
sum += val * val;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return Math.sqrt(sum / timeDomainData.length);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 停止音频输入
|
|
102
|
+
* Stop audio input
|
|
103
|
+
*/
|
|
104
|
+
stop() {
|
|
105
|
+
if (this.mediaStream) {
|
|
106
|
+
this.mediaStream.getTracks().forEach((track) => track.stop());
|
|
107
|
+
this.mediaStream = null;
|
|
108
|
+
}
|
|
109
|
+
if (this.sourceNode) {
|
|
110
|
+
this.sourceNode.disconnect();
|
|
111
|
+
this.sourceNode = null;
|
|
112
|
+
}
|
|
113
|
+
if (this.analyserNode) {
|
|
114
|
+
this.analyserNode.disconnect();
|
|
115
|
+
this.analyserNode = null;
|
|
116
|
+
}
|
|
117
|
+
if (this.audioContext) {
|
|
118
|
+
this.audioContext.close();
|
|
119
|
+
this.audioContext = null;
|
|
120
|
+
}
|
|
121
|
+
this.setState("stopped");
|
|
122
|
+
console.log("AudioInputService \u5DF2\u505C\u6B62");
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 获取当前状态
|
|
126
|
+
* Get current state
|
|
127
|
+
*/
|
|
128
|
+
getState() {
|
|
129
|
+
return this.state;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* 获取配置
|
|
133
|
+
* Get configuration
|
|
134
|
+
*/
|
|
135
|
+
getConfig() {
|
|
136
|
+
return { ...this.config };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 获取音频上下文
|
|
140
|
+
* Get audio context
|
|
141
|
+
*/
|
|
142
|
+
getAudioContext() {
|
|
143
|
+
return this.audioContext;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 获取分析器节点
|
|
147
|
+
* Get analyser node
|
|
148
|
+
*/
|
|
149
|
+
getAnalyserNode() {
|
|
150
|
+
return this.analyserNode;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* 设置状态
|
|
154
|
+
* Set state
|
|
155
|
+
*/
|
|
156
|
+
setState(state) {
|
|
157
|
+
this.state = state;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 检查是否正在运行
|
|
161
|
+
* Check if running
|
|
162
|
+
*/
|
|
163
|
+
isRunning() {
|
|
164
|
+
return this.state === "active";
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// src/audioDetection/core/PitchDetector.ts
|
|
169
|
+
var _PitchDetector = class _PitchDetector {
|
|
170
|
+
constructor(sampleRate = 44100, minFrequency = 27.5, maxFrequency = 4186) {
|
|
171
|
+
this.sampleRate = sampleRate;
|
|
172
|
+
this.minFrequency = minFrequency;
|
|
173
|
+
this.maxFrequency = maxFrequency;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 检测音高
|
|
177
|
+
* Detect pitch using autocorrelation
|
|
178
|
+
*/
|
|
179
|
+
detectPitch(audioBuffer, minVolume = 0.01) {
|
|
180
|
+
const volume = this.calculateRMS(audioBuffer);
|
|
181
|
+
if (volume < minVolume) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const frequency = this.autoCorrelate(audioBuffer);
|
|
185
|
+
if (frequency === -1 || frequency < this.minFrequency || frequency > this.maxFrequency) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
return frequency;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 自相关算法
|
|
192
|
+
* Autocorrelation algorithm
|
|
193
|
+
*/
|
|
194
|
+
autoCorrelate(buffer) {
|
|
195
|
+
const SIZE = buffer.length;
|
|
196
|
+
const MAX_SAMPLES = Math.floor(SIZE / 2);
|
|
197
|
+
let best_offset = -1;
|
|
198
|
+
let best_correlation = 0;
|
|
199
|
+
let rms = 0;
|
|
200
|
+
for (let i = 0; i < SIZE; i++) {
|
|
201
|
+
const val = buffer[i];
|
|
202
|
+
if (val !== void 0) {
|
|
203
|
+
rms += val * val;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
rms = Math.sqrt(rms / SIZE);
|
|
207
|
+
if (rms < 0.01) {
|
|
208
|
+
return -1;
|
|
209
|
+
}
|
|
210
|
+
let lastCorrelation = 1;
|
|
211
|
+
for (let offset = 0; offset < MAX_SAMPLES; offset++) {
|
|
212
|
+
let correlation = 0;
|
|
213
|
+
for (let i = 0; i < MAX_SAMPLES; i++) {
|
|
214
|
+
const val1 = buffer[i];
|
|
215
|
+
const val2 = buffer[i + offset];
|
|
216
|
+
if (val1 !== void 0 && val2 !== void 0) {
|
|
217
|
+
correlation += Math.abs(val1 - val2);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
correlation = 1 - correlation / MAX_SAMPLES;
|
|
221
|
+
if (correlation > 0.9 && correlation > lastCorrelation) {
|
|
222
|
+
const foundGoodCorrelation = correlation > best_correlation;
|
|
223
|
+
if (foundGoodCorrelation) {
|
|
224
|
+
best_correlation = correlation;
|
|
225
|
+
best_offset = offset;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
lastCorrelation = correlation;
|
|
229
|
+
}
|
|
230
|
+
if (best_correlation > 0.01 && best_offset !== -1) {
|
|
231
|
+
const frequency = this.sampleRate / best_offset;
|
|
232
|
+
return frequency;
|
|
233
|
+
}
|
|
234
|
+
return -1;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* 计算RMS音量
|
|
238
|
+
* Calculate RMS volume
|
|
239
|
+
*/
|
|
240
|
+
calculateRMS(buffer) {
|
|
241
|
+
let sum = 0;
|
|
242
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
243
|
+
const val = buffer[i];
|
|
244
|
+
if (val !== void 0) {
|
|
245
|
+
sum += val * val;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return Math.sqrt(sum / buffer.length);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* 将频率转换为音符信息
|
|
252
|
+
* Convert frequency to note information
|
|
253
|
+
*/
|
|
254
|
+
frequencyToNote(frequency, volume = 1) {
|
|
255
|
+
if (frequency <= 0) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
const midi = Math.round(12 * Math.log2(frequency / 440) + 69);
|
|
259
|
+
if (midi < 0 || midi > 127) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
const noteIndex = midi % 12;
|
|
263
|
+
const octave = Math.floor(midi / 12) - 1;
|
|
264
|
+
const noteName = _PitchDetector.NOTE_NAMES[noteIndex] || "C";
|
|
265
|
+
const name = `${noteName}${octave}`;
|
|
266
|
+
const standardFrequency = 440 * Math.pow(2, (midi - 69) / 12);
|
|
267
|
+
const cents = 1200 * Math.log2(frequency / standardFrequency);
|
|
268
|
+
const confidence = Math.max(0, 1 - Math.abs(cents) / 50);
|
|
269
|
+
return {
|
|
270
|
+
name,
|
|
271
|
+
frequency,
|
|
272
|
+
noteName,
|
|
273
|
+
octave,
|
|
274
|
+
midi,
|
|
275
|
+
volume,
|
|
276
|
+
confidence
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* 使用FFT检测主频率
|
|
281
|
+
* Detect dominant frequency using FFT
|
|
282
|
+
*/
|
|
283
|
+
detectDominantFrequency(frequencyData, minVolume = -60) {
|
|
284
|
+
const nyquist = this.sampleRate / 2;
|
|
285
|
+
const binWidth = nyquist / frequencyData.length;
|
|
286
|
+
let maxIndex = -1;
|
|
287
|
+
let maxValue = -Infinity;
|
|
288
|
+
const minBin = Math.floor(this.minFrequency / binWidth);
|
|
289
|
+
const maxBin = Math.ceil(this.maxFrequency / binWidth);
|
|
290
|
+
for (let i = minBin; i < Math.min(maxBin, frequencyData.length); i++) {
|
|
291
|
+
const value = frequencyData[i];
|
|
292
|
+
if (value !== void 0 && value > maxValue && value > minVolume) {
|
|
293
|
+
maxValue = value;
|
|
294
|
+
maxIndex = i;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (maxIndex === -1) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
const frequency = this.parabolicInterpolation(frequencyData, maxIndex, binWidth);
|
|
301
|
+
return frequency;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* 抛物线插值
|
|
305
|
+
* Parabolic interpolation for better frequency estimation
|
|
306
|
+
*/
|
|
307
|
+
parabolicInterpolation(data, index, binWidth) {
|
|
308
|
+
if (index <= 0 || index >= data.length - 1) {
|
|
309
|
+
return index * binWidth;
|
|
310
|
+
}
|
|
311
|
+
const y1 = data[index - 1];
|
|
312
|
+
const y2 = data[index];
|
|
313
|
+
const y3 = data[index + 1];
|
|
314
|
+
if (y1 === void 0 || y2 === void 0 || y3 === void 0) {
|
|
315
|
+
return index * binWidth;
|
|
316
|
+
}
|
|
317
|
+
const delta = 0.5 * (y3 - y1) / (2 * y2 - y1 - y3);
|
|
318
|
+
const interpolatedIndex = index + delta;
|
|
319
|
+
return interpolatedIndex * binWidth;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* 检测多个音高(和弦)
|
|
323
|
+
* Detect multiple pitches (chords)
|
|
324
|
+
*/
|
|
325
|
+
detectMultiplePitches(frequencyData, minVolume = -60) {
|
|
326
|
+
const notes = [];
|
|
327
|
+
const nyquist = this.sampleRate / 2;
|
|
328
|
+
const binWidth = nyquist / frequencyData.length;
|
|
329
|
+
const peaks = [];
|
|
330
|
+
for (let i = 1; i < frequencyData.length - 1; i++) {
|
|
331
|
+
const freq = i * binWidth;
|
|
332
|
+
if (freq < this.minFrequency || freq > this.maxFrequency) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const value = frequencyData[i];
|
|
336
|
+
if (value !== void 0 && value > minVolume && value > (frequencyData[i - 1] ?? -Infinity) && value > (frequencyData[i + 1] ?? -Infinity)) {
|
|
337
|
+
peaks.push({ index: i, value });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
peaks.sort((a, b) => b.value - a.value);
|
|
341
|
+
const topPeaks = peaks.slice(0, 5);
|
|
342
|
+
for (const peak of topPeaks) {
|
|
343
|
+
const frequency = this.parabolicInterpolation(frequencyData, peak.index, binWidth);
|
|
344
|
+
const volume = Math.pow(10, peak.value / 20);
|
|
345
|
+
const note = this.frequencyToNote(frequency, volume);
|
|
346
|
+
if (note) {
|
|
347
|
+
notes.push(note);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return notes;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
// 音符名称映射
|
|
354
|
+
_PitchDetector.NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
|
355
|
+
var PitchDetector = _PitchDetector;
|
|
356
|
+
|
|
357
|
+
// src/audioDetection/core/ChordRecognizer.ts
|
|
358
|
+
var _ChordRecognizer = class _ChordRecognizer {
|
|
359
|
+
/**
|
|
360
|
+
* 识别和弦
|
|
361
|
+
* Recognize chord from notes
|
|
362
|
+
*/
|
|
363
|
+
recognizeChord(notes) {
|
|
364
|
+
if (notes.length < 2) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
const sortedNotes = [...notes].sort((a, b) => a.midi - b.midi);
|
|
368
|
+
const uniqueNoteClasses = this.getUniqueNoteClasses(sortedNotes);
|
|
369
|
+
if (uniqueNoteClasses.length < 2) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
let bestMatch = null;
|
|
373
|
+
for (let i = 0; i < uniqueNoteClasses.length; i++) {
|
|
374
|
+
const root = uniqueNoteClasses[i];
|
|
375
|
+
if (!root) continue;
|
|
376
|
+
const intervals = this.calculateIntervals(root, uniqueNoteClasses);
|
|
377
|
+
for (const pattern of _ChordRecognizer.CHORD_PATTERNS) {
|
|
378
|
+
const confidence = this.matchPattern(intervals, pattern.intervals);
|
|
379
|
+
if (confidence > 0.8 && (!bestMatch || confidence > bestMatch.confidence)) {
|
|
380
|
+
bestMatch = { root, pattern, confidence };
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (!bestMatch) {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
const chordName = `${bestMatch.root}${this.getChordSuffix(bestMatch.pattern)}`;
|
|
388
|
+
return {
|
|
389
|
+
name: chordName,
|
|
390
|
+
root: bestMatch.root,
|
|
391
|
+
type: bestMatch.pattern.name,
|
|
392
|
+
notes: sortedNotes,
|
|
393
|
+
confidence: bestMatch.confidence
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* 获取唯一的音符类别(不考虑八度)
|
|
398
|
+
* Get unique note classes (ignoring octave)
|
|
399
|
+
*/
|
|
400
|
+
getUniqueNoteClasses(notes) {
|
|
401
|
+
const noteSet = /* @__PURE__ */ new Set();
|
|
402
|
+
for (const note of notes) {
|
|
403
|
+
noteSet.add(note.noteName);
|
|
404
|
+
}
|
|
405
|
+
return Array.from(noteSet);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* 计算音程
|
|
409
|
+
* Calculate intervals from root
|
|
410
|
+
*/
|
|
411
|
+
calculateIntervals(root, noteClasses) {
|
|
412
|
+
const rootIndex = _ChordRecognizer.NOTE_NAMES.indexOf(root);
|
|
413
|
+
const intervals = [];
|
|
414
|
+
for (const noteClass of noteClasses) {
|
|
415
|
+
const noteIndex = _ChordRecognizer.NOTE_NAMES.indexOf(noteClass);
|
|
416
|
+
let interval = (noteIndex - rootIndex + 12) % 12;
|
|
417
|
+
intervals.push(interval);
|
|
418
|
+
}
|
|
419
|
+
return intervals.sort((a, b) => a - b);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* 匹配和弦模式
|
|
423
|
+
* Match chord pattern
|
|
424
|
+
*/
|
|
425
|
+
matchPattern(intervals, pattern) {
|
|
426
|
+
if (intervals.length < pattern.length) {
|
|
427
|
+
return 0;
|
|
428
|
+
}
|
|
429
|
+
let matches = 0;
|
|
430
|
+
for (const patternInterval of pattern) {
|
|
431
|
+
if (intervals.includes(patternInterval)) {
|
|
432
|
+
matches++;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const precision = matches / pattern.length;
|
|
436
|
+
const recall = matches / intervals.length;
|
|
437
|
+
if (precision + recall === 0) {
|
|
438
|
+
return 0;
|
|
439
|
+
}
|
|
440
|
+
return 2 * precision * recall / (precision + recall);
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* 获取和弦后缀
|
|
444
|
+
* Get chord suffix for display
|
|
445
|
+
*/
|
|
446
|
+
getChordSuffix(pattern) {
|
|
447
|
+
if (pattern.aliases && pattern.aliases.length > 0) {
|
|
448
|
+
return pattern.aliases[0] || "";
|
|
449
|
+
}
|
|
450
|
+
const suffixMap = {
|
|
451
|
+
"major": "",
|
|
452
|
+
"minor": "m",
|
|
453
|
+
"diminished": "dim",
|
|
454
|
+
"augmented": "aug",
|
|
455
|
+
"dominant7": "7"
|
|
456
|
+
};
|
|
457
|
+
return suffixMap[pattern.name] ?? pattern.name;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* 识别八度内的音程(用于简单的音程识别)
|
|
461
|
+
* Recognize interval within an octave
|
|
462
|
+
*/
|
|
463
|
+
recognizeInterval(note1, note2) {
|
|
464
|
+
const semitones = Math.abs(note1.midi - note2.midi) % 12;
|
|
465
|
+
const intervalNames = {
|
|
466
|
+
0: "\u7EAF\u4E00\u5EA6 (Unison)",
|
|
467
|
+
1: "\u5C0F\u4E8C\u5EA6 (Minor 2nd)",
|
|
468
|
+
2: "\u5927\u4E8C\u5EA6 (Major 2nd)",
|
|
469
|
+
3: "\u5C0F\u4E09\u5EA6 (Minor 3rd)",
|
|
470
|
+
4: "\u5927\u4E09\u5EA6 (Major 3rd)",
|
|
471
|
+
5: "\u7EAF\u56DB\u5EA6 (Perfect 4th)",
|
|
472
|
+
6: "\u589E\u56DB\u5EA6/\u51CF\u4E94\u5EA6 (Tritone)",
|
|
473
|
+
7: "\u7EAF\u4E94\u5EA6 (Perfect 5th)",
|
|
474
|
+
8: "\u5C0F\u516D\u5EA6 (Minor 6th)",
|
|
475
|
+
9: "\u5927\u516D\u5EA6 (Major 6th)",
|
|
476
|
+
10: "\u5C0F\u4E03\u5EA6 (Minor 7th)",
|
|
477
|
+
11: "\u5927\u4E03\u5EA6 (Major 7th)"
|
|
478
|
+
};
|
|
479
|
+
return intervalNames[semitones] || "\u672A\u77E5\u97F3\u7A0B";
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* 分析和弦质量
|
|
483
|
+
* Analyze chord quality
|
|
484
|
+
*/
|
|
485
|
+
analyzeChordQuality(chord) {
|
|
486
|
+
const qualities = [];
|
|
487
|
+
if (chord.type.includes("major")) {
|
|
488
|
+
qualities.push("\u5927\u8C03");
|
|
489
|
+
} else if (chord.type.includes("minor")) {
|
|
490
|
+
qualities.push("\u5C0F\u8C03");
|
|
491
|
+
}
|
|
492
|
+
if (chord.type.includes("diminished")) {
|
|
493
|
+
qualities.push("\u51CF");
|
|
494
|
+
} else if (chord.type.includes("augmented")) {
|
|
495
|
+
qualities.push("\u589E");
|
|
496
|
+
}
|
|
497
|
+
if (chord.type.includes("7")) {
|
|
498
|
+
qualities.push("\u4E03\u548C\u5F26");
|
|
499
|
+
} else if (chord.type.includes("9")) {
|
|
500
|
+
qualities.push("\u4E5D\u548C\u5F26");
|
|
501
|
+
}
|
|
502
|
+
if (chord.type.includes("sus")) {
|
|
503
|
+
qualities.push("\u6302\u7559");
|
|
504
|
+
}
|
|
505
|
+
return qualities.join(" ") || "\u57FA\u7840\u548C\u5F26";
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
// 和弦模式定义
|
|
509
|
+
_ChordRecognizer.CHORD_PATTERNS = [
|
|
510
|
+
// 三和弦
|
|
511
|
+
{ name: "major", intervals: [0, 4, 7], aliases: ["maj", "M"] },
|
|
512
|
+
{ name: "minor", intervals: [0, 3, 7], aliases: ["min", "m"] },
|
|
513
|
+
{ name: "diminished", intervals: [0, 3, 6], aliases: ["dim", "\xB0"] },
|
|
514
|
+
{ name: "augmented", intervals: [0, 4, 8], aliases: ["aug", "+"] },
|
|
515
|
+
{ name: "sus2", intervals: [0, 2, 7] },
|
|
516
|
+
{ name: "sus4", intervals: [0, 5, 7] },
|
|
517
|
+
// 七和弦
|
|
518
|
+
{ name: "major7", intervals: [0, 4, 7, 11], aliases: ["maj7", "M7"] },
|
|
519
|
+
{ name: "minor7", intervals: [0, 3, 7, 10], aliases: ["min7", "m7"] },
|
|
520
|
+
{ name: "dominant7", intervals: [0, 4, 7, 10], aliases: ["7"] },
|
|
521
|
+
{ name: "diminished7", intervals: [0, 3, 6, 9], aliases: ["dim7", "\xB07"] },
|
|
522
|
+
{ name: "half-diminished7", intervals: [0, 3, 6, 10], aliases: ["m7b5", "\xF87"] },
|
|
523
|
+
{ name: "minor-major7", intervals: [0, 3, 7, 11], aliases: ["mM7", "m(maj7)"] },
|
|
524
|
+
{ name: "augmented7", intervals: [0, 4, 8, 10], aliases: ["aug7", "+7"] },
|
|
525
|
+
// 扩展和弦
|
|
526
|
+
{ name: "major9", intervals: [0, 4, 7, 11, 14], aliases: ["maj9", "M9"] },
|
|
527
|
+
{ name: "minor9", intervals: [0, 3, 7, 10, 14], aliases: ["min9", "m9"] },
|
|
528
|
+
{ name: "dominant9", intervals: [0, 4, 7, 10, 14], aliases: ["9"] },
|
|
529
|
+
{ name: "add9", intervals: [0, 4, 7, 14] },
|
|
530
|
+
{ name: "madd9", intervals: [0, 3, 7, 14], aliases: ["m(add9)"] }
|
|
531
|
+
];
|
|
532
|
+
_ChordRecognizer.NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
|
533
|
+
var ChordRecognizer = _ChordRecognizer;
|
|
534
|
+
|
|
535
|
+
// src/audioDetection/core/AudioDetector.ts
|
|
536
|
+
var AudioDetector = class {
|
|
537
|
+
constructor(config = {}, events = {}) {
|
|
538
|
+
this.isDetecting = false;
|
|
539
|
+
this.animationFrameId = null;
|
|
540
|
+
this.lastResult = null;
|
|
541
|
+
/**
|
|
542
|
+
* 检测循环
|
|
543
|
+
* Detection loop
|
|
544
|
+
*/
|
|
545
|
+
this.detectLoop = () => {
|
|
546
|
+
if (!this.isDetecting) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
const result = this.detect();
|
|
551
|
+
if (result) {
|
|
552
|
+
this.lastResult = result;
|
|
553
|
+
this.events.onDetection?.(result);
|
|
554
|
+
}
|
|
555
|
+
} catch (error) {
|
|
556
|
+
console.error("\u68C0\u6D4B\u8FC7\u7A0B\u4E2D\u51FA\u9519:", error);
|
|
557
|
+
this.events.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
558
|
+
}
|
|
559
|
+
this.animationFrameId = requestAnimationFrame(this.detectLoop);
|
|
560
|
+
};
|
|
561
|
+
this.audioInput = new AudioInputService(config);
|
|
562
|
+
const audioConfig = this.audioInput.getConfig();
|
|
563
|
+
this.pitchDetector = new PitchDetector(
|
|
564
|
+
audioConfig.sampleRate,
|
|
565
|
+
audioConfig.frequencyRange.min,
|
|
566
|
+
audioConfig.frequencyRange.max
|
|
567
|
+
);
|
|
568
|
+
this.chordRecognizer = new ChordRecognizer();
|
|
569
|
+
this.events = events;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* 启动音频检测
|
|
573
|
+
* Start audio detection
|
|
574
|
+
*/
|
|
575
|
+
async start() {
|
|
576
|
+
if (this.isDetecting) {
|
|
577
|
+
console.warn("AudioDetector \u5DF2\u7ECF\u5728\u8FD0\u884C");
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
await this.audioInput.initialize();
|
|
582
|
+
this.events.onStateChange?.("active");
|
|
583
|
+
this.isDetecting = true;
|
|
584
|
+
this.detectLoop();
|
|
585
|
+
console.log("AudioDetector \u5DF2\u542F\u52A8");
|
|
586
|
+
} catch (error) {
|
|
587
|
+
this.events.onStateChange?.("error");
|
|
588
|
+
this.events.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
589
|
+
throw error;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* 停止音频检测
|
|
594
|
+
* Stop audio detection
|
|
595
|
+
*/
|
|
596
|
+
stop() {
|
|
597
|
+
if (!this.isDetecting) {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
this.isDetecting = false;
|
|
601
|
+
if (this.animationFrameId !== null) {
|
|
602
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
603
|
+
this.animationFrameId = null;
|
|
604
|
+
}
|
|
605
|
+
this.audioInput.stop();
|
|
606
|
+
this.events.onStateChange?.("stopped");
|
|
607
|
+
console.log("AudioDetector \u5DF2\u505C\u6B62");
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* 执行一次检测
|
|
611
|
+
* Perform single detection
|
|
612
|
+
*/
|
|
613
|
+
detect() {
|
|
614
|
+
const config = this.audioInput.getConfig();
|
|
615
|
+
const timeDomainData = this.audioInput.getTimeDomainData();
|
|
616
|
+
const frequencyData = this.audioInput.getFrequencyData();
|
|
617
|
+
const volume = this.audioInput.getCurrentVolume();
|
|
618
|
+
if (Date.now() % 2e3 < 100) {
|
|
619
|
+
console.log(`[AudioDetector] \u5F53\u524D\u97F3\u91CF: ${volume.toFixed(6)}, \u9608\u503C: ${config.minVolume}, \u72B6\u6001: ${volume >= config.minVolume ? "\u2705 \u6709\u58F0\u97F3" : "\u274C \u97F3\u91CF\u592A\u4F4E"}`);
|
|
620
|
+
}
|
|
621
|
+
if (volume < config.minVolume) {
|
|
622
|
+
return {
|
|
623
|
+
notes: [],
|
|
624
|
+
timestamp: Date.now(),
|
|
625
|
+
isDetecting: false
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const dominantFrequency = this.pitchDetector.detectPitch(timeDomainData, config.minVolume);
|
|
629
|
+
const detectedNotes = this.pitchDetector.detectMultiplePitches(frequencyData, -60);
|
|
630
|
+
if (dominantFrequency && dominantFrequency > 0) {
|
|
631
|
+
const dominantNote = this.pitchDetector.frequencyToNote(dominantFrequency, volume);
|
|
632
|
+
if (dominantNote) {
|
|
633
|
+
const exists = detectedNotes.some((n) => Math.abs(n.midi - dominantNote.midi) < 1);
|
|
634
|
+
if (!exists) {
|
|
635
|
+
detectedNotes.unshift(dominantNote);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const filteredNotes = detectedNotes.filter((note) => note.confidence >= config.minConfidence);
|
|
640
|
+
let chord;
|
|
641
|
+
if (filteredNotes.length >= 2) {
|
|
642
|
+
chord = this.chordRecognizer.recognizeChord(filteredNotes) ?? void 0;
|
|
643
|
+
}
|
|
644
|
+
return {
|
|
645
|
+
notes: filteredNotes,
|
|
646
|
+
chord,
|
|
647
|
+
timestamp: Date.now(),
|
|
648
|
+
isDetecting: filteredNotes.length > 0
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* 获取当前状态
|
|
653
|
+
* Get current state
|
|
654
|
+
*/
|
|
655
|
+
getState() {
|
|
656
|
+
return this.audioInput.getState();
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* 获取最后的检测结果
|
|
660
|
+
* Get last detection result
|
|
661
|
+
*/
|
|
662
|
+
getLastResult() {
|
|
663
|
+
return this.lastResult;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* 检查是否正在运行
|
|
667
|
+
* Check if running
|
|
668
|
+
*/
|
|
669
|
+
isRunning() {
|
|
670
|
+
return this.isDetecting && this.audioInput.isRunning();
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* 获取音频输入服务
|
|
674
|
+
* Get audio input service
|
|
675
|
+
*/
|
|
676
|
+
getAudioInput() {
|
|
677
|
+
return this.audioInput;
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
// src/audioDetection/core/AudioDetectorDebugger.ts
|
|
682
|
+
var AudioDetectorDebugger = class {
|
|
683
|
+
constructor(config = {}) {
|
|
684
|
+
this.debugInterval = null;
|
|
685
|
+
this.detector = new AudioDetector(config, {
|
|
686
|
+
onDetection: (result) => {
|
|
687
|
+
if (result.isDetecting) {
|
|
688
|
+
console.log("\u{1F3B5} \u68C0\u6D4B\u5230\u97F3\u7B26:", result.notes.map((n) => `${n.name}(${n.frequency.toFixed(1)}Hz)`).join(", "));
|
|
689
|
+
if (result.chord) {
|
|
690
|
+
console.log("\u{1F3B9} \u68C0\u6D4B\u5230\u548C\u5F26:", result.chord.name);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
onStateChange: (state) => {
|
|
695
|
+
console.log("\u{1F4CA} \u72B6\u6001\u53D8\u5316:", state);
|
|
696
|
+
},
|
|
697
|
+
onError: (error) => {
|
|
698
|
+
console.error("\u274C \u9519\u8BEF:", error);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
async start() {
|
|
703
|
+
await this.detector.start();
|
|
704
|
+
this.debugInterval = window.setInterval(() => {
|
|
705
|
+
const audioInput = this.detector.getAudioInput();
|
|
706
|
+
const volume = audioInput.getCurrentVolume();
|
|
707
|
+
const config = audioInput.getConfig();
|
|
708
|
+
const analyser = audioInput.getAnalyserNode();
|
|
709
|
+
if (analyser) {
|
|
710
|
+
const freqData = new Float32Array(analyser.frequencyBinCount);
|
|
711
|
+
analyser.getFloatFrequencyData(freqData);
|
|
712
|
+
let maxFreqValue = -Infinity;
|
|
713
|
+
for (let i = 0; i < freqData.length; i++) {
|
|
714
|
+
const val = freqData[i];
|
|
715
|
+
if (val !== void 0 && val > maxFreqValue) {
|
|
716
|
+
maxFreqValue = val;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
console.log(`
|
|
720
|
+
\u{1F3A4} \u97F3\u9891\u8C03\u8BD5\u4FE1\u606F:
|
|
721
|
+
- \u97F3\u91CF (RMS): ${volume.toFixed(6)}
|
|
722
|
+
- \u97F3\u91CF\u9608\u503C: ${config.minVolume}
|
|
723
|
+
- \u97F3\u91CF\u72B6\u6001: ${volume >= config.minVolume ? "\u2705 \u8D85\u8FC7\u9608\u503C" : "\u274C \u4F4E\u4E8E\u9608\u503C"}
|
|
724
|
+
- \u6700\u5927\u9891\u7387\u5F3A\u5EA6: ${maxFreqValue.toFixed(2)} dB
|
|
725
|
+
- \u91C7\u6837\u7387: ${config.sampleRate} Hz
|
|
726
|
+
- FFT\u5927\u5C0F: ${config.fftSize}
|
|
727
|
+
- \u5E73\u6ED1\u7CFB\u6570: ${config.smoothing}
|
|
728
|
+
`);
|
|
729
|
+
}
|
|
730
|
+
}, 2e3);
|
|
731
|
+
}
|
|
732
|
+
stop() {
|
|
733
|
+
if (this.debugInterval !== null) {
|
|
734
|
+
clearInterval(this.debugInterval);
|
|
735
|
+
this.debugInterval = null;
|
|
736
|
+
}
|
|
737
|
+
this.detector.stop();
|
|
738
|
+
}
|
|
739
|
+
getDetector() {
|
|
740
|
+
return this.detector;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* 测试麦克风是否正常工作
|
|
744
|
+
*/
|
|
745
|
+
async testMicrophone() {
|
|
746
|
+
console.log("\u{1F50D} \u5F00\u59CB\u6D4B\u8BD5\u9EA6\u514B\u98CE...");
|
|
747
|
+
await this.detector.start();
|
|
748
|
+
const volumes = [];
|
|
749
|
+
const audioInput = this.detector.getAudioInput();
|
|
750
|
+
return new Promise((resolve) => {
|
|
751
|
+
const sampleInterval = setInterval(() => {
|
|
752
|
+
volumes.push(audioInput.getCurrentVolume());
|
|
753
|
+
}, 100);
|
|
754
|
+
setTimeout(() => {
|
|
755
|
+
clearInterval(sampleInterval);
|
|
756
|
+
const averageVolume = volumes.reduce((a, b) => a + b, 0) / volumes.length;
|
|
757
|
+
const peakVolume = Math.max(...volumes);
|
|
758
|
+
const isReceivingAudio = peakVolume > 1e-4;
|
|
759
|
+
console.log(`
|
|
760
|
+
\u2705 \u9EA6\u514B\u98CE\u6D4B\u8BD5\u5B8C\u6210:
|
|
761
|
+
- \u6743\u9650\u72B6\u6001: \u2705 \u5DF2\u6388\u6743
|
|
762
|
+
- \u63A5\u6536\u97F3\u9891: ${isReceivingAudio ? "\u2705 \u662F" : "\u274C \u5426"}
|
|
763
|
+
- \u5E73\u5747\u97F3\u91CF: ${averageVolume.toFixed(6)}
|
|
764
|
+
- \u5CF0\u503C\u97F3\u91CF: ${peakVolume.toFixed(6)}
|
|
765
|
+
- \u5EFA\u8BAE\u9608\u503C: ${(peakVolume * 0.1).toFixed(6)}
|
|
766
|
+
`);
|
|
767
|
+
resolve({
|
|
768
|
+
hasPermission: true,
|
|
769
|
+
isReceivingAudio,
|
|
770
|
+
averageVolume,
|
|
771
|
+
peakVolume
|
|
772
|
+
});
|
|
773
|
+
}, 5e3);
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
async function debugAudioDetection(config) {
|
|
778
|
+
const dbg = new AudioDetectorDebugger(config);
|
|
779
|
+
await dbg.start();
|
|
780
|
+
return dbg;
|
|
781
|
+
}
|
|
782
|
+
async function testMicrophone() {
|
|
783
|
+
const dbg = new AudioDetectorDebugger();
|
|
784
|
+
await dbg.testMicrophone();
|
|
785
|
+
dbg.stop();
|
|
786
|
+
}
|
|
787
|
+
function useAudioDetection(options = {}) {
|
|
788
|
+
const { autoStart = false, updateInterval = 100, ...config } = options;
|
|
789
|
+
const [result, setResult] = useState(null);
|
|
790
|
+
const [state, setState] = useState("idle");
|
|
791
|
+
const [error, setError] = useState(null);
|
|
792
|
+
const [isDetecting, setIsDetecting] = useState(false);
|
|
793
|
+
const detectorRef = useRef(null);
|
|
794
|
+
const lastUpdateRef = useRef(0);
|
|
795
|
+
useEffect(() => {
|
|
796
|
+
const detector = new AudioDetector(config, {
|
|
797
|
+
onDetection: (detectionResult) => {
|
|
798
|
+
const now = Date.now();
|
|
799
|
+
if (now - lastUpdateRef.current >= updateInterval) {
|
|
800
|
+
setResult(detectionResult);
|
|
801
|
+
lastUpdateRef.current = now;
|
|
802
|
+
}
|
|
803
|
+
},
|
|
804
|
+
onStateChange: (newState) => {
|
|
805
|
+
setState(newState);
|
|
806
|
+
},
|
|
807
|
+
onError: (err) => {
|
|
808
|
+
setError(err);
|
|
809
|
+
setIsDetecting(false);
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
detectorRef.current = detector;
|
|
813
|
+
if (autoStart) {
|
|
814
|
+
detector.start().catch((err) => {
|
|
815
|
+
setError(err);
|
|
816
|
+
});
|
|
817
|
+
setIsDetecting(true);
|
|
818
|
+
}
|
|
819
|
+
return () => {
|
|
820
|
+
if (detector.isRunning()) {
|
|
821
|
+
detector.stop();
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
}, []);
|
|
825
|
+
const start = useCallback(async () => {
|
|
826
|
+
if (!detectorRef.current) {
|
|
827
|
+
const err = new Error("\u68C0\u6D4B\u5668\u672A\u521D\u59CB\u5316");
|
|
828
|
+
setError(err);
|
|
829
|
+
throw err;
|
|
830
|
+
}
|
|
831
|
+
try {
|
|
832
|
+
setError(null);
|
|
833
|
+
await detectorRef.current.start();
|
|
834
|
+
setIsDetecting(true);
|
|
835
|
+
} catch (err) {
|
|
836
|
+
const error2 = err instanceof Error ? err : new Error(String(err));
|
|
837
|
+
setError(error2);
|
|
838
|
+
setIsDetecting(false);
|
|
839
|
+
throw error2;
|
|
840
|
+
}
|
|
841
|
+
}, []);
|
|
842
|
+
const stop = useCallback(() => {
|
|
843
|
+
if (detectorRef.current) {
|
|
844
|
+
detectorRef.current.stop();
|
|
845
|
+
setIsDetecting(false);
|
|
846
|
+
setResult(null);
|
|
847
|
+
}
|
|
848
|
+
}, []);
|
|
849
|
+
const getDetector = useCallback(() => {
|
|
850
|
+
return detectorRef.current;
|
|
851
|
+
}, []);
|
|
852
|
+
return {
|
|
853
|
+
result,
|
|
854
|
+
state,
|
|
855
|
+
isDetecting,
|
|
856
|
+
error,
|
|
857
|
+
start,
|
|
858
|
+
stop,
|
|
859
|
+
getDetector
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
var AudioDetectionDisplay = ({
|
|
863
|
+
className = "",
|
|
864
|
+
showDebugInfo = false,
|
|
865
|
+
renderNote,
|
|
866
|
+
renderChord,
|
|
867
|
+
startButtonText = "\u5F00\u59CB\u68C0\u6D4B",
|
|
868
|
+
stopButtonText = "\u505C\u6B62\u68C0\u6D4B",
|
|
869
|
+
...options
|
|
870
|
+
}) => {
|
|
871
|
+
const { result, state, isDetecting, error, start, stop } = useAudioDetection(options);
|
|
872
|
+
const handleToggle = async () => {
|
|
873
|
+
if (isDetecting) {
|
|
874
|
+
stop();
|
|
875
|
+
} else {
|
|
876
|
+
try {
|
|
877
|
+
await start();
|
|
878
|
+
} catch (err) {
|
|
879
|
+
console.error("\u542F\u52A8\u5931\u8D25:", err);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
return /* @__PURE__ */ React.createElement("div", { className: `audio-detection-display ${className}` }, /* @__PURE__ */ React.createElement("div", { className: "audio-detection-controls" }, /* @__PURE__ */ React.createElement(
|
|
884
|
+
"button",
|
|
885
|
+
{
|
|
886
|
+
onClick: handleToggle,
|
|
887
|
+
disabled: state === "initializing",
|
|
888
|
+
className: `audio-detection-button ${isDetecting ? "active" : ""}`
|
|
889
|
+
},
|
|
890
|
+
state === "initializing" ? "\u521D\u59CB\u5316\u4E2D..." : isDetecting ? stopButtonText : startButtonText
|
|
891
|
+
), /* @__PURE__ */ React.createElement("div", { className: `audio-detection-status status-${state}` }, "\u72B6\u6001: ", getStateLabel(state))), error && /* @__PURE__ */ React.createElement("div", { className: "audio-detection-error" }, /* @__PURE__ */ React.createElement("strong", null, "\u9519\u8BEF:"), " ", error.message), result && result.isDetecting && /* @__PURE__ */ React.createElement("div", { className: "audio-detection-result" }, result.notes.length > 0 && /* @__PURE__ */ React.createElement("div", { className: "audio-detection-notes" }, /* @__PURE__ */ React.createElement("h3", null, "\u68C0\u6D4B\u5230\u7684\u97F3\u7B26:"), /* @__PURE__ */ React.createElement("div", { className: "notes-grid" }, result.notes.map((note, index) => /* @__PURE__ */ React.createElement("div", { key: index, className: "note-item" }, renderNote ? renderNote(note) : /* @__PURE__ */ React.createElement(DefaultNoteDisplay, { note }))))), result.chord && /* @__PURE__ */ React.createElement("div", { className: "audio-detection-chord" }, /* @__PURE__ */ React.createElement("h3", null, "\u8BC6\u522B\u7684\u548C\u5F26:"), renderChord ? renderChord(result.chord) : /* @__PURE__ */ React.createElement(DefaultChordDisplay, { chord: result.chord })), showDebugInfo && /* @__PURE__ */ React.createElement("div", { className: "audio-detection-debug" }, /* @__PURE__ */ React.createElement("h4", null, "\u8C03\u8BD5\u4FE1\u606F:"), /* @__PURE__ */ React.createElement("pre", null, JSON.stringify(result, null, 2)))), isDetecting && (!result || !result.isDetecting) && /* @__PURE__ */ React.createElement("div", { className: "audio-detection-waiting" }, "\u6B63\u5728\u76D1\u542C... \u8BF7\u5F39\u594F\u7535\u5B50\u7434"));
|
|
892
|
+
};
|
|
893
|
+
var DefaultNoteDisplay = ({ note }) => /* @__PURE__ */ React.createElement("div", { className: "default-note-display" }, /* @__PURE__ */ React.createElement("div", { className: "note-name" }, note.name), /* @__PURE__ */ React.createElement("div", { className: "note-frequency" }, note.frequency.toFixed(2), " Hz"), /* @__PURE__ */ React.createElement("div", { className: "note-confidence" }, "\u7F6E\u4FE1\u5EA6: ", (note.confidence * 100).toFixed(0), "%"));
|
|
894
|
+
var DefaultChordDisplay = ({ chord }) => /* @__PURE__ */ React.createElement("div", { className: "default-chord-display" }, /* @__PURE__ */ React.createElement("div", { className: "chord-name" }, chord.name), /* @__PURE__ */ React.createElement("div", { className: "chord-type" }, "\u7C7B\u578B: ", chord.type), /* @__PURE__ */ React.createElement("div", { className: "chord-notes" }, "\u97F3\u7B26: ", chord.notes.map((n) => n.name).join(", ")), /* @__PURE__ */ React.createElement("div", { className: "chord-confidence" }, "\u7F6E\u4FE1\u5EA6: ", (chord.confidence * 100).toFixed(0), "%"));
|
|
895
|
+
function getStateLabel(state) {
|
|
896
|
+
const labels = {
|
|
897
|
+
idle: "\u7A7A\u95F2",
|
|
898
|
+
initializing: "\u521D\u59CB\u5316\u4E2D",
|
|
899
|
+
active: "\u8FD0\u884C\u4E2D",
|
|
900
|
+
error: "\u9519\u8BEF",
|
|
901
|
+
stopped: "\u5DF2\u505C\u6B62"
|
|
902
|
+
};
|
|
903
|
+
return labels[state] || state;
|
|
904
|
+
}
|
|
905
|
+
var audioDetectionStyles = `
|
|
906
|
+
.audio-detection-display {
|
|
907
|
+
padding: 20px;
|
|
908
|
+
border: 1px solid #ddd;
|
|
909
|
+
border-radius: 8px;
|
|
910
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
.audio-detection-controls {
|
|
914
|
+
display: flex;
|
|
915
|
+
gap: 12px;
|
|
916
|
+
align-items: center;
|
|
917
|
+
margin-bottom: 20px;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
.audio-detection-button {
|
|
921
|
+
padding: 10px 20px;
|
|
922
|
+
font-size: 16px;
|
|
923
|
+
border: none;
|
|
924
|
+
border-radius: 6px;
|
|
925
|
+
background-color: #007bff;
|
|
926
|
+
color: white;
|
|
927
|
+
cursor: pointer;
|
|
928
|
+
transition: background-color 0.3s;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
.audio-detection-button:hover {
|
|
932
|
+
background-color: #0056b3;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
.audio-detection-button:disabled {
|
|
936
|
+
background-color: #ccc;
|
|
937
|
+
cursor: not-allowed;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
.audio-detection-button.active {
|
|
941
|
+
background-color: #dc3545;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
.audio-detection-button.active:hover {
|
|
945
|
+
background-color: #c82333;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
.audio-detection-status {
|
|
949
|
+
padding: 8px 16px;
|
|
950
|
+
border-radius: 4px;
|
|
951
|
+
font-size: 14px;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
.status-idle { background-color: #e7f3ff; color: #004085; }
|
|
955
|
+
.status-initializing { background-color: #fff3cd; color: #856404; }
|
|
956
|
+
.status-active { background-color: #d4edda; color: #155724; }
|
|
957
|
+
.status-error { background-color: #f8d7da; color: #721c24; }
|
|
958
|
+
.status-stopped { background-color: #e2e3e5; color: #383d41; }
|
|
959
|
+
|
|
960
|
+
.audio-detection-error {
|
|
961
|
+
padding: 12px;
|
|
962
|
+
background-color: #f8d7da;
|
|
963
|
+
border: 1px solid #f5c6cb;
|
|
964
|
+
border-radius: 4px;
|
|
965
|
+
color: #721c24;
|
|
966
|
+
margin-bottom: 16px;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
.audio-detection-result {
|
|
970
|
+
margin-top: 20px;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
.audio-detection-notes h3,
|
|
974
|
+
.audio-detection-chord h3 {
|
|
975
|
+
margin-bottom: 12px;
|
|
976
|
+
font-size: 18px;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
.notes-grid {
|
|
980
|
+
display: grid;
|
|
981
|
+
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
982
|
+
gap: 12px;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
.note-item {
|
|
986
|
+
padding: 12px;
|
|
987
|
+
border: 2px solid #007bff;
|
|
988
|
+
border-radius: 6px;
|
|
989
|
+
background-color: #f8f9fa;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
.default-note-display {
|
|
993
|
+
text-align: center;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.note-name {
|
|
997
|
+
font-size: 24px;
|
|
998
|
+
font-weight: bold;
|
|
999
|
+
color: #007bff;
|
|
1000
|
+
margin-bottom: 4px;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
.note-frequency {
|
|
1004
|
+
font-size: 14px;
|
|
1005
|
+
color: #666;
|
|
1006
|
+
margin-bottom: 4px;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
.note-confidence {
|
|
1010
|
+
font-size: 12px;
|
|
1011
|
+
color: #999;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
.audio-detection-chord {
|
|
1015
|
+
margin-top: 20px;
|
|
1016
|
+
padding: 16px;
|
|
1017
|
+
border: 2px solid #28a745;
|
|
1018
|
+
border-radius: 8px;
|
|
1019
|
+
background-color: #f8f9fa;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
.default-chord-display {
|
|
1023
|
+
text-align: center;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
.chord-name {
|
|
1027
|
+
font-size: 32px;
|
|
1028
|
+
font-weight: bold;
|
|
1029
|
+
color: #28a745;
|
|
1030
|
+
margin-bottom: 8px;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
.chord-type,
|
|
1034
|
+
.chord-notes,
|
|
1035
|
+
.chord-confidence {
|
|
1036
|
+
margin-bottom: 4px;
|
|
1037
|
+
color: #666;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
.audio-detection-waiting {
|
|
1041
|
+
text-align: center;
|
|
1042
|
+
padding: 40px;
|
|
1043
|
+
color: #666;
|
|
1044
|
+
font-size: 18px;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
.audio-detection-debug {
|
|
1048
|
+
margin-top: 20px;
|
|
1049
|
+
padding: 12px;
|
|
1050
|
+
background-color: #f4f4f4;
|
|
1051
|
+
border-radius: 4px;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
.audio-detection-debug pre {
|
|
1055
|
+
margin: 8px 0 0 0;
|
|
1056
|
+
font-size: 12px;
|
|
1057
|
+
overflow-x: auto;
|
|
1058
|
+
}
|
|
1059
|
+
`;
|
|
1060
|
+
var PianoKeyboard = ({
|
|
1061
|
+
activeNotes = [],
|
|
1062
|
+
startOctave = 2,
|
|
1063
|
+
endOctave = 6,
|
|
1064
|
+
className = "",
|
|
1065
|
+
showNoteNames = true
|
|
1066
|
+
}) => {
|
|
1067
|
+
const keys = useMemo(() => {
|
|
1068
|
+
const noteNames = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
|
1069
|
+
const blackKeys2 = /* @__PURE__ */ new Set(["C#", "D#", "F#", "G#", "A#"]);
|
|
1070
|
+
const allKeys = [];
|
|
1071
|
+
for (let octave = startOctave; octave <= endOctave; octave++) {
|
|
1072
|
+
for (const noteName of noteNames) {
|
|
1073
|
+
const midi = (octave + 1) * 12 + noteNames.indexOf(noteName);
|
|
1074
|
+
const isActive = activeNotes.some((note) => note.midi === midi);
|
|
1075
|
+
allKeys.push({
|
|
1076
|
+
noteName,
|
|
1077
|
+
octave,
|
|
1078
|
+
midi,
|
|
1079
|
+
isBlack: blackKeys2.has(noteName),
|
|
1080
|
+
isActive
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return allKeys;
|
|
1085
|
+
}, [startOctave, endOctave, activeNotes]);
|
|
1086
|
+
const whiteKeys = keys.filter((k) => !k.isBlack);
|
|
1087
|
+
const blackKeys = keys.filter((k) => k.isBlack);
|
|
1088
|
+
return /* @__PURE__ */ React.createElement("div", { className: `piano-keyboard ${className}` }, /* @__PURE__ */ React.createElement("div", { className: "piano-keys-container" }, /* @__PURE__ */ React.createElement("div", { className: "white-keys" }, whiteKeys.map((key, index) => /* @__PURE__ */ React.createElement(
|
|
1089
|
+
"div",
|
|
1090
|
+
{
|
|
1091
|
+
key: `white-${key.midi}`,
|
|
1092
|
+
className: `piano-key white-key ${key.isActive ? "active" : ""}`,
|
|
1093
|
+
title: `${key.noteName}${key.octave}`
|
|
1094
|
+
},
|
|
1095
|
+
showNoteNames && /* @__PURE__ */ React.createElement("span", { className: "key-label" }, key.noteName, key.octave)
|
|
1096
|
+
))), /* @__PURE__ */ React.createElement("div", { className: "black-keys" }, blackKeys.map((key) => {
|
|
1097
|
+
const whiteKeyIndex = getWhiteKeyIndexBeforeBlack(key.noteName);
|
|
1098
|
+
const octaveOffset = (key.octave - startOctave) * 7;
|
|
1099
|
+
const position = whiteKeyIndex + octaveOffset;
|
|
1100
|
+
return /* @__PURE__ */ React.createElement(
|
|
1101
|
+
"div",
|
|
1102
|
+
{
|
|
1103
|
+
key: `black-${key.midi}`,
|
|
1104
|
+
className: `piano-key black-key ${key.isActive ? "active" : ""}`,
|
|
1105
|
+
style: { left: `${(position + 0.7) * (100 / whiteKeys.length)}%` },
|
|
1106
|
+
title: `${key.noteName}${key.octave}`
|
|
1107
|
+
},
|
|
1108
|
+
showNoteNames && /* @__PURE__ */ React.createElement("span", { className: "key-label" }, key.noteName, key.octave)
|
|
1109
|
+
);
|
|
1110
|
+
}))));
|
|
1111
|
+
};
|
|
1112
|
+
function getWhiteKeyIndexBeforeBlack(noteName) {
|
|
1113
|
+
const map = {
|
|
1114
|
+
"C#": 0,
|
|
1115
|
+
"D#": 1,
|
|
1116
|
+
"F#": 3,
|
|
1117
|
+
"G#": 4,
|
|
1118
|
+
"A#": 5
|
|
1119
|
+
};
|
|
1120
|
+
return map[noteName] || 0;
|
|
1121
|
+
}
|
|
1122
|
+
var pianoKeyboardStyles = `
|
|
1123
|
+
.piano-keyboard {
|
|
1124
|
+
width: 100%;
|
|
1125
|
+
padding: 20px;
|
|
1126
|
+
background-color: #2c3e50;
|
|
1127
|
+
border-radius: 8px;
|
|
1128
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
.piano-keys-container {
|
|
1132
|
+
position: relative;
|
|
1133
|
+
width: 100%;
|
|
1134
|
+
height: 200px;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
.white-keys {
|
|
1138
|
+
display: flex;
|
|
1139
|
+
height: 100%;
|
|
1140
|
+
gap: 2px;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
.piano-key {
|
|
1144
|
+
position: relative;
|
|
1145
|
+
border: 1px solid #000;
|
|
1146
|
+
cursor: pointer;
|
|
1147
|
+
transition: all 0.15s ease;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
.white-key {
|
|
1151
|
+
flex: 1;
|
|
1152
|
+
background: linear-gradient(to bottom, #ffffff 0%, #f0f0f0 100%);
|
|
1153
|
+
border-radius: 0 0 4px 4px;
|
|
1154
|
+
display: flex;
|
|
1155
|
+
align-items: flex-end;
|
|
1156
|
+
justify-content: center;
|
|
1157
|
+
padding-bottom: 8px;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
.white-key:hover {
|
|
1161
|
+
background: linear-gradient(to bottom, #f8f8f8 0%, #e8e8e8 100%);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
.white-key.active {
|
|
1165
|
+
background: linear-gradient(to bottom, #4CAF50 0%, #45a049 100%) !important;
|
|
1166
|
+
box-shadow: 0 0 20px rgba(76, 175, 80, 0.8);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
.black-keys {
|
|
1170
|
+
position: absolute;
|
|
1171
|
+
top: 0;
|
|
1172
|
+
left: 0;
|
|
1173
|
+
width: 100%;
|
|
1174
|
+
height: 60%;
|
|
1175
|
+
pointer-events: none;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
.black-key {
|
|
1179
|
+
position: absolute;
|
|
1180
|
+
width: calc(100% / 52 * 0.6); /* \u5047\u8BBE\u670952\u4E2A\u767D\u952E\u7684\u5BBD\u5EA6 */
|
|
1181
|
+
height: 100%;
|
|
1182
|
+
background: linear-gradient(to bottom, #2c3e50 0%, #1a252f 100%);
|
|
1183
|
+
border-radius: 0 0 3px 3px;
|
|
1184
|
+
pointer-events: all;
|
|
1185
|
+
transform: translateX(-50%);
|
|
1186
|
+
display: flex;
|
|
1187
|
+
align-items: flex-end;
|
|
1188
|
+
justify-content: center;
|
|
1189
|
+
padding-bottom: 6px;
|
|
1190
|
+
z-index: 10;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
.black-key:hover {
|
|
1194
|
+
background: linear-gradient(to bottom, #34495e 0%, #2c3e50 100%);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
.black-key.active {
|
|
1198
|
+
background: linear-gradient(to bottom, #4CAF50 0%, #388E3C 100%) !important;
|
|
1199
|
+
box-shadow: 0 0 20px rgba(76, 175, 80, 0.8);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
.key-label {
|
|
1203
|
+
font-size: 11px;
|
|
1204
|
+
font-weight: bold;
|
|
1205
|
+
user-select: none;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
.white-key .key-label {
|
|
1209
|
+
color: #666;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
.white-key.active .key-label {
|
|
1213
|
+
color: white;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
.black-key .key-label {
|
|
1217
|
+
color: #ddd;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
.black-key.active .key-label {
|
|
1221
|
+
color: white;
|
|
1222
|
+
}
|
|
1223
|
+
`;
|
|
1224
|
+
|
|
1225
|
+
export { AudioDetectionDisplay, AudioDetector, AudioDetectorDebugger, AudioInputService, ChordRecognizer, PianoKeyboard, PitchDetector, audioDetectionStyles, debugAudioDetection, pianoKeyboardStyles, testMicrophone, useAudioDetection };
|
|
1226
|
+
//# sourceMappingURL=index.mjs.map
|
|
1227
|
+
//# sourceMappingURL=index.mjs.map
|