@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 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.
@@ -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 };
@@ -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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,kBAAqB;AAyCrB,SAAS,UAAU,KAAqB;AACtC,SAAO,IAAI,QAAQ,SAAS,CAAC,MAAM,EAAE,YAAY,CAAC;AACpD;AAEA,SAAS,SAAS,OAA+C;AAC/D,SAAO,MAAM,MAAM,IAAI,CAAC,UAAU;AAAA,IAChC,MAAM,KAAK;AAAA,IACX,MAAM,KAAK;AAAA,IACX,MAAM,KAAK;AAAA,IACX,UAAU,KAAK;AAAA,IACf,UAAU,KAAK;AAAA,IACf,SAAS,MAAM;AAAA,EACjB,EAAE;AACJ;AAEA,SAAS,iBAAiB,OAA+B;AACvD,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,SAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC;AAC1D;AAQO,SAAS,cAAc,MAAmB,UAA4B,CAAC,GAAe;AAC3F,QAAM,OAAO,IAAI,iBAAK,IAAI;AAE1B,QAAM,MAAM,KAAK,OAAO,OAAO,SAAS,IAAI,KAAK,OAAO,OAAO,CAAC,EAAE,MAAM;AAExE,QAAM,UACJ,KAAK,OAAO,eAAe,SAAS,IAAI,KAAK,OAAO,eAAe,CAAC,EAAE,gBAAgB,CAAC,GAAG,CAAC;AAG7F,QAAM,eAAkC,KAAK,OAC1C,OAAO,CAAC,UAAU,MAAM,MAAM,SAAS,CAAC,EACxC,IAAI,CAAC,UAAU;AACd,UAAM,QAAQ,SAAS,KAAK;AAC5B,UAAM,aAAa,MAAM,WAAW;AACpC,UAAM,gBAAgB,MAAM,WAAW;AAKvC,QAAI;AACJ,QAAI,MAAM,YAAY,GAAG;AACvB,aAAO;AAAA,IACT,WAAW,gBAAgB,GAAG;AAC5B,aAAO,UAAU,UAAU;AAAA,IAC7B,OAAO;AACL,aAAO,MAAM,KAAK,KAAK,KAAK,UAAU,UAAU,KAAK,WAAW,MAAM,UAAU,CAAC;AAAA,IACnF;AACA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,UAAU,iBAAiB,KAAK;AAAA,MAChC,SAAS,MAAM;AAAA,MACf;AAAA,MACA,eAAe,MAAM,WAAW;AAAA,IAClC;AAAA,EACF,CAAC;AAEH,MAAI,QAAQ,WAAW,aAAa,SAAS,GAAG;AAC9C,UAAM,WAAW,aAAa,QAAQ,CAAC,MAAM,EAAE,KAAK;AACpD,aAAS,KAAK,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,IAAI;AACvC,UAAMA,YAAW,iBAAiB,QAAQ;AAE1C,WAAO;AAAA,MACL,QAAQ;AAAA,QACN;AAAA,UACE,MAAM,KAAK,QAAQ;AAAA,UACnB,OAAO;AAAA,UACP,UAAAA;AAAA,UACA,SAAS,aAAa,CAAC,EAAE;AAAA,UACzB,YAAY,aAAa,CAAC,EAAE;AAAA,UAC5B,eAAe,aAAa,CAAC,EAAE;AAAA,QACjC;AAAA,MACF;AAAA,MACA,UAAAA;AAAA,MACA,MAAM,KAAK,QAAQ;AAAA,MACnB;AAAA,MACA,eAAe;AAAA,IACjB;AAAA,EACF;AAEA,QAAM,WAAW,aAAa,SAAS,IAAI,KAAK,IAAI,GAAG,aAAa,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI;AAE9F,SAAO;AAAA,IACL,QAAQ;AAAA,IACR;AAAA,IACA,MAAM,KAAK,QAAQ;AAAA,IACnB;AAAA,IACA,eAAe;AAAA,EACjB;AACF;AAOA,eAAsB,aACpB,KACA,UAA4B,CAAC,GAC7B,QACqB;AACrB,QAAM,WAAW,MAAM,MAAM,KAAK,EAAE,OAAO,CAAC;AAC5C,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,6BAA6B,GAAG,KAAK,SAAS,UAAU,EAAE;AAAA,EAC5E;AACA,QAAM,SAAS,MAAM,SAAS,YAAY;AAC1C,SAAO,cAAc,QAAQ,OAAO;AACtC;;;AC1JA,mBAA4C;AAC5C,kBAKO;AAqEA,SAAS,cAAc,SAAiD;AAC7E,QAAM,CAAC,QAAQ,SAAS,QAAI,uBAAsB,CAAC,CAAC;AACpD,QAAM,CAAC,SAAS,UAAU,QAAI,uBAAS,IAAI;AAC3C,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAwB,IAAI;AACtD,QAAM,CAAC,aAAa,cAAc,QAAI,uBAAS,CAAC;AAChD,QAAM,CAAC,YAAY,aAAa,QAAI,uBAAS,QAAQ,MAAM;AAG3D,QAAM,qBAAiB,qBAAO,oBAAI,IAAyB,CAAC;AAE5D,8BAAU,MAAM;AACd,QAAI,QAAQ,WAAW,GAAG;AACxB,gBAAU,CAAC,CAAC;AACZ,iBAAW,KAAK;AAChB,qBAAe,CAAC;AAChB,oBAAc,CAAC;AACf;AAAA,IACF;AAEA,QAAI,YAAY;AAChB,UAAM,kBAAkB,IAAI,gBAAgB;AAE5C,UAAM,uBAAuB,CAC3B,QACA,OACA,WACA,cACA,aACA,gBACc;AACd,YAAM,aAAa,OAAO,cAAc;AACxC,YAAM,eAAe,OAAO,YAAY;AAExC,YAAM,WAAO,mCAAsB;AAAA,QACjC,WAAW,OAAO,aAAa;AAAA,QAC/B,UAAU;AAAA,QACV;AAAA,QACA,gBAAgB;AAAA,QAChB,WAAW;AAAA,QACX;AAAA,QACA;AAAA,QACA,MAAM;AAAA,MACR,CAAC;AAED,iBAAO,yBAAY;AAAA,QACjB,MAAM;AAAA,QACN,OAAO,CAAC,IAAI;AAAA,QACZ,OAAO,OAAO,SAAS;AAAA,QACvB,QAAQ,OAAO,UAAU;AAAA,QACzB,QAAQ,OAAO,UAAU;AAAA,QACzB,KAAK,OAAO,OAAO;AAAA,QACnB,OAAO,OAAO;AAAA,MAChB,CAAC;AAAA,IACH;AAIA,UAAM,YAAY,QAAQ,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,eAAe,QAAQ,IAAI,EAAE,GAAG,CAAC;AAElF,UAAM,aAAa,YAAY;AAC7B,UAAI;AACF,YAAI,CAAC,WAAW;AACd,qBAAW,IAAI;AACf,yBAAe,CAAC;AAAA,QAClB;AACA,iBAAS,IAAI;AAEb,cAAM,YAAyB,CAAC;AAEhC,mBAAW,UAAU,SAAS;AAC5B,cAAI,UAAW;AAEf,cAAI,OAAO,WAAW;AAEpB,kBAAM,QAAQ,OAAO;AACrB,kBAAM,WACJ,MAAM,SAAS,IAAI,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,IAAI;AAC1E,kBAAM,YAAY,OAAO,QAAQ;AACjC,sBAAU,KAAK,qBAAqB,QAAQ,OAAO,WAAW,QAAQ,CAAC;AAAA,UACzE,WAAW,OAAO,KAAK;AAErB,gBAAI,SAAS,eAAe,QAAQ,IAAI,OAAO,GAAG;AAClD,gBAAI,CAAC,QAAQ;AACX,oBAAM,WAAW,MAAM,MAAM,OAAO,KAAK;AAAA,gBACvC,QAAQ,gBAAgB;AAAA,cAC1B,CAAC;AACD,kBAAI,CAAC,SAAS,IAAI;AAChB,sBAAM,IAAI,MAAM,mBAAmB,OAAO,GAAG,KAAK,SAAS,UAAU,EAAE;AAAA,cACzE;AACA,uBAAS,MAAM,SAAS,YAAY;AACpC,6BAAe,QAAQ,IAAI,OAAO,KAAK,MAAM;AAAA,YAC/C;AAEA,kBAAM,SAAS,cAAc,QAAQ;AAAA,cACnC,SAAS,OAAO;AAAA,YAClB,CAAC;AACD,uBAAW,eAAe,OAAO,QAAQ;AACvC,kBAAI,UAAW;AACf,oBAAM,YAAY,YAAY;AAC9B,wBAAU;AAAA,gBACR;AAAA,kBACE;AAAA,kBACA,YAAY;AAAA,kBACZ;AAAA,kBACA,YAAY;AAAA,kBACZ,YAAY;AAAA,kBACZ,YAAY;AAAA,gBACd;AAAA,cACF;AAAA,YACF;AAAA,UACF,OAAO;AACL,kBAAM,IAAI,MAAM,iDAAiD;AAAA,UACnE;AAAA,QACF;AAEA,YAAI,CAAC,WAAW;AACd,oBAAU,SAAS;AACnB,yBAAe,UAAU,MAAM;AAC/B,wBAAc,UAAU,MAAM;AAC9B,qBAAW,KAAK;AAAA,QAClB;AAAA,MACF,SAAS,KAAK;AACZ,YAAI,CAAC,WAAW;AACd,gBAAM,eAAe,eAAe,QAAQ,IAAI,UAAU;AAC1D,mBAAS,YAAY;AACrB,qBAAW,KAAK;AAChB,kBAAQ,MAAM,8BAA8B,GAAG;AAAA,QACjD;AAAA,MACF;AAAA,IACF;AAEA,eAAW;AAEX,WAAO,MAAM;AACX,kBAAY;AACZ,sBAAgB,MAAM;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,OAAO,CAAC;AAEZ,SAAO,EAAE,QAAQ,SAAS,OAAO,aAAa,WAAW;AAC3D;","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
+ }