Я пытаюсь создать приложение для настройки гитары, используя API реакции и веб-аудио. Вот структура DOM моего проекта
index.js
\__App.js
\__Tuner.js
Вот краткий обзор того, как работает приложение...
По сути.
На главном экране вы нажимаете кнопку «Пуск», и мы получаем доступ к микрофону пользователя, затем мы создаем экземпляр analyserNode, используя класс window.AudioContext, который предоставляет данные в реальном времени об аудиочастотах, принимаемых микрофоном, затем данные преобразуются в одно числовое значение благодаря алгоритму, записанному в файле AutoCorrelate.js, значение сохраняется в состоянии хука, и, наконец, через серию рендеров различных состояний браузер отображает значение шага #, шаг буква и полоса индикатора/состояния, которая перемещается в зависимости от значения состояния поля.
Как вы можете видеть из логов и из l.60 кода, функцию updatePitch
необходимо вызывать каждую 1 мс, чтобы обновить значение высоты тона, отображаемое на экране. Внутри нашей функции updatePitch
вложены различные обработчики состояний, которые вызываются каждую 1 мс: setPitchNote
, setPitchScale
, setDetune
, setNotification
. Можно было бы предположить, что это вызовет проблему повторного рендеринга, но на самом деле это работает отлично.
Файл: Tuner.js
import React, { useState, useEffect } from 'react';
import AudioContext from '../contexts/AudioContext.js';
import autoCorrelate from "../libs/AutoCorrelate.js";
import {
noteFromPitch,
centsOffFromPitch,
getDetunePercent,
} from "../libs/Helpers.js";
const audioCtx = AudioContext.getAudioContext();
const analyserNode = AudioContext.getAnalyser();
const bufferlength = 2048;
let buf = new Float32Array(bufferlength);
const noteStrings = [
"C",
"C#",
"D",
"D#",
"E",
"F",
"F#",
"G",
"G#",
"A",
"A#",
"B",
];
const Tuner = () => {
/*////AUDIO STATE////*/
const [source, setSource] = useState(null);
const [started, setStart] = useState(false);
const [pitchNote, setPitchNote] = useState("C");
const [pitchScale, setPitchScale] = useState("4");
const [pitch, setPitch] = useState("0 Hz");
const [detune, setDetune] = useState("0");
const [notification, setNotification] = useState(false);
/*////UPDATES PITCH////*/
const updatePitch = (time) => {
analyserNode.getFloatTimeDomainData(buf);
var ac = autoCorrelate(buf, audioCtx.sampleRate);
if (ac > -1) {
let note = noteFromPitch(ac);
let sym = noteStrings[note % 12];
let scl = Math.floor(note / 12) - 1;
let dtune = centsOffFromPitch(ac, note);
setPitch(parseFloat(ac).toFixed(2) + " Hz");
setPitchNote(sym);
setPitchScale(scl);
setDetune(dtune);
setNotification(false);
console.info(note, sym, scl, dtune, ac);
}
};
setInterval(updatePitch, 1);
useEffect(() => {
if (source != null) {
source.connect(analyserNode);
}
}, [source]);
const start = async () => {
const input = await getMicInput();
if (audioCtx.state === "suspended") {
await audioCtx.resume();
}
setStart(true);
setNotification(true);
setTimeout(() => setNotification(false), 5000);
setSource(audioCtx.createMediaStreamSource(input));
};
const stop = () => {
source.disconnect(analyserNode);
setStart(false);
};
const getMicInput = () => {
return navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
autoGainControl: false,
noiseSuppression: false,
latency: 0,
},
});
};
return (
<div className='tuner'>
<div className='notification' style = {{color: notification ? 'black' : 'white'}}>
Please, bring your instrument near to the microphone!
</div>
<div className ='container'>
<div className='screen'>
<div className='top-half'>
<span className='note-letter'>{pitchNote}</span>
<span className='note-number'>{pitchScale}</span>
</div>
<div className='bottom-half'>
<span className='meter-left' style = {{
width: (detune < 0 ? getDetunePercent(detune) : "50") + "%",
}}></span>
<span className='dial'>|</span>
<span className='meter-right' style = {{
width: (detune > 0 ? getDetunePercent(detune) : "50") + "%",
}}></span>
</div>
<div className='text'>
<span>{pitch}</span>
</div>
</div>
</div>
<div className='tuning-btn'>
{!started ?
(<button onClick = {() => {start()}}>Start</button>)
:
(<button onClick = {() => {stop()}}>Stop</button>)
}
</div>
</div>
)
}
export default Tuner;
Теперь я хочу сделать правильный гитарный тюнер. Это означает, что вместо рендеринга каждое значение высоты тона возвращается на экран. Я хочу сравнить текущее значение шага с другим «целевым» значением и заставить элементы пользовательского интерфейса реагировать по-разному в зависимости от того, соответствует ли текущий шаг целевому шагу.
Стандартная гитара имеет 6 струн... следовательно, 6 целевых тонов.
const standard = {
E: 82.41,
A: 110,
D: 146.8,
G: 196,
B: 246.9,
e: 329.6
}
Я попытался закодировать логику, стоящую за этим, только для нижней строки E на данный момент, и вот что я придумал.
Посмотрите на l.61-82 для функции высоты тона струны lowE и l.138 ... для изменений, которые я внес в элемент JSX.
Файл: Tuner.js
import React, { useState, useEffect } from 'react';
import AudioContext from '../contexts/AudioContext.js';
import autoCorrelate from "../libs/AutoCorrelate.js";
import {
noteFromPitch,
centsOffFromPitch,
getDetunePercent,
} from "../libs/Helpers.js";
const audioCtx = AudioContext.getAudioContext();
const analyserNode = AudioContext.getAnalyser();
const bufferlength = 2048;
let buf = new Float32Array(bufferlength);
let log = console.info.bind(console);
const noteStrings = [
"C",
"C#",
"D",
"D#",
"E",
"F",
"F#",
"G",
"G#",
"A",
"A#",
"B",
];
const standardStrings = ['A', 'D', 'G', 'B', 'e'];
const standard = {
E: 82.41,
A: 110,
D: 146.8,
G: 196,
B: 246.9,
e: 329.6
}
const dropD = {
D: 73.42,
A: 110,
D: 146.8,
G: 196,
B: 246.9,
E: 329.6
}
const Tuner = () => {
/*////AUDIO STATE////*/
const [source, setSource] = useState(null);
const [started, setStart] = useState(false);
const [pitchNote, setPitchNote] = useState("C");
const [pitchScale, setPitchScale] = useState("4");
const [pitch, setPitch] = useState("0 Hz");
const [detune, setDetune] = useState("0");
const [notification, setNotification] = useState(false);
/*Low E String */
const [ENote, setENote] = useState("E");
const [Epitch, setEPitchScale] = useState("2");
const [findingE, startFindingE] = useState(false);
const [onKey, isOnKey] = useState('Play');
const isE = () => {
let ac = autoCorrelate(buf, audioCtx.sampleRate);
if (ac > -1) {
let pitchValue = parseFloat(ac).toFixed(2);
log('ac:', ac);
log('pitchValue:', pitchValue);
if (standard.E - .75 <= pitchValue && pitchValue <= standard.E + .75) {
isOnKey('GOOD');
} else if (pitchValue <= standard.E - .75) {
isOnKey('b');
} else if (pitchValue >= standard.E - .75) {
isOnKey('#');
}
}
}
if (findingE) {setInterval(isE, 100)};
/*////UPDATES PITCH////*/
const updatePitch = (time) => {
analyserNode.getFloatTimeDomainData(buf);
var ac = autoCorrelate(buf, audioCtx.sampleRate);
if (ac > -1) {
let note = noteFromPitch(ac);
let sym = noteStrings[note % 12];
let scl = Math.floor(note / 12) - 1;
let dtune = centsOffFromPitch(ac, note);
setPitch(parseFloat(ac).toFixed(2) + " Hz");
setPitchNote(sym);
setPitchScale(scl);
setDetune(dtune);
setNotification(false);
// console.info(note, sym, scl, dtune, ac);
}
};
setInterval(updatePitch, 1);
useEffect(() => {
if (source) {
source.connect(analyserNode);
}
}, [source]);
const start = async () => {
const input = await getMicInput();
if (audioCtx.state === "suspended") {
await audioCtx.resume();
}
setStart(true);
setNotification(true);
setTimeout(() => setNotification(false), 5000);
setSource(audioCtx.createMediaStreamSource(input));
};
const stop = () => {
source.disconnect(analyserNode);
setStart(false);
};
const getMicInput = () => {
return navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
autoGainControl: false,
noiseSuppression: false,
latency: 0,
},
});
};
return (
<div className='tuner'>
<div className='notification' style = { notification ? {color:'white', backgroundColor: 'lightgrey'} : {color: 'white'}}>
Please, bring your instrument near to the microphone!
</div>
<div className ='tuner-container'>
<div className='screen'>
<div className='top-half'>
<span className='note-letter'
style = { (findingE && onKey === 'b' || findingE && onKey === '#' ) ? {color: 'red'} : (findingE && onKey === 'GOOD' ? r: 'lightgreen'} : {color: 'black'} )}>
{!findingE ? (pitchNote) : (ENote)}
</span>
<span style = { (findingE && onKey === 'b' || findingE && onKey === '#' ) ? {color: 'red'} : (findingE && onKey === 'GOOD' lor: 'lightgreen'} : {color: 'black'} )}className='note-number'>{!findingE ? (pitchScale) : (Epitch)}</span>
</div>
<div className='bottom-half'>
<span className='meter-left' style = {{
width: (detune < 0 ? getDetunePercent(detune) : "50") + "%",
}}></span>
<span style = { (findingE && onKey === 'b' || findingE && onKey === '#' ) ? {color: 'red'} : (findingE && onKey === 'GOOD' lor: 'lightgreen'} : {color: 'black'} )} className='dial'>|</span>
<span className='meter-right' style = {{
width: (detune > 0 ? getDetunePercent(detune) : "50") + "%",
}}></span>
</div>
<div className='pitch-text'>
<span style = { (findingE && onKey === 'b' || findingE && onKey === '#' ) ? {color: 'red'} : (findingE && onKey === 'GOOD' lor: 'lightgreen'} : {color: 'black'} )}>{!findingE ? (pitch) : (onKey)}</span>
</div>
</div>
</div>
<div className='tuning-btn'>
{!started ?
(<button onClick = {() => {start()}}>Start</button>)
:
(<button onClick = {() => {stop()}}>Stop</button>)
}
</div>
<div>
<div className='string'>
{!findingE ?
(<button onClick = {() => {startFindingE(true)}}>E</button>)
:
(<button onClick = {() => {startFindingE(false)}}>E</button>)
}
</div>
</div>
</div>
)
}
export default Tuner;
Логика очень похожа на первую версию этого приложения, с вложенными состояниями и всем остальным. И это работает так же хорошо
Теперь проблема заключается в том, чтобы применить это к оставшимся 6 целевым полям. Очевидно, я не хочу писать отдельную функцию isNote
для каждой строки. Я хочу написать функцию, которая захватывает букву innerHTML с каждой кнопки «гитарная струна» и отображает другую букву на экране в зависимости от того, какую кнопку я нажал.
Это привело меня к этому
Файл: Tuner.js
const standardStrings = ['E', 'A', 'D', 'G', 'B', 'e'];
const standard = {
E: 82.41,
A: 110,
D: 146.8,
G: 196,
B: 246.9,
e: 329.6
}
/*////STANDARD TUNING////*/
const [standardNote, setStandardNote] = useState('');
const [standardPitch, setStandardPitch] = useState('');
const [findingStandard, startFindingStandard] = useState({finding: false, note: 'note', pitch: null});
const [onKey, isOnKey] = useState('play');
const standardTuning = (note) => {
const standard = {
E: [82.41, 2],
A: [110, 2],
D: [146.8, 3],
G: [196, 3],
B: [246.9, 3],
e: [329.6, 4]
}
let ac = autoCorrelate(buf, audioCtx.sampleRate);
let pitchValue = parseFloat(ac).toFixed(2);
log('pitchValue:', pitchValue);
log('standard[note]:', standard[note]);
if (ac > -1) {
startFindingStandard({...findingStandard, pitch: standard[note][1]})
if (standard[note][0] - .75 <= ac && ac <= standard[note][0] + .75) {
isOnKey('GOOD');
} else if (ac <= standard[note][0] - .75) {
isOnKey('b');
} else if (ac >= standard[note][0] - .75) {
isOnKey('#');
}
}
}
return (
<div className='tuner'>
<div className='notification' style = { notification ? {color:'white', backgroundColor: 'lightgrey'} : {color: 'white'}}>
Please, bring your instrument near to the microphone!
</div>
<div className ='tuner-container'>
<div className='screen'>
<div className='top-half'>
<span className='note-letter'
style = { (findingStandard.finding && onKey === 'b' || findingStandard.finding && onKey === '#' ) ? {color: 'red'} : (ngStandard.finding && onKey === 'GOOD' ? {color: 'lightgreen'} : {color: 'black'} )}>
{!findingStandard.finding ? (pitchNote) : (findingStandard.note)}
</span>
<span style = { (findingStandard.finding && onKey === 'b' || findingStandard.finding && onKey === '#' ) ? {color: 'red'} : ingStandard.finding && onKey === 'GOOD' ? {color: 'lightgreen'} : {color: 'black'} ssName='note-number'>{!findingStandard.finding ? (pitchScale) : (findingStandard.pitch)}</span>
</div>
<div className='bottom-half'>
<span className='meter-left' style = {{
width: (detune < 0 ? getDetunePercent(detune) : "50") + "%",
}}></span>
<span style = { (findingStandard.finding && onKey === 'b' || findingStandard.finding && onKey === '#' ) ? {color: 'red'} : ingStandard.finding && onKey === 'GOOD' ? {color: 'lightgreen'} : {color: 'black'} )} className='dial'>|</span>
<span className='meter-right' style = {{
width: (detune > 0 ? getDetunePercent(detune) : "50") + "%",
}}></span>
</div>
<div className='pitch-text'>
<span style = { (findingStandard.finding && onKey === 'b' || findingStandard.finding && onKey === '#' ) ? {color: 'red'} : ingStandard.finding && onKey === 'GOOD' ? {color: 'lightgreen'} : {color: 'black'} )}>{!findingStandard.finding ? (pitch) : ()}</span>
</div>
</div>
</div>
<div className='tuning-btn'>
{!started ?
(<button onClick = {() => {start()}}>Start</button>)
:
(<button onClick = {() => {stop()}}>Stop</button>)
}
</div>
<div>
{standardStrings.map((string) => {
return (
<div className='string'>
{!findingStandard.finding ?
(<button onClick = {(e) => {startFindingStandard({...findingStandard, finding: true, note: e.target.innerHTML, pitch: }>{string}</button>)
:
(<button onClick = {() => {startFindingStandard({...findingStandard, finding: false, note: 'note', pitch: '' {string}</button>)
}
</div>
)
})}
</div>
</div>
)
Теперь приложение вылетает из-за слишком большого количества повторных рендеров.
Я не совсем уверен, как справиться с этим. Я знаю, что это сообщение об ошибке, которое обычно возникает, когда вы вставляете хук в хук useEffect и не можете дать ему зависимость, которая вызывает бесконечный цикл... но я не нашел, где происходит бесконечный цикл. Логика на самом деле не отличается от того, когда я пытался подобрать высоту звука только с помощью нижней струны E.
Какие-нибудь мысли? Что еще более важно, есть ли у кого-нибудь какие-либо советы о том, как отлаживать подобные проблемы с этим сообщением об ошибке в будущем?
Пожалуйста, дайте мне знать, если вам нужна дополнительная информация.
Я также включу код, используемый для преобразования аудиоданных в одно значение высоты тона.
Файл: AutoCorrelate.js
const autoCorrelate = (buf, sampleRate) => {
let [SIZE, rms] = [buf.length, 0];
for (let i = 0; i < SIZE; i++) {
let val = buf[i];
rms += val * val;
}
rms = Math.sqrt(rms / SIZE);
if (rms < 0.01) {
// not enough signal
return -1;
}
let [r1, r2, thres] = [0, SIZE - 1, 0.2];
for (let i = 0; i < SIZE / 2; i++)
if (Math.abs(buf[i]) < thres) {
r1 = i;
break;
}
for (let i = 1; i < SIZE / 2; i++)
if (Math.abs(buf[SIZE - i]) < thres) {
r2 = SIZE - i;
break;
}
buf = buf.slice(r1, r2);
SIZE = buf.length;
let c = new Array(SIZE).fill(0);
for (let i = 0; i < SIZE; i++) {
for (let j = 0; j < SIZE - i; j++) {
c[i] = c[i] + buf[j] * buf[j + i];
}
}
let d = 0;
while (c[d] > c[d + 1]) {
d++;
}
let [maxval, maxpos] = [-1, -1];
for (let i = d; i < SIZE; i++) {
if (c[i] > maxval) {
maxval = c[i];
maxpos = i;
}
}
let T0 = maxpos;
let [x1, x2, x3] = [c[T0 - 1], c[T0], c[T0 + 1]];
let [a, b] =[ (x1 + x3 - 2 * x2) / 2, (x3 - x1) / 2]
if (a) {
T0 = T0 - b / (2 * a)
};
return sampleRate / T0;
};
module.exports = autoCorrelate;
🤔 А знаете ли вы, что...
JavaScript имеет множество встроенных объектов, таких как Array, Date и Math.
Мне удалось получить рабочее решение для всех 6 целевых шагов, я не уверен, в чем проблема. Я просто переделал его с нуля. Если у кого-то есть понимание, пожалуйста, не стесняйтесь поделиться. Спасибо.
/*////STATE & HOOKS////*/
const strings = [['E', 2], ['A', 2], ['D', 3], ['G', 3], ['B', 3], ['e', 4]];
const standard = {
E: 82.41,
A: 110,
D: 146.8,
G: 196,
B: 246.9,
e: 329.6
}
const [note, setNote] = useState("C");
const [Epitch, setEPitchScale] = useState("4");
const [findingPitch, startFindingPitch] = useState(false);
const [onKey, isOnKey] = useState('Play');
const isStandard = () => {
let ac = autoCorrelate(buf, audioCtx.sampleRate);
if (ac > -1) {
let pitchValue = Number(pitch.split('').slice(0, -3).join(''));
log('buf:', buf);
log('audioCtx.sampleRate', audioCtx.sampleRate);
log('ac:', ac);
log('pitchValue:', pitchValue);
if (standard[note] - .75 <= pitchValue && pitchValue <= standard[note] + .75) {
isOnKey('GOOD');
} else if (pitchValue <= standard[note] - .75) {
isOnKey('b');
} else if (pitchValue >= standard[note] - .75) {
isOnKey('#');
}
}
}
if (findingPitch) {setInterval(isStandard, 100)};
/*////JSX////*/
return (
<div className='tuner'>
<div className='notification' style = { notification ? {color:'white', backgroundColor: 'lightgrey'} : {color: 'white'}}>
Please, bring your instrument near to the microphone!
</div>
<div className ='tuner-container'>
<div className='screen'>
<div className='top-half'>
<span className='note-letter'
style = { (findingPitch && onKey === 'b' || findingPitch && onKey === '#' ) ? {color: 'red'} : (findingPitch && onKey === 'GOOD' ? {color: 'lightgreen'} : {color: 'black'} )}>
{!findingPitch ? (pitchNote) : (note)}
</span>
<span style = { (findingPitch && onKey === 'b' || findingPitch && onKey === '#' ) ? {color: 'red'} : (findingPitch && onKey === 'GOOD' ? {color: 'lightgreen'} : {color: 'black'} )}className='note-number'>{!findingPitch ? (pitchScale) : (Epitch)}</span>
</div>
<div className='bottom-half'>
<span className='meter-left' style = {{
width: (detune < 0 ? getDetunePercent(detune) : "50") + "%",
}}></span>
<span style = { (findingPitch && onKey === 'b' || findingPitch && onKey === '#' ) ? {color: 'red'} : (findingPitch && onKey === 'GOOD' ? {color: 'lightgreen'} : {color: 'black'} )} className='dial'>|</span>
<span className='meter-right' style = {{
width: (detune > 0 ? getDetunePercent(detune) : "50") + "%",
}}></span>
</div>
<div className='pitch-text'>
<span style = { (findingPitch && onKey === 'b' || findingPitch && onKey === '#' ) ? {color: 'red'} : (findingPitch && onKey === 'GOOD' ? {color: 'lightgreen'} : {color: 'black'} )}>{!findingPitch ? (pitch) : (onKey)}</span>
</div>
</div>
</div>
<div className='tuning-btn'>
{!started ?
(<button onClick = {() => {start()}}>Start</button>)
:
(<button onClick = {() => {stop()}}>Stop</button>)
}
</div>
<div>
{strings.map((string) => {
return (
<div className='string'>
{!findingPitch ?
(<button onClick = {(e) => {startFindingPitch(true); setNote(string[0]); setPitchScale(string[1])}}> {string[0]} </button>)
:
(<button onClick = {() => {startFindingPitch(false)}}>{string[0]}</button>)
}
</div>
)
})}
</div>
</div>
)