繁体   English   中英

错误:重新渲染太多。 React 限制渲染次数以防止无限循环。 GUITAR TUNER 项目使用 Web 音频 API

[英]Error: Too many re-renders. React limits the number of renders to prevent an infinite loop. GUITAR TUNER project using Web Audio API

我正在尝试使用 React 和 web 音频 API 创建吉他调音应用程序。这是我项目的 DOM 结构

index.js
 \__App.js
  \__Tuner.js

这是有关该应用程序如何工作的简要概述...

本质上。

在主屏幕上,您单击开始按钮,我们可以访问用户的麦克风,然后我们使用 window.AudioContext class 实例化一个 analyserNode,它提供麦克风拾取的音频频率的实时数据,接下来我们由于 AutoCorrelate.js 文件中编写的算法,数据被转换为单个数值,该值存储在钩子 state 中,最后 - 通过各种 state 的一系列呈现 - 浏览器呈现音高#值,音高字母和相对于球场的 state 值移动的仪表/状态栏。

  • 截图:

在此处输入图像描述

在此处输入图像描述

从日志和代码的 l.60 中可以看出, updatePitch函数需要每 1 毫秒调用一次,以便更新呈现在屏幕上的音高值。 嵌套在我们的updatePitch函数中的是各种 state 钩子,每 1 毫秒被调用一次: setPitchNotesetPitchScalesetDetunesetNotification 人们会认为这会导致重新渲染问题,但实际上效果很好。

文件: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.log(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;

现在,我想做一个合适的吉他调音器。 这意味着不是渲染返回到屏幕的每个音高值。 我想将当前音高值与另一个“目标”值进行比较,并让 UI 元素根据当前音高是否与目标音高匹配做出不同反应。

一把标准吉他有 6 根弦……因此有 6 个目标音高

const standard = {
  E: 82.41,
  A: 110,
  D: 146.8,
  G: 196,
  B: 246.9,
  e: 329.6
}

我暂时试图为低 E 弦编码出这背后的逻辑,这就是我想出的。

查看 l.61- 82 的低 E 弦音 function 和 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.log.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.log(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 function 。 我想编写一个 function 从每个“吉他弦”按钮获取 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;

我设法为所有 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>
  )

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM