在React网站中播放MIDI文件或在播放前将MIDI转换为WAV

问题描述 投票:0回答:1

我有一个 next.js React 网站,它使用 api 生成 MIDI 文件的延续。 api 获取 MIDI 文件,对其进行处理并将处理后的 MIDI 文件上传到云端,并将 URL 返回到网站。

我已经构建了一个音频播放器组件(在克劳德的帮助下,因为我以前从未这样做过), 然而,它产生的声音低于最佳效果,几种乐器重叠,鼓声听起来像高音调的机械噪音。

我将附上下面的组件代码,但我有几个问题:

  1. 有没有比编写自己的音频播放器更好的播放 MIDI 文件的方法?我想要由我定制组件
  2. 假设 MIDI 文件没有损坏,我的代码中造成这种不和谐的原因是什么,而这种不和谐在 Windows Media Player 等播放器中不存在?
  3. 更好的选择是将文件转换为其他格式(例如 wav),而不仅仅是表示乐谱并播放它吗?如果是这样,如何进行转换,是否有库可以处理它,或者我需要自己编写转换吗?

我是音频处理领域的新手,因此非常感谢任何帮助。

附件是完整的组件代码(减去自定义)

'use client';

import React, { useState, useEffect, useRef, useCallback } from 'react';
import styled from 'styled-components';
import * as Tone from 'tone';
import { Midi } from '@tonejs/midi';
import { FaPlay, FaPause, FaStepBackward, FaStepForward } from 'react-icons/fa';

interface AudioPlayerProps {
  midiUrl: string;
}

interface NoteData {
  time: number;
  note: string;
  duration: number;
  velocity: number;
}

const AudioPlayer: React.FC<AudioPlayerProps> = ({ midiUrl }) => {
  const [isPlaying, setIsPlaying] = useState(false);
  const [duration, setDuration] = useState(0);
  const [currentTime, setCurrentTime] = useState(0);
  const [error, setError] = useState<string | null>(null);
  const [progress, setProgress] = useState(0);

  const synthRef = useRef<Tone.PolySynth | null>(null);
  const midiRef = useRef<Midi | null>(null);
  const notesRef = useRef<NoteData[]>([]);

  const initializeAudioContext = useCallback(async () => {
    if (!synthRef.current) {
      await Tone.start();
      synthRef.current = new Tone.PolySynth(Tone.Synth, {
        envelope: {
          attack: 0.02,
          decay: 0.1,
          sustain: 0.3,
          release: 0.8,
        },
      }).toDestination();
    }
  }, []);

  useEffect(() => {
    const loadMidi = async () => {
      try {
        await initializeAudioContext();
        const midi = await Midi.fromUrl(midiUrl);
        midiRef.current = midi;
        setDuration(midi.duration);

        notesRef.current = midi.tracks.flatMap(track => 
          track.notes.map(note => ({
            time: note.time,
            note: note.name,
            duration: note.duration,
            velocity: note.velocity
          }))
        ).sort((a, b) => a.time - b.time);

      } catch (error) {
        console.error('Error loading MIDI file:', error);
        setError('Error loading MIDI file');
      }
    };

    loadMidi();
  }, [midiUrl, initializeAudioContext]);

  const togglePlayPause = useCallback(async () => {
    await initializeAudioContext();
    if (isPlaying) {
      Tone.getTransport().pause();
    } else {
      Tone.getTransport().cancel();
      Tone.getTransport().seconds = currentTime;
      
      notesRef.current.filter(note => note.time >= currentTime).forEach(note => {
        Tone.getTransport().schedule((time) => {
          synthRef.current?.triggerAttackRelease(note.note, note.duration, time, note.velocity);
        }, note.time);
      });

      Tone.getTransport().start();
    }
    setIsPlaying(!isPlaying);
  }, [isPlaying, currentTime, initializeAudioContext]);

  const handleProgressChange = useCallback((newTime: number) => {
    setCurrentTime(newTime);
    setProgress((newTime / duration) * 100);
    Tone.getTransport().seconds = newTime;
  }, [duration]);

  const handleProgressBarInteraction = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
    const bounds = event.currentTarget.getBoundingClientRect();
    const x = event.clientX - bounds.left;
    const clickedValue = (x / bounds.width) * duration;
    handleProgressChange(clickedValue);
  }, [duration, handleProgressChange]);

  const rewind = useCallback(() => {
    handleProgressChange(Math.max(currentTime - 10, 0));
  }, [currentTime, handleProgressChange]);

  const fastForward = useCallback(() => {
    handleProgressChange(Math.min(currentTime + 10, duration));
  }, [currentTime, duration, handleProgressChange]);

  useEffect(() => {
    const interval = setInterval(() => {
      if (isPlaying) {
        const current = Tone.getTransport().seconds;
        setCurrentTime(current);
        setProgress((current / duration) * 100);
        if (current >= duration) {
          setIsPlaying(false);
          Tone.getTransport().stop();
        }
      }
    }, 100);

    return () => clearInterval(interval);
  }, [isPlaying, duration]);

  const formatTime = (time: number) => {
    const minutes = Math.floor(time / 60);
    const seconds = Math.floor(time % 60);
    return `${minutes}:${seconds.toString().padStart(2, '0')}`;
  };

  return (
    <PlayerContainer>
      <ProgressBarContainer
        onClick={handleProgressBarInteraction}
        onMouseDown={handleProgressBarInteraction}
      >
        <ProgressBar width={progress} />
        <ProgressCircle left={progress} />
      </ProgressBarContainer>
      <Controls>
        <Button onClick={rewind} disabled={!synthRef.current}>
          <FaStepBackward />
        </Button>
        <PlayButton onClick={togglePlayPause}>
          {isPlaying ? <FaPause /> : <FaPlay />}
        </PlayButton>
        <Button onClick={fastForward} disabled={!synthRef.current}>
          <FaStepForward />
        </Button>
      </Controls>
      <TimeInfo>
        <span>{formatTime(currentTime)}</span>
        <span>{formatTime(duration)}</span>
      </TimeInfo>
      {error && <div style={{ color: 'red', marginTop: '1rem' }}>{error}</div>}
    </PlayerContainer>
  );
};

export default AudioPlayer;
reactjs next.js13 wav midi audio-player
1个回答
0
投票

您可能需要为要使用的乐器分配一些样本,以获得更好的音质。

工具组合 -tone -tonejs/Midi(使用 midi 文件)也可能因生成的文件而出现问题。

MIDI 文件是二进制文件,并且在实现上存在很大差异,我遇到了许多格式奇怪的文件,其中许多文件导致 MIDI 文件库感到困惑。

对于您的情况,我会推荐不同的游戏解决方案。

https://github.com/fraigo/javascript-midi-player

也许值得一试。欢迎私信我。

© www.soinside.com 2019 - 2024. All rights reserved.