Home > Software design >  How do I avoid audio from disappearing when using `Tone.PolySynth()` with `Sequence` method?
How do I avoid audio from disappearing when using `Tone.PolySynth()` with `Sequence` method?

Time:08-22

Context

I've been trying to build a step sequencer similar to the official example in Tone.js documentation. Instead of using playing MP3 files with Players method, however, I wanted to implement PolySynth to explore a variety of sounds like Jon Oliver's example.

After looking at a few demos with different approaches—like this drum sequencer (using MP3s with Players), this step sequencer (using several mono Synths)—I managed to build a basic functioning sequencer. My working demo in CodeSandbox here.

Problem

While it works as intended—i.e. playing individual notes when toggled on/off, looping the sequence per defined speed, toggling the play/pause mode, etc—the polysynth sound starts to get a little distorted after a few loops and soon becomes completely mute.

Even after the audio disappears, the Sequence call seems to continue and all other features do work as intended—but it becomes noticeably stuttering and laggy.

I suspect it's the way I'm handling Sequence call and unintentionally overloading the memory, although I'm not seeing any significant console errors.

What I tried

  • Using a Transport.scheduleRepeat with a callback: (similar to this Medium article resulting in the same laggy, muting behavior
  • Using an async call to handle Sequence playback toggle: (similar to this CodeSandbox example resulting in the same laggy, muting behavior
  • Using an Array.forEach or for loop to manually loop the sequence: failed to keep the timing in sync without Tone timing control
  • Using a ref to keep track of Sequence while continuously disposing the prior beat: (modified on this CodeSandbox example resulting in the same laggy, muting behavior. This is what's currently in the working code.

Code snippet

It's built with React and TypeScript. The main App portion is shown below. You can view the full working code at this CodeSandbox.

I am a complete beginner in Tone.js although fairly proficient in React. Any guidance and insights will be highly appreciated.

I understand the data handling, DOM handling, and other aspects of the app are also pretty clumsy, and hopefully they won't be what's causing this issue.

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;

CodePudding user response:

The synth object is created on every render. You can move it out of App (or useRef) and everything works fine.

Another thing I noticed that no dependency list here:

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

P.S. What a nice project!

  • Related