@waveform-playlist/midi 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/dist/index.d.mts +113 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.js +233 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +207 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +60 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015 Naomi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { MidiNoteData, ClipTrack } from '@waveform-playlist/core';
|
|
2
|
+
|
|
3
|
+
interface ParsedMidiTrack {
|
|
4
|
+
/** Track name from the MIDI file */
|
|
5
|
+
name: string;
|
|
6
|
+
/** Parsed notes in MidiNoteData format */
|
|
7
|
+
notes: MidiNoteData[];
|
|
8
|
+
/** Duration in seconds (end of last note) */
|
|
9
|
+
duration: number;
|
|
10
|
+
/** MIDI channel (9/10 = percussion) */
|
|
11
|
+
channel: number;
|
|
12
|
+
/** Instrument name from MIDI program change */
|
|
13
|
+
instrument: string;
|
|
14
|
+
/** GM program number (0-127) from MIDI program change event */
|
|
15
|
+
programNumber: number;
|
|
16
|
+
}
|
|
17
|
+
interface ParsedMidi {
|
|
18
|
+
/** Individual MIDI tracks with their notes */
|
|
19
|
+
tracks: ParsedMidiTrack[];
|
|
20
|
+
/** Total duration in seconds (max of all track durations) */
|
|
21
|
+
duration: number;
|
|
22
|
+
/** Song name from MIDI header */
|
|
23
|
+
name: string;
|
|
24
|
+
/** First tempo in BPM (default 120 if none specified) */
|
|
25
|
+
bpm: number;
|
|
26
|
+
/** Time signature as [numerator, denominator] (default [4, 4]) */
|
|
27
|
+
timeSignature: [number, number];
|
|
28
|
+
}
|
|
29
|
+
interface ParseMidiOptions {
|
|
30
|
+
/** When true, merges all MIDI tracks into a single ParsedMidiTrack */
|
|
31
|
+
flatten?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse a MIDI file from an ArrayBuffer.
|
|
35
|
+
*
|
|
36
|
+
* Returns structured data with tracks, notes, tempo, and time signature.
|
|
37
|
+
* Notes are already in seconds (tempo-adjusted by @tonejs/midi).
|
|
38
|
+
*/
|
|
39
|
+
declare function parseMidiFile(data: ArrayBuffer, options?: ParseMidiOptions): ParsedMidi;
|
|
40
|
+
/**
|
|
41
|
+
* Fetch and parse a MIDI file from a URL.
|
|
42
|
+
*
|
|
43
|
+
* Supports AbortSignal for cancellation (e.g., component unmount cleanup).
|
|
44
|
+
*/
|
|
45
|
+
declare function parseMidiUrl(url: string, options?: ParseMidiOptions, signal?: AbortSignal): Promise<ParsedMidi>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Configuration for a single MIDI track to load.
|
|
49
|
+
*
|
|
50
|
+
* MIDI data can be provided in two ways:
|
|
51
|
+
* 1. `src` — URL to a .mid file (fetched, parsed with @tonejs/midi)
|
|
52
|
+
* 2. `midiNotes` — Pre-parsed notes (skip fetch+parse)
|
|
53
|
+
*/
|
|
54
|
+
interface MidiTrackConfig {
|
|
55
|
+
/** URL to .mid file */
|
|
56
|
+
src?: string;
|
|
57
|
+
/** Pre-parsed MIDI notes (skip fetch+parse) */
|
|
58
|
+
midiNotes?: MidiNoteData[];
|
|
59
|
+
/** Track display name */
|
|
60
|
+
name?: string;
|
|
61
|
+
/** Whether this track is muted */
|
|
62
|
+
muted?: boolean;
|
|
63
|
+
/** Whether this track is soloed */
|
|
64
|
+
soloed?: boolean;
|
|
65
|
+
/** Track volume (default 1.0) */
|
|
66
|
+
volume?: number;
|
|
67
|
+
/** Stereo pan (default 0) */
|
|
68
|
+
pan?: number;
|
|
69
|
+
/** Track color */
|
|
70
|
+
color?: string;
|
|
71
|
+
/** Clip position on timeline in seconds (default 0) */
|
|
72
|
+
startTime?: number;
|
|
73
|
+
/** Override clip duration in seconds (default: derived from last note) */
|
|
74
|
+
duration?: number;
|
|
75
|
+
/** Sample rate for sample-based positioning (default 44100) */
|
|
76
|
+
sampleRate?: number;
|
|
77
|
+
/** Merge all MIDI tracks from the file into one ClipTrack (default false) */
|
|
78
|
+
flatten?: boolean;
|
|
79
|
+
}
|
|
80
|
+
interface UseMidiTracksReturn {
|
|
81
|
+
/** Loaded ClipTrack array with midiNotes on clips */
|
|
82
|
+
tracks: ClipTrack[];
|
|
83
|
+
/** Whether any tracks are still loading */
|
|
84
|
+
loading: boolean;
|
|
85
|
+
/** Error message if loading failed, null otherwise */
|
|
86
|
+
error: string | null;
|
|
87
|
+
/** Number of tracks that have finished loading */
|
|
88
|
+
loadedCount: number;
|
|
89
|
+
/** Total number of tracks (known after parsing) */
|
|
90
|
+
totalCount: number;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Hook to load MIDI files and convert to ClipTrack format with midiNotes.
|
|
94
|
+
*
|
|
95
|
+
* All tracks are returned at once after loading completes. This ensures
|
|
96
|
+
* all track controls and layout containers appear simultaneously in React,
|
|
97
|
+
* while canvas rendering is deferred to the UI layer for progressive drawing.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* const { tracks, loading, error } = useMidiTracks([
|
|
102
|
+
* { src: '/music/song.mid', name: 'Piano' },
|
|
103
|
+
* ]);
|
|
104
|
+
*
|
|
105
|
+
* // Pre-parsed notes (no fetch)
|
|
106
|
+
* const { tracks } = useMidiTracks([
|
|
107
|
+
* { midiNotes: myNotes, name: 'Synth Lead', duration: 30 },
|
|
108
|
+
* ]);
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
declare function useMidiTracks(configs: MidiTrackConfig[]): UseMidiTracksReturn;
|
|
112
|
+
|
|
113
|
+
export { type MidiTrackConfig, type ParseMidiOptions, type ParsedMidi, type ParsedMidiTrack, type UseMidiTracksReturn, parseMidiFile, parseMidiUrl, useMidiTracks };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { MidiNoteData, ClipTrack } from '@waveform-playlist/core';
|
|
2
|
+
|
|
3
|
+
interface ParsedMidiTrack {
|
|
4
|
+
/** Track name from the MIDI file */
|
|
5
|
+
name: string;
|
|
6
|
+
/** Parsed notes in MidiNoteData format */
|
|
7
|
+
notes: MidiNoteData[];
|
|
8
|
+
/** Duration in seconds (end of last note) */
|
|
9
|
+
duration: number;
|
|
10
|
+
/** MIDI channel (9/10 = percussion) */
|
|
11
|
+
channel: number;
|
|
12
|
+
/** Instrument name from MIDI program change */
|
|
13
|
+
instrument: string;
|
|
14
|
+
/** GM program number (0-127) from MIDI program change event */
|
|
15
|
+
programNumber: number;
|
|
16
|
+
}
|
|
17
|
+
interface ParsedMidi {
|
|
18
|
+
/** Individual MIDI tracks with their notes */
|
|
19
|
+
tracks: ParsedMidiTrack[];
|
|
20
|
+
/** Total duration in seconds (max of all track durations) */
|
|
21
|
+
duration: number;
|
|
22
|
+
/** Song name from MIDI header */
|
|
23
|
+
name: string;
|
|
24
|
+
/** First tempo in BPM (default 120 if none specified) */
|
|
25
|
+
bpm: number;
|
|
26
|
+
/** Time signature as [numerator, denominator] (default [4, 4]) */
|
|
27
|
+
timeSignature: [number, number];
|
|
28
|
+
}
|
|
29
|
+
interface ParseMidiOptions {
|
|
30
|
+
/** When true, merges all MIDI tracks into a single ParsedMidiTrack */
|
|
31
|
+
flatten?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse a MIDI file from an ArrayBuffer.
|
|
35
|
+
*
|
|
36
|
+
* Returns structured data with tracks, notes, tempo, and time signature.
|
|
37
|
+
* Notes are already in seconds (tempo-adjusted by @tonejs/midi).
|
|
38
|
+
*/
|
|
39
|
+
declare function parseMidiFile(data: ArrayBuffer, options?: ParseMidiOptions): ParsedMidi;
|
|
40
|
+
/**
|
|
41
|
+
* Fetch and parse a MIDI file from a URL.
|
|
42
|
+
*
|
|
43
|
+
* Supports AbortSignal for cancellation (e.g., component unmount cleanup).
|
|
44
|
+
*/
|
|
45
|
+
declare function parseMidiUrl(url: string, options?: ParseMidiOptions, signal?: AbortSignal): Promise<ParsedMidi>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Configuration for a single MIDI track to load.
|
|
49
|
+
*
|
|
50
|
+
* MIDI data can be provided in two ways:
|
|
51
|
+
* 1. `src` — URL to a .mid file (fetched, parsed with @tonejs/midi)
|
|
52
|
+
* 2. `midiNotes` — Pre-parsed notes (skip fetch+parse)
|
|
53
|
+
*/
|
|
54
|
+
interface MidiTrackConfig {
|
|
55
|
+
/** URL to .mid file */
|
|
56
|
+
src?: string;
|
|
57
|
+
/** Pre-parsed MIDI notes (skip fetch+parse) */
|
|
58
|
+
midiNotes?: MidiNoteData[];
|
|
59
|
+
/** Track display name */
|
|
60
|
+
name?: string;
|
|
61
|
+
/** Whether this track is muted */
|
|
62
|
+
muted?: boolean;
|
|
63
|
+
/** Whether this track is soloed */
|
|
64
|
+
soloed?: boolean;
|
|
65
|
+
/** Track volume (default 1.0) */
|
|
66
|
+
volume?: number;
|
|
67
|
+
/** Stereo pan (default 0) */
|
|
68
|
+
pan?: number;
|
|
69
|
+
/** Track color */
|
|
70
|
+
color?: string;
|
|
71
|
+
/** Clip position on timeline in seconds (default 0) */
|
|
72
|
+
startTime?: number;
|
|
73
|
+
/** Override clip duration in seconds (default: derived from last note) */
|
|
74
|
+
duration?: number;
|
|
75
|
+
/** Sample rate for sample-based positioning (default 44100) */
|
|
76
|
+
sampleRate?: number;
|
|
77
|
+
/** Merge all MIDI tracks from the file into one ClipTrack (default false) */
|
|
78
|
+
flatten?: boolean;
|
|
79
|
+
}
|
|
80
|
+
interface UseMidiTracksReturn {
|
|
81
|
+
/** Loaded ClipTrack array with midiNotes on clips */
|
|
82
|
+
tracks: ClipTrack[];
|
|
83
|
+
/** Whether any tracks are still loading */
|
|
84
|
+
loading: boolean;
|
|
85
|
+
/** Error message if loading failed, null otherwise */
|
|
86
|
+
error: string | null;
|
|
87
|
+
/** Number of tracks that have finished loading */
|
|
88
|
+
loadedCount: number;
|
|
89
|
+
/** Total number of tracks (known after parsing) */
|
|
90
|
+
totalCount: number;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Hook to load MIDI files and convert to ClipTrack format with midiNotes.
|
|
94
|
+
*
|
|
95
|
+
* All tracks are returned at once after loading completes. This ensures
|
|
96
|
+
* all track controls and layout containers appear simultaneously in React,
|
|
97
|
+
* while canvas rendering is deferred to the UI layer for progressive drawing.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* const { tracks, loading, error } = useMidiTracks([
|
|
102
|
+
* { src: '/music/song.mid', name: 'Piano' },
|
|
103
|
+
* ]);
|
|
104
|
+
*
|
|
105
|
+
* // Pre-parsed notes (no fetch)
|
|
106
|
+
* const { tracks } = useMidiTracks([
|
|
107
|
+
* { midiNotes: myNotes, name: 'Synth Lead', duration: 30 },
|
|
108
|
+
* ]);
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
declare function useMidiTracks(configs: MidiTrackConfig[]): UseMidiTracksReturn;
|
|
112
|
+
|
|
113
|
+
export { type MidiTrackConfig, type ParseMidiOptions, type ParsedMidi, type ParsedMidiTrack, type UseMidiTracksReturn, parseMidiFile, parseMidiUrl, useMidiTracks };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
parseMidiFile: () => parseMidiFile,
|
|
24
|
+
parseMidiUrl: () => parseMidiUrl,
|
|
25
|
+
useMidiTracks: () => useMidiTracks
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/parseMidiFile.ts
|
|
30
|
+
var import_midi = require("@tonejs/midi");
|
|
31
|
+
function titleCase(str) {
|
|
32
|
+
return str.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
33
|
+
}
|
|
34
|
+
function mapNotes(track) {
|
|
35
|
+
return track.notes.map((note) => ({
|
|
36
|
+
midi: note.midi,
|
|
37
|
+
name: note.name,
|
|
38
|
+
time: note.time,
|
|
39
|
+
duration: note.duration,
|
|
40
|
+
velocity: note.velocity,
|
|
41
|
+
channel: track.channel
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
function getTrackDuration(notes) {
|
|
45
|
+
if (notes.length === 0) return 0;
|
|
46
|
+
return Math.max(...notes.map((n) => n.time + n.duration));
|
|
47
|
+
}
|
|
48
|
+
function parseMidiFile(data, options = {}) {
|
|
49
|
+
const midi = new import_midi.Midi(data);
|
|
50
|
+
const bpm = midi.header.tempos.length > 0 ? midi.header.tempos[0].bpm : 120;
|
|
51
|
+
const timeSig = midi.header.timeSignatures.length > 0 ? midi.header.timeSignatures[0].timeSignature : [4, 4];
|
|
52
|
+
const parsedTracks = midi.tracks.filter((track) => track.notes.length > 0).map((track) => {
|
|
53
|
+
const notes = mapNotes(track);
|
|
54
|
+
const instrument = track.instrument.name;
|
|
55
|
+
const programNumber = track.instrument.number;
|
|
56
|
+
let name;
|
|
57
|
+
if (track.channel === 9) {
|
|
58
|
+
name = "Drums";
|
|
59
|
+
} else if (programNumber > 0) {
|
|
60
|
+
name = titleCase(instrument);
|
|
61
|
+
} else {
|
|
62
|
+
name = track.name.trim() || titleCase(instrument) || `Channel ${track.channel + 1}`;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
name,
|
|
66
|
+
notes,
|
|
67
|
+
duration: getTrackDuration(notes),
|
|
68
|
+
channel: track.channel,
|
|
69
|
+
instrument,
|
|
70
|
+
programNumber: track.instrument.number
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
if (options.flatten && parsedTracks.length > 0) {
|
|
74
|
+
const allNotes = parsedTracks.flatMap((t) => t.notes);
|
|
75
|
+
allNotes.sort((a, b) => a.time - b.time);
|
|
76
|
+
const duration2 = getTrackDuration(allNotes);
|
|
77
|
+
return {
|
|
78
|
+
tracks: [
|
|
79
|
+
{
|
|
80
|
+
name: midi.name || "MIDI",
|
|
81
|
+
notes: allNotes,
|
|
82
|
+
duration: duration2,
|
|
83
|
+
channel: parsedTracks[0].channel,
|
|
84
|
+
instrument: parsedTracks[0].instrument,
|
|
85
|
+
programNumber: parsedTracks[0].programNumber
|
|
86
|
+
}
|
|
87
|
+
],
|
|
88
|
+
duration: duration2,
|
|
89
|
+
name: midi.name || "",
|
|
90
|
+
bpm,
|
|
91
|
+
timeSignature: timeSig
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const duration = parsedTracks.length > 0 ? Math.max(...parsedTracks.map((t) => t.duration)) : 0;
|
|
95
|
+
return {
|
|
96
|
+
tracks: parsedTracks,
|
|
97
|
+
duration,
|
|
98
|
+
name: midi.name || "",
|
|
99
|
+
bpm,
|
|
100
|
+
timeSignature: timeSig
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async function parseMidiUrl(url, options = {}, signal) {
|
|
104
|
+
const response = await fetch(url, { signal });
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error(`Failed to fetch MIDI file ${url}: ${response.statusText}`);
|
|
107
|
+
}
|
|
108
|
+
const buffer = await response.arrayBuffer();
|
|
109
|
+
return parseMidiFile(buffer, options);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/useMidiTracks.ts
|
|
113
|
+
var import_react = require("react");
|
|
114
|
+
var import_core = require("@waveform-playlist/core");
|
|
115
|
+
function useMidiTracks(configs) {
|
|
116
|
+
const [tracks, setTracks] = (0, import_react.useState)([]);
|
|
117
|
+
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
118
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
119
|
+
const [loadedCount, setLoadedCount] = (0, import_react.useState)(0);
|
|
120
|
+
const [totalCount, setTotalCount] = (0, import_react.useState)(configs.length);
|
|
121
|
+
const bufferCacheRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
122
|
+
(0, import_react.useEffect)(() => {
|
|
123
|
+
if (configs.length === 0) {
|
|
124
|
+
setTracks([]);
|
|
125
|
+
setLoading(false);
|
|
126
|
+
setLoadedCount(0);
|
|
127
|
+
setTotalCount(0);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
let cancelled = false;
|
|
131
|
+
const abortController = new AbortController();
|
|
132
|
+
const createTrackFromNotes = (config, notes, trackName, noteDuration, midiChannel, midiProgram) => {
|
|
133
|
+
const sampleRate = config.sampleRate ?? 44100;
|
|
134
|
+
const clipDuration = config.duration ?? noteDuration;
|
|
135
|
+
const clip = (0, import_core.createClipFromSeconds)({
|
|
136
|
+
startTime: config.startTime ?? 0,
|
|
137
|
+
duration: clipDuration,
|
|
138
|
+
sampleRate,
|
|
139
|
+
sourceDuration: clipDuration,
|
|
140
|
+
midiNotes: notes,
|
|
141
|
+
midiChannel,
|
|
142
|
+
midiProgram,
|
|
143
|
+
name: trackName
|
|
144
|
+
});
|
|
145
|
+
return (0, import_core.createTrack)({
|
|
146
|
+
name: trackName,
|
|
147
|
+
clips: [clip],
|
|
148
|
+
muted: config.muted ?? false,
|
|
149
|
+
soloed: config.soloed ?? false,
|
|
150
|
+
volume: config.volume ?? 1,
|
|
151
|
+
pan: config.pan ?? 0,
|
|
152
|
+
color: config.color
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
const allCached = configs.every((c) => !c.src || bufferCacheRef.current.has(c.src));
|
|
156
|
+
const loadTracks = async () => {
|
|
157
|
+
try {
|
|
158
|
+
if (!allCached) {
|
|
159
|
+
setLoading(true);
|
|
160
|
+
setLoadedCount(0);
|
|
161
|
+
}
|
|
162
|
+
setError(null);
|
|
163
|
+
const allTracks = [];
|
|
164
|
+
for (const config of configs) {
|
|
165
|
+
if (cancelled) break;
|
|
166
|
+
if (config.midiNotes) {
|
|
167
|
+
const notes = config.midiNotes;
|
|
168
|
+
const duration = notes.length > 0 ? Math.max(...notes.map((n) => n.time + n.duration)) : 0;
|
|
169
|
+
const trackName = config.name || "MIDI Track";
|
|
170
|
+
allTracks.push(createTrackFromNotes(config, notes, trackName, duration));
|
|
171
|
+
} else if (config.src) {
|
|
172
|
+
let buffer = bufferCacheRef.current.get(config.src);
|
|
173
|
+
if (!buffer) {
|
|
174
|
+
const response = await fetch(config.src, {
|
|
175
|
+
signal: abortController.signal
|
|
176
|
+
});
|
|
177
|
+
if (!response.ok) {
|
|
178
|
+
throw new Error(`Failed to fetch ${config.src}: ${response.statusText}`);
|
|
179
|
+
}
|
|
180
|
+
buffer = await response.arrayBuffer();
|
|
181
|
+
bufferCacheRef.current.set(config.src, buffer);
|
|
182
|
+
}
|
|
183
|
+
const parsed = parseMidiFile(buffer, {
|
|
184
|
+
flatten: config.flatten
|
|
185
|
+
});
|
|
186
|
+
for (const parsedTrack of parsed.tracks) {
|
|
187
|
+
if (cancelled) break;
|
|
188
|
+
const trackName = parsedTrack.name;
|
|
189
|
+
allTracks.push(
|
|
190
|
+
createTrackFromNotes(
|
|
191
|
+
config,
|
|
192
|
+
parsedTrack.notes,
|
|
193
|
+
trackName,
|
|
194
|
+
parsedTrack.duration,
|
|
195
|
+
parsedTrack.channel,
|
|
196
|
+
parsedTrack.programNumber
|
|
197
|
+
)
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
throw new Error("MIDI track config must provide src or midiNotes");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!cancelled) {
|
|
205
|
+
setTracks(allTracks);
|
|
206
|
+
setLoadedCount(allTracks.length);
|
|
207
|
+
setTotalCount(allTracks.length);
|
|
208
|
+
setLoading(false);
|
|
209
|
+
}
|
|
210
|
+
} catch (err) {
|
|
211
|
+
if (!cancelled) {
|
|
212
|
+
const errorMessage = err instanceof Error ? err.message : "Unknown error loading MIDI";
|
|
213
|
+
setError(errorMessage);
|
|
214
|
+
setLoading(false);
|
|
215
|
+
console.error("Error loading MIDI tracks:", err);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
loadTracks();
|
|
220
|
+
return () => {
|
|
221
|
+
cancelled = true;
|
|
222
|
+
abortController.abort();
|
|
223
|
+
};
|
|
224
|
+
}, [configs]);
|
|
225
|
+
return { tracks, loading, error, loadedCount, totalCount };
|
|
226
|
+
}
|
|
227
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
228
|
+
0 && (module.exports = {
|
|
229
|
+
parseMidiFile,
|
|
230
|
+
parseMidiUrl,
|
|
231
|
+
useMidiTracks
|
|
232
|
+
});
|
|
233
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/parseMidiFile.ts","../src/useMidiTracks.ts"],"sourcesContent":["export { parseMidiFile, parseMidiUrl } from './parseMidiFile';\nexport type { ParsedMidi, ParsedMidiTrack, ParseMidiOptions } from './parseMidiFile';\nexport { useMidiTracks } from './useMidiTracks';\nexport type { MidiTrackConfig, UseMidiTracksReturn } from './useMidiTracks';\n","import { Midi } from '@tonejs/midi';\nimport type { MidiNoteData } from '@waveform-playlist/core';\n\nexport interface ParsedMidiTrack {\n /** Track name from the MIDI file */\n name: string;\n /** Parsed notes in MidiNoteData format */\n notes: MidiNoteData[];\n /** Duration in seconds (end of last note) */\n duration: number;\n /** MIDI channel (9/10 = percussion) */\n channel: number;\n /** Instrument name from MIDI program change */\n instrument: string;\n /** GM program number (0-127) from MIDI program change event */\n programNumber: number;\n}\n\nexport interface ParsedMidi {\n /** Individual MIDI tracks with their notes */\n tracks: ParsedMidiTrack[];\n /** Total duration in seconds (max of all track durations) */\n duration: number;\n /** Song name from MIDI header */\n name: string;\n /** First tempo in BPM (default 120 if none specified) */\n bpm: number;\n /** Time signature as [numerator, denominator] (default [4, 4]) */\n timeSignature: [number, number];\n}\n\nexport interface ParseMidiOptions {\n /** When true, merges all MIDI tracks into a single ParsedMidiTrack */\n flatten?: boolean;\n}\n\n/**\n * Title-case an instrument name from @tonejs/midi.\n * \"acoustic grand piano\" → \"Acoustic Grand Piano\"\n * \"electric bass (finger)\" → \"Electric Bass (Finger)\"\n */\nfunction titleCase(str: string): string {\n return str.replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\nfunction mapNotes(track: Midi['tracks'][number]): MidiNoteData[] {\n return track.notes.map((note) => ({\n midi: note.midi,\n name: note.name,\n time: note.time,\n duration: note.duration,\n velocity: note.velocity,\n channel: track.channel,\n }));\n}\n\nfunction getTrackDuration(notes: MidiNoteData[]): number {\n if (notes.length === 0) return 0;\n return Math.max(...notes.map((n) => n.time + n.duration));\n}\n\n/**\n * Parse a MIDI file from an ArrayBuffer.\n *\n * Returns structured data with tracks, notes, tempo, and time signature.\n * Notes are already in seconds (tempo-adjusted by @tonejs/midi).\n */\nexport function parseMidiFile(data: ArrayBuffer, options: ParseMidiOptions = {}): ParsedMidi {\n const midi = new Midi(data);\n\n const bpm = midi.header.tempos.length > 0 ? midi.header.tempos[0].bpm : 120;\n\n const timeSig =\n midi.header.timeSignatures.length > 0 ? midi.header.timeSignatures[0].timeSignature : [4, 4];\n\n // Parse all tracks that have notes\n const parsedTracks: ParsedMidiTrack[] = midi.tracks\n .filter((track) => track.notes.length > 0)\n .map((track) => {\n const notes = mapNotes(track);\n const instrument = track.instrument.name;\n const programNumber = track.instrument.number;\n // Channel 9 = GM percussion. Use \"Drums\" for clarity.\n // For melodic tracks: prefer GM instrument name when program is non-default (> 0),\n // since @tonejs/midi defaults to \"acoustic grand piano\" even with no program change.\n // Fall back to track name, then channel number.\n let name: string;\n if (track.channel === 9) {\n name = 'Drums';\n } else if (programNumber > 0) {\n name = titleCase(instrument);\n } else {\n name = track.name.trim() || titleCase(instrument) || `Channel ${track.channel + 1}`;\n }\n return {\n name,\n notes,\n duration: getTrackDuration(notes),\n channel: track.channel,\n instrument,\n programNumber: track.instrument.number,\n };\n });\n\n if (options.flatten && parsedTracks.length > 0) {\n const allNotes = parsedTracks.flatMap((t) => t.notes);\n allNotes.sort((a, b) => a.time - b.time);\n const duration = getTrackDuration(allNotes);\n\n return {\n tracks: [\n {\n name: midi.name || 'MIDI',\n notes: allNotes,\n duration,\n channel: parsedTracks[0].channel,\n instrument: parsedTracks[0].instrument,\n programNumber: parsedTracks[0].programNumber,\n },\n ],\n duration,\n name: midi.name || '',\n bpm,\n timeSignature: timeSig as [number, number],\n };\n }\n\n const duration = parsedTracks.length > 0 ? Math.max(...parsedTracks.map((t) => t.duration)) : 0;\n\n return {\n tracks: parsedTracks,\n duration,\n name: midi.name || '',\n bpm,\n timeSignature: timeSig as [number, number],\n };\n}\n\n/**\n * Fetch and parse a MIDI file from a URL.\n *\n * Supports AbortSignal for cancellation (e.g., component unmount cleanup).\n */\nexport async function parseMidiUrl(\n url: string,\n options: ParseMidiOptions = {},\n signal?: AbortSignal\n): Promise<ParsedMidi> {\n const response = await fetch(url, { signal });\n if (!response.ok) {\n throw new Error(`Failed to fetch MIDI file ${url}: ${response.statusText}`);\n }\n const buffer = await response.arrayBuffer();\n return parseMidiFile(buffer, options);\n}\n","import { useState, useEffect, useRef } from 'react';\nimport {\n type ClipTrack,\n type MidiNoteData,\n createClipFromSeconds,\n createTrack,\n} from '@waveform-playlist/core';\nimport { parseMidiFile } from './parseMidiFile';\n\n/**\n * Configuration for a single MIDI track to load.\n *\n * MIDI data can be provided in two ways:\n * 1. `src` — URL to a .mid file (fetched, parsed with @tonejs/midi)\n * 2. `midiNotes` — Pre-parsed notes (skip fetch+parse)\n */\nexport interface MidiTrackConfig {\n /** URL to .mid file */\n src?: string;\n /** Pre-parsed MIDI notes (skip fetch+parse) */\n midiNotes?: MidiNoteData[];\n /** Track display name */\n name?: string;\n /** Whether this track is muted */\n muted?: boolean;\n /** Whether this track is soloed */\n soloed?: boolean;\n /** Track volume (default 1.0) */\n volume?: number;\n /** Stereo pan (default 0) */\n pan?: number;\n /** Track color */\n color?: string;\n /** Clip position on timeline in seconds (default 0) */\n startTime?: number;\n /** Override clip duration in seconds (default: derived from last note) */\n duration?: number;\n /** Sample rate for sample-based positioning (default 44100) */\n sampleRate?: number;\n /** Merge all MIDI tracks from the file into one ClipTrack (default false) */\n flatten?: boolean;\n}\n\nexport interface UseMidiTracksReturn {\n /** Loaded ClipTrack array with midiNotes on clips */\n tracks: ClipTrack[];\n /** Whether any tracks are still loading */\n loading: boolean;\n /** Error message if loading failed, null otherwise */\n error: string | null;\n /** Number of tracks that have finished loading */\n loadedCount: number;\n /** Total number of tracks (known after parsing) */\n totalCount: number;\n}\n\n/**\n * Hook to load MIDI files and convert to ClipTrack format with midiNotes.\n *\n * All tracks are returned at once after loading completes. This ensures\n * all track controls and layout containers appear simultaneously in React,\n * while canvas rendering is deferred to the UI layer for progressive drawing.\n *\n * @example\n * ```typescript\n * const { tracks, loading, error } = useMidiTracks([\n * { src: '/music/song.mid', name: 'Piano' },\n * ]);\n *\n * // Pre-parsed notes (no fetch)\n * const { tracks } = useMidiTracks([\n * { midiNotes: myNotes, name: 'Synth Lead', duration: 30 },\n * ]);\n * ```\n */\nexport function useMidiTracks(configs: MidiTrackConfig[]): UseMidiTracksReturn {\n const [tracks, setTracks] = useState<ClipTrack[]>([]);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [loadedCount, setLoadedCount] = useState(0);\n const [totalCount, setTotalCount] = useState(configs.length);\n\n // Cache fetched ArrayBuffers by URL so re-parses skip the network.\n const bufferCacheRef = useRef(new Map<string, ArrayBuffer>());\n\n useEffect(() => {\n if (configs.length === 0) {\n setTracks([]);\n setLoading(false);\n setLoadedCount(0);\n setTotalCount(0);\n return;\n }\n\n let cancelled = false;\n const abortController = new AbortController();\n\n const createTrackFromNotes = (\n config: MidiTrackConfig,\n notes: MidiNoteData[],\n trackName: string,\n noteDuration: number,\n midiChannel?: number,\n midiProgram?: number\n ): ClipTrack => {\n const sampleRate = config.sampleRate ?? 44100;\n const clipDuration = config.duration ?? noteDuration;\n\n const clip = createClipFromSeconds({\n startTime: config.startTime ?? 0,\n duration: clipDuration,\n sampleRate,\n sourceDuration: clipDuration,\n midiNotes: notes,\n midiChannel,\n midiProgram,\n name: trackName,\n });\n\n return createTrack({\n name: trackName,\n clips: [clip],\n muted: config.muted ?? false,\n soloed: config.soloed ?? false,\n volume: config.volume ?? 1.0,\n pan: config.pan ?? 0,\n color: config.color,\n });\n };\n\n // Check if all src URLs are already cached — if so, skip loading state\n // to avoid flashing the loading overlay on re-parse-only changes.\n const allCached = configs.every((c) => !c.src || bufferCacheRef.current.has(c.src));\n\n const loadTracks = async () => {\n try {\n if (!allCached) {\n setLoading(true);\n setLoadedCount(0);\n }\n setError(null);\n\n const allTracks: ClipTrack[] = [];\n\n for (const config of configs) {\n if (cancelled) break;\n\n if (config.midiNotes) {\n // Pre-parsed notes — no fetch needed\n const notes = config.midiNotes;\n const duration =\n notes.length > 0 ? Math.max(...notes.map((n) => n.time + n.duration)) : 0;\n const trackName = config.name || 'MIDI Track';\n allTracks.push(createTrackFromNotes(config, notes, trackName, duration));\n } else if (config.src) {\n // Use cached buffer if available, otherwise fetch\n let buffer = bufferCacheRef.current.get(config.src);\n if (!buffer) {\n const response = await fetch(config.src, {\n signal: abortController.signal,\n });\n if (!response.ok) {\n throw new Error(`Failed to fetch ${config.src}: ${response.statusText}`);\n }\n buffer = await response.arrayBuffer();\n bufferCacheRef.current.set(config.src, buffer);\n }\n\n const parsed = parseMidiFile(buffer, {\n flatten: config.flatten,\n });\n for (const parsedTrack of parsed.tracks) {\n if (cancelled) break;\n const trackName = parsedTrack.name;\n allTracks.push(\n createTrackFromNotes(\n config,\n parsedTrack.notes,\n trackName,\n parsedTrack.duration,\n parsedTrack.channel,\n parsedTrack.programNumber\n )\n );\n }\n } else {\n throw new Error('MIDI track config must provide src or midiNotes');\n }\n }\n\n if (!cancelled) {\n setTracks(allTracks);\n setLoadedCount(allTracks.length);\n setTotalCount(allTracks.length);\n setLoading(false);\n }\n } catch (err) {\n if (!cancelled) {\n const errorMessage = err instanceof Error ? err.message : 'Unknown error loading MIDI';\n setError(errorMessage);\n setLoading(false);\n console.error('Error loading MIDI tracks:', err);\n }\n }\n };\n\n loadTracks();\n\n return () => {\n cancelled = true;\n abortController.abort();\n };\n }, [configs]);\n\n return { tracks, loading, error, loadedCount, totalCount };\n}\n"],"mappings":"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","names":["duration"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// src/parseMidiFile.ts
|
|
2
|
+
import { Midi } from "@tonejs/midi";
|
|
3
|
+
function titleCase(str) {
|
|
4
|
+
return str.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
5
|
+
}
|
|
6
|
+
function mapNotes(track) {
|
|
7
|
+
return track.notes.map((note) => ({
|
|
8
|
+
midi: note.midi,
|
|
9
|
+
name: note.name,
|
|
10
|
+
time: note.time,
|
|
11
|
+
duration: note.duration,
|
|
12
|
+
velocity: note.velocity,
|
|
13
|
+
channel: track.channel
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
function getTrackDuration(notes) {
|
|
17
|
+
if (notes.length === 0) return 0;
|
|
18
|
+
return Math.max(...notes.map((n) => n.time + n.duration));
|
|
19
|
+
}
|
|
20
|
+
function parseMidiFile(data, options = {}) {
|
|
21
|
+
const midi = new Midi(data);
|
|
22
|
+
const bpm = midi.header.tempos.length > 0 ? midi.header.tempos[0].bpm : 120;
|
|
23
|
+
const timeSig = midi.header.timeSignatures.length > 0 ? midi.header.timeSignatures[0].timeSignature : [4, 4];
|
|
24
|
+
const parsedTracks = midi.tracks.filter((track) => track.notes.length > 0).map((track) => {
|
|
25
|
+
const notes = mapNotes(track);
|
|
26
|
+
const instrument = track.instrument.name;
|
|
27
|
+
const programNumber = track.instrument.number;
|
|
28
|
+
let name;
|
|
29
|
+
if (track.channel === 9) {
|
|
30
|
+
name = "Drums";
|
|
31
|
+
} else if (programNumber > 0) {
|
|
32
|
+
name = titleCase(instrument);
|
|
33
|
+
} else {
|
|
34
|
+
name = track.name.trim() || titleCase(instrument) || `Channel ${track.channel + 1}`;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
name,
|
|
38
|
+
notes,
|
|
39
|
+
duration: getTrackDuration(notes),
|
|
40
|
+
channel: track.channel,
|
|
41
|
+
instrument,
|
|
42
|
+
programNumber: track.instrument.number
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
if (options.flatten && parsedTracks.length > 0) {
|
|
46
|
+
const allNotes = parsedTracks.flatMap((t) => t.notes);
|
|
47
|
+
allNotes.sort((a, b) => a.time - b.time);
|
|
48
|
+
const duration2 = getTrackDuration(allNotes);
|
|
49
|
+
return {
|
|
50
|
+
tracks: [
|
|
51
|
+
{
|
|
52
|
+
name: midi.name || "MIDI",
|
|
53
|
+
notes: allNotes,
|
|
54
|
+
duration: duration2,
|
|
55
|
+
channel: parsedTracks[0].channel,
|
|
56
|
+
instrument: parsedTracks[0].instrument,
|
|
57
|
+
programNumber: parsedTracks[0].programNumber
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
duration: duration2,
|
|
61
|
+
name: midi.name || "",
|
|
62
|
+
bpm,
|
|
63
|
+
timeSignature: timeSig
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const duration = parsedTracks.length > 0 ? Math.max(...parsedTracks.map((t) => t.duration)) : 0;
|
|
67
|
+
return {
|
|
68
|
+
tracks: parsedTracks,
|
|
69
|
+
duration,
|
|
70
|
+
name: midi.name || "",
|
|
71
|
+
bpm,
|
|
72
|
+
timeSignature: timeSig
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function parseMidiUrl(url, options = {}, signal) {
|
|
76
|
+
const response = await fetch(url, { signal });
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
throw new Error(`Failed to fetch MIDI file ${url}: ${response.statusText}`);
|
|
79
|
+
}
|
|
80
|
+
const buffer = await response.arrayBuffer();
|
|
81
|
+
return parseMidiFile(buffer, options);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/useMidiTracks.ts
|
|
85
|
+
import { useState, useEffect, useRef } from "react";
|
|
86
|
+
import {
|
|
87
|
+
createClipFromSeconds,
|
|
88
|
+
createTrack
|
|
89
|
+
} from "@waveform-playlist/core";
|
|
90
|
+
function useMidiTracks(configs) {
|
|
91
|
+
const [tracks, setTracks] = useState([]);
|
|
92
|
+
const [loading, setLoading] = useState(true);
|
|
93
|
+
const [error, setError] = useState(null);
|
|
94
|
+
const [loadedCount, setLoadedCount] = useState(0);
|
|
95
|
+
const [totalCount, setTotalCount] = useState(configs.length);
|
|
96
|
+
const bufferCacheRef = useRef(/* @__PURE__ */ new Map());
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (configs.length === 0) {
|
|
99
|
+
setTracks([]);
|
|
100
|
+
setLoading(false);
|
|
101
|
+
setLoadedCount(0);
|
|
102
|
+
setTotalCount(0);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
let cancelled = false;
|
|
106
|
+
const abortController = new AbortController();
|
|
107
|
+
const createTrackFromNotes = (config, notes, trackName, noteDuration, midiChannel, midiProgram) => {
|
|
108
|
+
const sampleRate = config.sampleRate ?? 44100;
|
|
109
|
+
const clipDuration = config.duration ?? noteDuration;
|
|
110
|
+
const clip = createClipFromSeconds({
|
|
111
|
+
startTime: config.startTime ?? 0,
|
|
112
|
+
duration: clipDuration,
|
|
113
|
+
sampleRate,
|
|
114
|
+
sourceDuration: clipDuration,
|
|
115
|
+
midiNotes: notes,
|
|
116
|
+
midiChannel,
|
|
117
|
+
midiProgram,
|
|
118
|
+
name: trackName
|
|
119
|
+
});
|
|
120
|
+
return createTrack({
|
|
121
|
+
name: trackName,
|
|
122
|
+
clips: [clip],
|
|
123
|
+
muted: config.muted ?? false,
|
|
124
|
+
soloed: config.soloed ?? false,
|
|
125
|
+
volume: config.volume ?? 1,
|
|
126
|
+
pan: config.pan ?? 0,
|
|
127
|
+
color: config.color
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
const allCached = configs.every((c) => !c.src || bufferCacheRef.current.has(c.src));
|
|
131
|
+
const loadTracks = async () => {
|
|
132
|
+
try {
|
|
133
|
+
if (!allCached) {
|
|
134
|
+
setLoading(true);
|
|
135
|
+
setLoadedCount(0);
|
|
136
|
+
}
|
|
137
|
+
setError(null);
|
|
138
|
+
const allTracks = [];
|
|
139
|
+
for (const config of configs) {
|
|
140
|
+
if (cancelled) break;
|
|
141
|
+
if (config.midiNotes) {
|
|
142
|
+
const notes = config.midiNotes;
|
|
143
|
+
const duration = notes.length > 0 ? Math.max(...notes.map((n) => n.time + n.duration)) : 0;
|
|
144
|
+
const trackName = config.name || "MIDI Track";
|
|
145
|
+
allTracks.push(createTrackFromNotes(config, notes, trackName, duration));
|
|
146
|
+
} else if (config.src) {
|
|
147
|
+
let buffer = bufferCacheRef.current.get(config.src);
|
|
148
|
+
if (!buffer) {
|
|
149
|
+
const response = await fetch(config.src, {
|
|
150
|
+
signal: abortController.signal
|
|
151
|
+
});
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
throw new Error(`Failed to fetch ${config.src}: ${response.statusText}`);
|
|
154
|
+
}
|
|
155
|
+
buffer = await response.arrayBuffer();
|
|
156
|
+
bufferCacheRef.current.set(config.src, buffer);
|
|
157
|
+
}
|
|
158
|
+
const parsed = parseMidiFile(buffer, {
|
|
159
|
+
flatten: config.flatten
|
|
160
|
+
});
|
|
161
|
+
for (const parsedTrack of parsed.tracks) {
|
|
162
|
+
if (cancelled) break;
|
|
163
|
+
const trackName = parsedTrack.name;
|
|
164
|
+
allTracks.push(
|
|
165
|
+
createTrackFromNotes(
|
|
166
|
+
config,
|
|
167
|
+
parsedTrack.notes,
|
|
168
|
+
trackName,
|
|
169
|
+
parsedTrack.duration,
|
|
170
|
+
parsedTrack.channel,
|
|
171
|
+
parsedTrack.programNumber
|
|
172
|
+
)
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
throw new Error("MIDI track config must provide src or midiNotes");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (!cancelled) {
|
|
180
|
+
setTracks(allTracks);
|
|
181
|
+
setLoadedCount(allTracks.length);
|
|
182
|
+
setTotalCount(allTracks.length);
|
|
183
|
+
setLoading(false);
|
|
184
|
+
}
|
|
185
|
+
} catch (err) {
|
|
186
|
+
if (!cancelled) {
|
|
187
|
+
const errorMessage = err instanceof Error ? err.message : "Unknown error loading MIDI";
|
|
188
|
+
setError(errorMessage);
|
|
189
|
+
setLoading(false);
|
|
190
|
+
console.error("Error loading MIDI tracks:", err);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
loadTracks();
|
|
195
|
+
return () => {
|
|
196
|
+
cancelled = true;
|
|
197
|
+
abortController.abort();
|
|
198
|
+
};
|
|
199
|
+
}, [configs]);
|
|
200
|
+
return { tracks, loading, error, loadedCount, totalCount };
|
|
201
|
+
}
|
|
202
|
+
export {
|
|
203
|
+
parseMidiFile,
|
|
204
|
+
parseMidiUrl,
|
|
205
|
+
useMidiTracks
|
|
206
|
+
};
|
|
207
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/parseMidiFile.ts","../src/useMidiTracks.ts"],"sourcesContent":["import { Midi } from '@tonejs/midi';\nimport type { MidiNoteData } from '@waveform-playlist/core';\n\nexport interface ParsedMidiTrack {\n /** Track name from the MIDI file */\n name: string;\n /** Parsed notes in MidiNoteData format */\n notes: MidiNoteData[];\n /** Duration in seconds (end of last note) */\n duration: number;\n /** MIDI channel (9/10 = percussion) */\n channel: number;\n /** Instrument name from MIDI program change */\n instrument: string;\n /** GM program number (0-127) from MIDI program change event */\n programNumber: number;\n}\n\nexport interface ParsedMidi {\n /** Individual MIDI tracks with their notes */\n tracks: ParsedMidiTrack[];\n /** Total duration in seconds (max of all track durations) */\n duration: number;\n /** Song name from MIDI header */\n name: string;\n /** First tempo in BPM (default 120 if none specified) */\n bpm: number;\n /** Time signature as [numerator, denominator] (default [4, 4]) */\n timeSignature: [number, number];\n}\n\nexport interface ParseMidiOptions {\n /** When true, merges all MIDI tracks into a single ParsedMidiTrack */\n flatten?: boolean;\n}\n\n/**\n * Title-case an instrument name from @tonejs/midi.\n * \"acoustic grand piano\" → \"Acoustic Grand Piano\"\n * \"electric bass (finger)\" → \"Electric Bass (Finger)\"\n */\nfunction titleCase(str: string): string {\n return str.replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\nfunction mapNotes(track: Midi['tracks'][number]): MidiNoteData[] {\n return track.notes.map((note) => ({\n midi: note.midi,\n name: note.name,\n time: note.time,\n duration: note.duration,\n velocity: note.velocity,\n channel: track.channel,\n }));\n}\n\nfunction getTrackDuration(notes: MidiNoteData[]): number {\n if (notes.length === 0) return 0;\n return Math.max(...notes.map((n) => n.time + n.duration));\n}\n\n/**\n * Parse a MIDI file from an ArrayBuffer.\n *\n * Returns structured data with tracks, notes, tempo, and time signature.\n * Notes are already in seconds (tempo-adjusted by @tonejs/midi).\n */\nexport function parseMidiFile(data: ArrayBuffer, options: ParseMidiOptions = {}): ParsedMidi {\n const midi = new Midi(data);\n\n const bpm = midi.header.tempos.length > 0 ? midi.header.tempos[0].bpm : 120;\n\n const timeSig =\n midi.header.timeSignatures.length > 0 ? midi.header.timeSignatures[0].timeSignature : [4, 4];\n\n // Parse all tracks that have notes\n const parsedTracks: ParsedMidiTrack[] = midi.tracks\n .filter((track) => track.notes.length > 0)\n .map((track) => {\n const notes = mapNotes(track);\n const instrument = track.instrument.name;\n const programNumber = track.instrument.number;\n // Channel 9 = GM percussion. Use \"Drums\" for clarity.\n // For melodic tracks: prefer GM instrument name when program is non-default (> 0),\n // since @tonejs/midi defaults to \"acoustic grand piano\" even with no program change.\n // Fall back to track name, then channel number.\n let name: string;\n if (track.channel === 9) {\n name = 'Drums';\n } else if (programNumber > 0) {\n name = titleCase(instrument);\n } else {\n name = track.name.trim() || titleCase(instrument) || `Channel ${track.channel + 1}`;\n }\n return {\n name,\n notes,\n duration: getTrackDuration(notes),\n channel: track.channel,\n instrument,\n programNumber: track.instrument.number,\n };\n });\n\n if (options.flatten && parsedTracks.length > 0) {\n const allNotes = parsedTracks.flatMap((t) => t.notes);\n allNotes.sort((a, b) => a.time - b.time);\n const duration = getTrackDuration(allNotes);\n\n return {\n tracks: [\n {\n name: midi.name || 'MIDI',\n notes: allNotes,\n duration,\n channel: parsedTracks[0].channel,\n instrument: parsedTracks[0].instrument,\n programNumber: parsedTracks[0].programNumber,\n },\n ],\n duration,\n name: midi.name || '',\n bpm,\n timeSignature: timeSig as [number, number],\n };\n }\n\n const duration = parsedTracks.length > 0 ? Math.max(...parsedTracks.map((t) => t.duration)) : 0;\n\n return {\n tracks: parsedTracks,\n duration,\n name: midi.name || '',\n bpm,\n timeSignature: timeSig as [number, number],\n };\n}\n\n/**\n * Fetch and parse a MIDI file from a URL.\n *\n * Supports AbortSignal for cancellation (e.g., component unmount cleanup).\n */\nexport async function parseMidiUrl(\n url: string,\n options: ParseMidiOptions = {},\n signal?: AbortSignal\n): Promise<ParsedMidi> {\n const response = await fetch(url, { signal });\n if (!response.ok) {\n throw new Error(`Failed to fetch MIDI file ${url}: ${response.statusText}`);\n }\n const buffer = await response.arrayBuffer();\n return parseMidiFile(buffer, options);\n}\n","import { useState, useEffect, useRef } from 'react';\nimport {\n type ClipTrack,\n type MidiNoteData,\n createClipFromSeconds,\n createTrack,\n} from '@waveform-playlist/core';\nimport { parseMidiFile } from './parseMidiFile';\n\n/**\n * Configuration for a single MIDI track to load.\n *\n * MIDI data can be provided in two ways:\n * 1. `src` — URL to a .mid file (fetched, parsed with @tonejs/midi)\n * 2. `midiNotes` — Pre-parsed notes (skip fetch+parse)\n */\nexport interface MidiTrackConfig {\n /** URL to .mid file */\n src?: string;\n /** Pre-parsed MIDI notes (skip fetch+parse) */\n midiNotes?: MidiNoteData[];\n /** Track display name */\n name?: string;\n /** Whether this track is muted */\n muted?: boolean;\n /** Whether this track is soloed */\n soloed?: boolean;\n /** Track volume (default 1.0) */\n volume?: number;\n /** Stereo pan (default 0) */\n pan?: number;\n /** Track color */\n color?: string;\n /** Clip position on timeline in seconds (default 0) */\n startTime?: number;\n /** Override clip duration in seconds (default: derived from last note) */\n duration?: number;\n /** Sample rate for sample-based positioning (default 44100) */\n sampleRate?: number;\n /** Merge all MIDI tracks from the file into one ClipTrack (default false) */\n flatten?: boolean;\n}\n\nexport interface UseMidiTracksReturn {\n /** Loaded ClipTrack array with midiNotes on clips */\n tracks: ClipTrack[];\n /** Whether any tracks are still loading */\n loading: boolean;\n /** Error message if loading failed, null otherwise */\n error: string | null;\n /** Number of tracks that have finished loading */\n loadedCount: number;\n /** Total number of tracks (known after parsing) */\n totalCount: number;\n}\n\n/**\n * Hook to load MIDI files and convert to ClipTrack format with midiNotes.\n *\n * All tracks are returned at once after loading completes. This ensures\n * all track controls and layout containers appear simultaneously in React,\n * while canvas rendering is deferred to the UI layer for progressive drawing.\n *\n * @example\n * ```typescript\n * const { tracks, loading, error } = useMidiTracks([\n * { src: '/music/song.mid', name: 'Piano' },\n * ]);\n *\n * // Pre-parsed notes (no fetch)\n * const { tracks } = useMidiTracks([\n * { midiNotes: myNotes, name: 'Synth Lead', duration: 30 },\n * ]);\n * ```\n */\nexport function useMidiTracks(configs: MidiTrackConfig[]): UseMidiTracksReturn {\n const [tracks, setTracks] = useState<ClipTrack[]>([]);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [loadedCount, setLoadedCount] = useState(0);\n const [totalCount, setTotalCount] = useState(configs.length);\n\n // Cache fetched ArrayBuffers by URL so re-parses skip the network.\n const bufferCacheRef = useRef(new Map<string, ArrayBuffer>());\n\n useEffect(() => {\n if (configs.length === 0) {\n setTracks([]);\n setLoading(false);\n setLoadedCount(0);\n setTotalCount(0);\n return;\n }\n\n let cancelled = false;\n const abortController = new AbortController();\n\n const createTrackFromNotes = (\n config: MidiTrackConfig,\n notes: MidiNoteData[],\n trackName: string,\n noteDuration: number,\n midiChannel?: number,\n midiProgram?: number\n ): ClipTrack => {\n const sampleRate = config.sampleRate ?? 44100;\n const clipDuration = config.duration ?? noteDuration;\n\n const clip = createClipFromSeconds({\n startTime: config.startTime ?? 0,\n duration: clipDuration,\n sampleRate,\n sourceDuration: clipDuration,\n midiNotes: notes,\n midiChannel,\n midiProgram,\n name: trackName,\n });\n\n return createTrack({\n name: trackName,\n clips: [clip],\n muted: config.muted ?? false,\n soloed: config.soloed ?? false,\n volume: config.volume ?? 1.0,\n pan: config.pan ?? 0,\n color: config.color,\n });\n };\n\n // Check if all src URLs are already cached — if so, skip loading state\n // to avoid flashing the loading overlay on re-parse-only changes.\n const allCached = configs.every((c) => !c.src || bufferCacheRef.current.has(c.src));\n\n const loadTracks = async () => {\n try {\n if (!allCached) {\n setLoading(true);\n setLoadedCount(0);\n }\n setError(null);\n\n const allTracks: ClipTrack[] = [];\n\n for (const config of configs) {\n if (cancelled) break;\n\n if (config.midiNotes) {\n // Pre-parsed notes — no fetch needed\n const notes = config.midiNotes;\n const duration =\n notes.length > 0 ? Math.max(...notes.map((n) => n.time + n.duration)) : 0;\n const trackName = config.name || 'MIDI Track';\n allTracks.push(createTrackFromNotes(config, notes, trackName, duration));\n } else if (config.src) {\n // Use cached buffer if available, otherwise fetch\n let buffer = bufferCacheRef.current.get(config.src);\n if (!buffer) {\n const response = await fetch(config.src, {\n signal: abortController.signal,\n });\n if (!response.ok) {\n throw new Error(`Failed to fetch ${config.src}: ${response.statusText}`);\n }\n buffer = await response.arrayBuffer();\n bufferCacheRef.current.set(config.src, buffer);\n }\n\n const parsed = parseMidiFile(buffer, {\n flatten: config.flatten,\n });\n for (const parsedTrack of parsed.tracks) {\n if (cancelled) break;\n const trackName = parsedTrack.name;\n allTracks.push(\n createTrackFromNotes(\n config,\n parsedTrack.notes,\n trackName,\n parsedTrack.duration,\n parsedTrack.channel,\n parsedTrack.programNumber\n )\n );\n }\n } else {\n throw new Error('MIDI track config must provide src or midiNotes');\n }\n }\n\n if (!cancelled) {\n setTracks(allTracks);\n setLoadedCount(allTracks.length);\n setTotalCount(allTracks.length);\n setLoading(false);\n }\n } catch (err) {\n if (!cancelled) {\n const errorMessage = err instanceof Error ? err.message : 'Unknown error loading MIDI';\n setError(errorMessage);\n setLoading(false);\n console.error('Error loading MIDI tracks:', err);\n }\n }\n };\n\n loadTracks();\n\n return () => {\n cancelled = true;\n abortController.abort();\n };\n }, [configs]);\n\n return { tracks, loading, error, loadedCount, totalCount };\n}\n"],"mappings":"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","names":["duration"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@waveform-playlist/midi",
|
|
3
|
+
"version": "9.1.0",
|
|
4
|
+
"description": "MIDI file loading and parsing for waveform-playlist",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"keywords": [
|
|
17
|
+
"waveform",
|
|
18
|
+
"audio",
|
|
19
|
+
"midi",
|
|
20
|
+
"waveform-playlist"
|
|
21
|
+
],
|
|
22
|
+
"author": "Naomi Aro",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/naomiaro/waveform-playlist.git",
|
|
27
|
+
"directory": "packages/midi"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://naomiaro.github.io/waveform-playlist",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/naomiaro/waveform-playlist/issues"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@tonejs/midi": "^2.0.28",
|
|
39
|
+
"@waveform-playlist/core": "9.1.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@testing-library/react": "^16.0.0",
|
|
43
|
+
"@types/react": "^18.2.45",
|
|
44
|
+
"jsdom": "^25.0.0",
|
|
45
|
+
"react": "^18.3.1",
|
|
46
|
+
"react-dom": "^18.3.1",
|
|
47
|
+
"tsup": "^8.0.1",
|
|
48
|
+
"typescript": "^5.3.3",
|
|
49
|
+
"vitest": "^3.0.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"react": "^18.0.0"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsup",
|
|
56
|
+
"dev": "tsup --watch",
|
|
57
|
+
"typecheck": "tsc --noEmit",
|
|
58
|
+
"test": "vitest run"
|
|
59
|
+
}
|
|
60
|
+
}
|