ค้นหาเว็บไซต์

javascript - ฉันจะหลีกเลี่ยงไม่ให้เสียงหายไปเมื่อใช้ `Tone.PolySynth()` กับวิธี `Sequence` ได้อย่างไร?


บริบท

ฉันพยายามจะสร้างสเต็ปซีเควนเซอร์ที่คล้ายกับตัวอย่างอย่างเป็นทางการในเอกสารของ Tone.js อย่างไรก็ตาม แทนที่จะใช้การเล่นไฟล์ MP3 ด้วยวิธี Players ฉันต้องการนำ PolySynth มาใช้เพื่อสำรวจเสียงที่หลากหลายเหมือนตัวอย่างของ Jon Oliver

หลังจากดูตัวอย่างสาธิตไม่กี่แบบที่มีวิธีการต่างกัน—เช่น ตัวเรียงลำดับกลองนี้ (ใช้ MP3 กับ Players) ตัวเรียงลำดับสเต็ปนี้ (ใช้ Synth โมโนหลายตัว)—ฉันสามารถสร้างตัวเรียงลำดับพื้นฐานที่ใช้งานได้ ตัวสาธิตที่ทำงานได้ของฉันอยู่ใน CodeSandbox ที่นี่

ปัญหา

แม้ว่ามันจะทำงานตามที่ตั้งใจไว้—เช่น การเล่นโน้ตแต่ละตัวเมื่อสลับเปิด/ปิด การวนซ้ำลำดับตามความเร็วที่กำหนด การสลับโหมดเล่น/หยุดชั่วคราว ฯลฯ—เสียงโพลีซินธ์เริ่มเพี้ยนเล็กน้อยหลังจากวนซ้ำไปไม่กี่ครั้งและไม่นานก็เงียบสนิท

แม้ว่าจะเสียงจะหายไป การเรียก Sequence ดูเหมือนว่าจะยังคงดำเนินต่อไปและคุณสมบัติอื่น ๆ ทั้งหมดก็ทำงานตามที่ตั้งใจไว้ แต่สังเกตได้ว่ามีการกระตุกและหน่วงชัดเจน

ฉันสงสัยว่ามันอาจเป็นวิธีที่ฉันกำลังจัดการการเรียก Sequence และโดยไม่ได้ตั้งใจทำให้หน่วยความจำเกินกำลัง แม้ว่าฉันจะไม่เห็นข้อผิดพลาดสำคัญใด ๆ ในคอนโซล

สิ่งที่ฉันพยายาม

  • การใช้ Transport.scheduleRepeat พร้อม callback: (คล้ายกับบทความ Medium นี้ซึ่งส่งผลให้เกิดพฤติกรรมหน่วงและปิดเสียงเหมือนกัน)
  • การใช้การเรียก async เพื่อจัดการการสลับการเล่น Sequence: (คล้ายกับตัวอย่าง CodeSandbox นี้ซึ่งส่งผลให้เกิดอาการหน่วงและเสียงเบาเหมือนกัน)
  • การใช้ Array.forEach หรือ for loop เพื่อวนซ้ำลำดับด้วยตนเอง: ไม่สามารถรักษาเวลาซิงค์ได้โดยไม่มีการควบคุมเวลาของ Tone
  • การใช้ ref เพื่อติดตาม Sequence ในขณะที่คอยลบ beat ก่อนหน้าอย่างต่อเนื่อง: (แก้ไขจากตัวอย่าง CodeSandbox นี้ซึ่งส่งผลให้เกิดพฤติกรรมหน่วงและเงียบเหมือนเดิม นี่คือตัวอย่างโค้ดที่ทำงานได้ในปัจจุบัน)

โค้ดตัวอย่าง

มันถูกสร้างขึ้นด้วย React และ TypeScript ส่วนหลักของแอปจะแสดงด้านล่าง คุณสามารถดูโค้ดที่ทำงานได้เต็มรูปแบบที่ CodeSandbox

ฉันเป็นผู้เริ่มต้นอย่างสมบูรณ์ใน Tone.js ถึงแม้ว่าจะมีความชำนาญใน React พอสมควร คำแนะนำและข้อมูลเชิงลึกใด ๆ จะได้รับการชื่นชมอย่างมาก

ฉันเข้าใจว่าการจัดการข้อมูล การจัดการ DOM และด้านอื่น ๆ ของแอปก็ยังค่อนข้างยุ่งเหยิงเช่นกัน และหวังว่ามันจะไม่ใช่สาเหตุที่ทำให้เกิดปัญหานี้

function App() {
  type Loop = {
    dispose: any;
  };
  const [isPlaying, setIsPlaying] = React.useState(false);
  const [currentBeat, setCurrentBeat] = React.useState(0);
  const [currentBPM, setCurrentBPM] = React.useState(120);
  const [noteMatrix, setNoteMatrix] = React.useState(defaultNoteMatrix);
  const [currentScale, setCurrentScale] = React.useState(SCALES.major);
  const synth = new Tone.PolySynth().toDestination();
  const loop = React.useRef<Loop | null>(null);
  const noteMap = noteMatrix.map((beat) =>
    beat
      .map((note, i) => {
        return { note: currentScale[i], on: note };
      })
      .filter((note) => note.on)
      .map((item) => item.note)
  );

  const playSingleNote = (note = "") => synth.triggerAttackRelease(note, "16n");

  const tick = (e: React.MouseEvent<HTMLButtonElement>): void => {
    const currentBeat = parseFloat(e.currentTarget.dataset.beat!);
    const selectedNote = parseFloat(e.currentTarget.dataset.index!);
    const isOn = e.currentTarget.dataset.on === "1" ? 0 : 1;
    const updatedMatrix = [...noteMatrix];

    playSingleNote(currentScale[selectedNote]);
    updatedMatrix[currentBeat][selectedNote] = isOn;
    setNoteMatrix(updatedMatrix);
  };
  const reset = useCallback(() => {
    setNoteMatrix([
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat]
    ]);
    setIsPlaying(false);
    Tone.Transport.stop();
  }, []);

  React.useEffect(() => {
    Tone.Transport.loop = false;
    Tone.Transport.on("stop", () => setCurrentBeat(0));
  });
  React.useEffect(() => {
    Tone.Transport.bpm.value = currentBPM;
  }, [currentBPM]);
  React.useEffect(() => {
    if (loop.current) {
      loop.current.dispose();
    }
    loop.current = new Tone.Sequence(
      (time, beat) => {
        setCurrentBeat(beat);
        synth.triggerAttackRelease(noteMap[beat], "16n", time);
      },
      [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
      "16n"
    ).start(0);
  }, [noteMap, isPlaying]);
  const togglePlay = React.useCallback(() => {
    Tone.context.resume();
    Tone.Transport.toggle();
    setIsPlaying((isPlaying) => !isPlaying);
  }, []);
  return (
    <>
      <GridStyle>
        <div className="beat">
          {currentScale.map((note, i) => (
            <div className="head" key={i}>
              {note}
            </div>
          ))}
        </div>
        {noteMatrix.map((beat, i) => (
          <div className="beat" key={i} data-active={currentBeat === i ? 1 : 0}>
            {currentScale.map((note, j) => (
              <button
                className="note"
                key={j}
                onClick={tick}
                data-note={note}
                data-on={beat[j]}
                data-beat={i}
                data-index={j}
              />
            ))}
          </div>
        ))}
      </GridStyle>
      <button onClick={togglePlay}>{isPlaying ? "Stop" : "Play"}</button>
      <button onClick={reset}>Clear</button>
      Scale
      <select
        name="scale"
        id="scale"
        defaultValue="major"
        onChange={(e) => setCurrentScale(SCALES[e.currentTarget.value])}
      >
        {["major", "minor", "suspended"].map((scale) => (
          <option value={scale} key={scale}>
            {scale}
          </option>
        ))}
      </select>
      BPM
      <select
        name="bpm"
        id="bpm"
        onChange={(e) => setCurrentBPM(parseFloat(e.currentTarget.value))}
        defaultValue={currentBPM}
      >
        {[80, 100, 120, 140, 160, 180, 200].map((bpm) => (
          <option value={bpm} key={bpm}>
            {bpm}
          </option>
        ))}
      </select>
    </>
  );
}

export default App;

คำตอบ #1

วัตถุ synth ถูกสร้างขึ้นทุกครั้งที่มีการเรนเดอร์ คุณสามารถย้ายมันออกจาก App (หรือ useRef) และทุกอย่างจะทำงานได้ปกติ

อีกอย่างที่ฉันสังเกตเห็นคือไม่มีรายการการพึ่งพาที่นี่:

React.useEffect(() => {
  Tone.Transport.loop = false;
  Tone.Transport.on("stop", () => setCurrentBeat(0));
}, []); // <--

ป.ล. โครงการนี้น่ารักจริงๆ!