Ошибка: слишком много повторных рендеров. React ограничивает количество рендеров, чтобы предотвратить бесконечный цикл. Проект GUITAR TUNER с использованием Web Audio API

Я пытаюсь создать приложение для настройки гитары, используя 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.


109
1

Ответ:

Решено

Мне удалось получить рабочее решение для всех 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>
  )