Skip to content

Commit

Permalink
Add Functional Midi Display
Browse files Browse the repository at this point in the history
  • Loading branch information
gabe-serna committed Oct 22, 2024
1 parent 6d104a6 commit 9624bbe
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 0 deletions.
13 changes: 13 additions & 0 deletions app/audio/preview/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"use client";

import PianoRoll from "@/components/PianoRoll";
import { useWavesurfer } from "@wavesurfer/react";
import { useEffect, useRef, useState } from "react";
import MinimapPlugin from "wavesurfer.js/dist/plugins/minimap";

export default function Preview() {
const containerRef = useRef<HTMLDivElement | null>(null);
const [midiFile, setMidiFile] = useState<File | null>(null);

const { wavesurfer, isReady, isPlaying, currentTime } = useWavesurfer({
container: containerRef,
Expand Down Expand Up @@ -54,6 +56,17 @@ export default function Preview() {
<div ref={containerRef} />

<button onClick={onPlayPause}>{isPlaying ? "Pause" : "Play"}</button>
<div>
<input
type="file"
accept=".mid"
onChange={(e) => {
if (!e.target.files) return;
setMidiFile(e.target.files[0]);
}}
/>
{midiFile && <PianoRoll midiFile={midiFile} />}{" "}
</div>
</div>
);
}
157 changes: 157 additions & 0 deletions components/PianoRoll.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
"use client";

import React, { useEffect, useRef, useState } from "react";
import * as Tone from "tone";
import { Midi } from "@tonejs/midi";

interface PianoRollProps {
midiFile: Blob; // Accepting the MIDI file as a Blob prop
}

interface MidiNote {
time: number;
note: string;
duration: number;
}

const PianoRoll: React.FC<PianoRollProps> = ({ midiFile }) => {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [midiData, setMidiData] = useState<MidiNote[]>([]);
const [isPlaying, setIsPlaying] = useState(false);
const [progress, setProgress] = useState(0); // Track progress of MIDI playback

useEffect(() => {
// Parse the MIDI file and extract note data from the Blob
const reader = new FileReader();
reader.onload = async (e) => {
const arrayBuffer = e.target?.result as ArrayBuffer;
if (arrayBuffer) {
const midi = await Midi.fromUrl(URL.createObjectURL(midiFile)); // Use Tone.Midi with ArrayBuffer

// Extract note data
const notes: MidiNote[] = [];
midi.tracks.forEach((track) => {
track.notes.forEach((note) => {
notes.push({
time: note.time,
note: note.name,
duration: note.duration,
});
});
});

setMidiData(notes); // Save parsed MIDI notes to state
drawPianoRoll(notes); // Render the piano roll
}
};

reader.readAsArrayBuffer(midiFile); // Read the MIDI Blob as an ArrayBuffer
}, [midiFile]);

// Function to render the piano roll
const drawPianoRoll = (notes: MidiNote[]) => {
const canvas = canvasRef.current;
if (!canvas) return;

const ctx = canvas.getContext("2d");
if (!ctx) return;

const width = canvas.width;
const height = canvas.height;
const noteHeight = 10; // Height of each note block
const totalDuration = Math.max(
...notes.map((note) => note.time + note.duration),
);
const timeScale = width / totalDuration; // Scale the time to fit canvas width

ctx.clearRect(0, 0, width, height);

// Draw each note as a rectangle
notes.forEach((note) => {
const x = note.time * timeScale;
const y = (Tone.Frequency(note.note).toMidi() - 21) * noteHeight;
const noteWidth = note.duration * timeScale;

ctx.fillStyle = "#007bff"; // Note color
ctx.fillRect(x, height - y - noteHeight, noteWidth, noteHeight); // Draw the note rectangle
});
};

// Play the MIDI file using Tone.js
const playMidi = () => {
const synth = new Tone.Synth().toDestination();
const transport = Tone.getTransport();

let previousTime = -1;
midiData.forEach((note) => {
let time = note.time;

if (time <= previousTime) {
time = previousTime + 0.001;
}
transport.schedule((t) => {
synth.triggerAttackRelease(note.note, note.duration, t);
}, time);

previousTime = time; // Update the previous time
});

transport.start();
setIsPlaying(true);

// Update the progress line as the MIDI plays
transport.scheduleRepeat(() => {
setProgress(transport.seconds);
}, 0.01);
};

// Stop playback
const stopMidi = () => {
const transport = Tone.getTransport();
transport.stop();
setIsPlaying(false);
setProgress(0);
};

// Draw the progress line on the piano roll
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;

const ctx = canvas.getContext("2d");
if (!ctx) return;

const width = canvas.width;
const height = canvas.height;
const totalDuration = Math.max(
...midiData.map((note) => note.time + note.duration),
);
const timeScale = width / totalDuration;

// Redraw the progress line
ctx.clearRect(0, 0, width, height);
drawPianoRoll(midiData); // Redraw the piano roll notes

// Draw progress line
ctx.strokeStyle = "red";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(progress * timeScale, 0);
ctx.lineTo(progress * timeScale, height);
ctx.stroke();
}, [progress, midiData]);

return (
<div>
<canvas ref={canvasRef} width={800} height={400} />

<div style={{ marginTop: "10px" }}>
<button onClick={isPlaying ? stopMidi : playMidi}>
{isPlaying ? "Stop" : "Play"}
</button>
</div>
</div>
);
};

export default PianoRoll;
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@supabase/ssr": "latest",
"@supabase/supabase-js": "latest",
"@tensorflow/tfjs-node": "^4.21.0",
"@tonejs/midi": "^2.0.28",
"@wavesurfer/react": "^1.0.7",
"audio-decode": "^2.2.2",
"autoprefixer": "10.4.17",
Expand Down

0 comments on commit 9624bbe

Please sign in to comment.