Tonejs/Midi

How to generate Piano sound with slight delay and how to play next midi file when user in play mode

Opened this issue · 0 comments

Hi tonejs team!

Can you please tell me how to generate a piano sound effect with a slight delay? I am also facing an issue playing the next midi file when the user clicks on the next button to play the next midi file.

I am going to share my code here, please check and let me know the solution:

'use client';

import React, { useLayoutEffect, useRef, useState } from 'react';
import { MidiFileDoc } from '@/database/models/MidiFile.model';
import Image from 'next/image';
import * as Tone from 'tone';
import { Midi } from '@tonejs/midi';
import { shuffleArray } from '@/shared/utils/shuffleArray';
import { Session } from 'next-auth';
import { SubscriptionDoc } from '@/database/models/Subscription.model';
import { useRouter } from 'next/navigation';

type AudioPlayerProps = {
  midiFiles: MidiFileDoc[];
  session: Session | null;
  subscription: Partial<SubscriptionDoc> | null;
};

const AudioPlayer = ({
  midiFiles,
  session,
  subscription,
}: AudioPlayerProps) => {
  const router = useRouter();
  const [midi, setMidi] = useState<null | Midi>(null);
  const [pause, setPause] = useState(true);
  const polySynthsRef = useRef<Tone.PolySynth[]>([]);
  const [currentFileIndex, setCurrentFileIndex] = useState(() => {
    if (midiFiles?.length) {
      return 0;
    }
    return -1;
  });

  const fileName =
    currentFileIndex < 0 ? null : midiFiles.at(currentFileIndex)?.fileName;

  useLayoutEffect(() => {
    if (midiFiles.length) {
      Midi.fromUrl(midiFiles[0].key)
        .then((midi) => {
          setMidi(midi);
        })
        .catch((error) => {
          console.error(error);
        });
    }
  }, [midiFiles]);

  const handleClearTransport = () => {
    if (Tone.Transport.state === 'started') {
      Tone.Transport.stop();
      Tone.Transport.cancel();
    }
  };

  const handleClearSynth = () => {
    while (polySynthsRef.current.length) {
      const synth = polySynthsRef.current.shift();
      // @ts-ignore
      synth?.disconnect();
      // @ts-ignore
      synth?.releaseAll();
    }
    polySynthsRef.current = [];
  };

  const handleNext = async () => {
    handleClearTransport();
    handleClearSynth();
    setPause(true);
    if (currentFileIndex < midiFiles.length - 1) {
      const file = midiFiles[currentFileIndex + 1];
      const midi = await Midi.fromUrl(file.key);
      setMidi(midi);
      setCurrentFileIndex((prev) => ++prev);
    } else {
      shuffleArray(midiFiles);
      setCurrentFileIndex(0);
    }
  };
  const handleBack = async () => {
    handleClearTransport();
    handleClearSynth();
    setPause(true);
    if (currentFileIndex > 0) {
      const file = midiFiles[currentFileIndex - 1];
      const midi = await Midi.fromUrl(file.key);
      setMidi(midi);
      setCurrentFileIndex((prev) => --prev);
    }
  };

  const handlePlay = async () => {
    setPause(false);
    const now = Tone.now() + 0.2;
    midi?.tracks.forEach((track) => {
      const synth = new Tone.PolySynth(Tone.Synth, {
        detune: 0,
        portamento: 0,
        volume: 0,
        oscillator: {
          type: 'triangle',
          partialCount: 0,
        },
        envelope: {
          attack: 0.02,
          decay: 0.1,
          sustain: 0.3,
          release: 1,
        },
      }).toDestination();
      polySynthsRef.current.push(synth);
      track.notes.forEach((note) => {
        const startTime = Tone.Time(note.time).toSeconds();
        const duration = Tone.Time(note.duration).toSeconds();

        Tone.Transport.scheduleOnce(() => {
          synth.triggerAttackRelease(
            note.name,
            duration,
            startTime + now,
            note.velocity
          );
        }, startTime + 0.2);
      });
    });
    Tone.Transport.toggle();
  };

  const handlePause = () => {
    handleClearTransport();
    handleClearSynth();
    setPause(true);
  };

  const triggerDownload = () => {
    const file = midiFiles[currentFileIndex];
    const anchor = document.createElement('a');
    anchor.href = file.key;
    anchor.download = file.fileName;
    anchor.click();
  };

  const handleDownload = async () => {
    if (!session) {
      router.push('/login');
    }

    if (currentFileIndex >= 0) {
      if (subscription?.status === 'active') {
        return triggerDownload();
      }

      const response = await fetch('/api/user/update-free-limit', {
        method: 'POST',
      });
      const data = await response.json();
      if (+data > -1 && +data <= 5) {
        return triggerDownload();
      }

      if (!subscription) {
        return router.push('/price');
      }
    }
  };
  return (
    <>
      <div className="max-w-[700px] flex flex-col items-center mx-auto mb-5 p-5 gap-5">
        <div className="text-white">{fileName}</div>
        <div
          className={`flex items-center gap-5 h-12 ${
            currentFileIndex < 0 && 'pointer-events-none'
          }`}
        >
          <Image
            className="cursor-pointer"
            src="/assets/images/audio-left.svg"
            width={25}
            height={25}
            alt="Player Back"
            onClick={handleBack}
          />
          <Image
            className={`${pause ? 'inline-block' : 'hidden'} cursor-pointer`}
            src="/assets/images/play.svg"
            width={35}
            height={35}
            alt="Player"
            onClick={handlePlay}
          />
          <Image
            className={`${!pause ? 'inline-block' : 'hidden'} cursor-pointer`}
            src="/assets/images/pause.svg"
            width={35}
            height={35}
            alt="Pause"
            onClick={handlePause}
          />
          <Image
            className="cursor-pointer"
            src="/assets/images/audio-right.svg"
            width={25}
            height={25}
            alt="Player Next"
            onClick={handleNext}
          />
        </div>
      </div>
      <div className="flex justify-center">
        <button onClick={handleDownload} className="download">
          Download midi file
        </button>
      </div>
    </>
  );
};

export default AudioPlayer;