@waveform-playlist/midi 10.4.0 → 11.0.1

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/dist/index.d.mts CHANGED
@@ -72,8 +72,6 @@ interface MidiTrackConfig {
72
72
  startTime?: number;
73
73
  /** Override clip duration in seconds (default: derived from last note) */
74
74
  duration?: number;
75
- /** Sample rate for sample-based positioning — pass AudioContext.sampleRate */
76
- sampleRate: number;
77
75
  /** Merge all MIDI tracks from the file into one ClipTrack (default false) */
78
76
  flatten?: boolean;
79
77
  }
@@ -89,6 +87,10 @@ interface UseMidiTracksReturn {
89
87
  /** Total number of tracks (known after parsing) */
90
88
  totalCount: number;
91
89
  }
90
+ interface UseMidiTracksOptions {
91
+ /** Sample rate for sample-based timeline positioning. Pass AudioContext.sampleRate. */
92
+ sampleRate: number;
93
+ }
92
94
  /**
93
95
  * Hook to load MIDI files and convert to ClipTrack format with midiNotes.
94
96
  *
@@ -98,16 +100,18 @@ interface UseMidiTracksReturn {
98
100
  *
99
101
  * @example
100
102
  * ```typescript
101
- * const { tracks, loading, error } = useMidiTracks([
102
- * { src: '/music/song.mid', name: 'Piano' },
103
- * ]);
103
+ * const { tracks, loading, error } = useMidiTracks(
104
+ * [{ src: '/music/song.mid', name: 'Piano' }],
105
+ * { sampleRate: audioContext.sampleRate }
106
+ * );
104
107
  *
105
108
  * // Pre-parsed notes (no fetch)
106
- * const { tracks } = useMidiTracks([
107
- * { midiNotes: myNotes, name: 'Synth Lead', duration: 30 },
108
- * ]);
109
+ * const { tracks } = useMidiTracks(
110
+ * [{ midiNotes: myNotes, name: 'Synth Lead', duration: 30 }],
111
+ * { sampleRate: 48000 }
112
+ * );
109
113
  * ```
110
114
  */
111
- declare function useMidiTracks(configs: MidiTrackConfig[]): UseMidiTracksReturn;
115
+ declare function useMidiTracks(configs: MidiTrackConfig[], options: UseMidiTracksOptions): UseMidiTracksReturn;
112
116
 
113
- export { type MidiTrackConfig, type ParseMidiOptions, type ParsedMidi, type ParsedMidiTrack, type UseMidiTracksReturn, parseMidiFile, parseMidiUrl, useMidiTracks };
117
+ export { type MidiTrackConfig, type ParseMidiOptions, type ParsedMidi, type ParsedMidiTrack, type UseMidiTracksOptions, type UseMidiTracksReturn, parseMidiFile, parseMidiUrl, useMidiTracks };
package/dist/index.d.ts CHANGED
@@ -72,8 +72,6 @@ interface MidiTrackConfig {
72
72
  startTime?: number;
73
73
  /** Override clip duration in seconds (default: derived from last note) */
74
74
  duration?: number;
75
- /** Sample rate for sample-based positioning — pass AudioContext.sampleRate */
76
- sampleRate: number;
77
75
  /** Merge all MIDI tracks from the file into one ClipTrack (default false) */
78
76
  flatten?: boolean;
79
77
  }
@@ -89,6 +87,10 @@ interface UseMidiTracksReturn {
89
87
  /** Total number of tracks (known after parsing) */
90
88
  totalCount: number;
91
89
  }
90
+ interface UseMidiTracksOptions {
91
+ /** Sample rate for sample-based timeline positioning. Pass AudioContext.sampleRate. */
92
+ sampleRate: number;
93
+ }
92
94
  /**
93
95
  * Hook to load MIDI files and convert to ClipTrack format with midiNotes.
94
96
  *
@@ -98,16 +100,18 @@ interface UseMidiTracksReturn {
98
100
  *
99
101
  * @example
100
102
  * ```typescript
101
- * const { tracks, loading, error } = useMidiTracks([
102
- * { src: '/music/song.mid', name: 'Piano' },
103
- * ]);
103
+ * const { tracks, loading, error } = useMidiTracks(
104
+ * [{ src: '/music/song.mid', name: 'Piano' }],
105
+ * { sampleRate: audioContext.sampleRate }
106
+ * );
104
107
  *
105
108
  * // Pre-parsed notes (no fetch)
106
- * const { tracks } = useMidiTracks([
107
- * { midiNotes: myNotes, name: 'Synth Lead', duration: 30 },
108
- * ]);
109
+ * const { tracks } = useMidiTracks(
110
+ * [{ midiNotes: myNotes, name: 'Synth Lead', duration: 30 }],
111
+ * { sampleRate: 48000 }
112
+ * );
109
113
  * ```
110
114
  */
111
- declare function useMidiTracks(configs: MidiTrackConfig[]): UseMidiTracksReturn;
115
+ declare function useMidiTracks(configs: MidiTrackConfig[], options: UseMidiTracksOptions): UseMidiTracksReturn;
112
116
 
113
- export { type MidiTrackConfig, type ParseMidiOptions, type ParsedMidi, type ParsedMidiTrack, type UseMidiTracksReturn, parseMidiFile, parseMidiUrl, useMidiTracks };
117
+ export { type MidiTrackConfig, type ParseMidiOptions, type ParsedMidi, type ParsedMidiTrack, type UseMidiTracksOptions, type UseMidiTracksReturn, parseMidiFile, parseMidiUrl, useMidiTracks };
package/dist/index.js CHANGED
@@ -112,7 +112,7 @@ async function parseMidiUrl(url, options = {}, signal) {
112
112
  // src/useMidiTracks.ts
113
113
  var import_react = require("react");
114
114
  var import_core = require("@waveform-playlist/core");
115
- function useMidiTracks(configs) {
115
+ function useMidiTracks(configs, options) {
116
116
  const [tracks, setTracks] = (0, import_react.useState)([]);
117
117
  const [loading, setLoading] = (0, import_react.useState)(true);
118
118
  const [error, setError] = (0, import_react.useState)(null);
@@ -130,7 +130,7 @@ function useMidiTracks(configs) {
130
130
  let cancelled = false;
131
131
  const abortController = new AbortController();
132
132
  const createTrackFromNotes = (config, notes, trackName, noteDuration, midiChannel, midiProgram) => {
133
- const sampleRate = config.sampleRate;
133
+ const sampleRate = options.sampleRate;
134
134
  const clipDuration = config.duration ?? noteDuration;
135
135
  const clip = (0, import_core.createClipFromSeconds)({
136
136
  startTime: config.startTime ?? 0,
@@ -221,7 +221,7 @@ function useMidiTracks(configs) {
221
221
  cancelled = true;
222
222
  abortController.abort();
223
223
  };
224
- }, [configs]);
224
+ }, [configs, options.sampleRate]);
225
225
  return { tracks, loading, error, loadedCount, totalCount };
226
226
  }
227
227
  // Annotate the CommonJS export names for ESM import in node:
package/dist/index.js.map CHANGED
@@ -1 +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 — pass AudioContext.sampleRate */\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;\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"]}
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, UseMidiTracksOptions, 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 /** 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\nexport interface UseMidiTracksOptions {\n /** Sample rate for sample-based timeline positioning. Pass AudioContext.sampleRate. */\n sampleRate: 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 * { sampleRate: audioContext.sampleRate }\n * );\n *\n * // Pre-parsed notes (no fetch)\n * const { tracks } = useMidiTracks(\n * [{ midiNotes: myNotes, name: 'Synth Lead', duration: 30 }],\n * { sampleRate: 48000 }\n * );\n * ```\n */\nexport function useMidiTracks(\n configs: MidiTrackConfig[],\n options: UseMidiTracksOptions\n): 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 = options.sampleRate;\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, options.sampleRate]);\n\n return { tracks, loading, error, loadedCount, totalCount };\n}\n"],"mappings":"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","names":["duration"]}
package/dist/index.mjs CHANGED
@@ -87,7 +87,7 @@ import {
87
87
  createClipFromSeconds,
88
88
  createTrack
89
89
  } from "@waveform-playlist/core";
90
- function useMidiTracks(configs) {
90
+ function useMidiTracks(configs, options) {
91
91
  const [tracks, setTracks] = useState([]);
92
92
  const [loading, setLoading] = useState(true);
93
93
  const [error, setError] = useState(null);
@@ -105,7 +105,7 @@ function useMidiTracks(configs) {
105
105
  let cancelled = false;
106
106
  const abortController = new AbortController();
107
107
  const createTrackFromNotes = (config, notes, trackName, noteDuration, midiChannel, midiProgram) => {
108
- const sampleRate = config.sampleRate;
108
+ const sampleRate = options.sampleRate;
109
109
  const clipDuration = config.duration ?? noteDuration;
110
110
  const clip = createClipFromSeconds({
111
111
  startTime: config.startTime ?? 0,
@@ -196,7 +196,7 @@ function useMidiTracks(configs) {
196
196
  cancelled = true;
197
197
  abortController.abort();
198
198
  };
199
- }, [configs]);
199
+ }, [configs, options.sampleRate]);
200
200
  return { tracks, loading, error, loadedCount, totalCount };
201
201
  }
202
202
  export {
@@ -1 +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 — pass AudioContext.sampleRate */\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;\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"]}
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 /** 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\nexport interface UseMidiTracksOptions {\n /** Sample rate for sample-based timeline positioning. Pass AudioContext.sampleRate. */\n sampleRate: 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 * { sampleRate: audioContext.sampleRate }\n * );\n *\n * // Pre-parsed notes (no fetch)\n * const { tracks } = useMidiTracks(\n * [{ midiNotes: myNotes, name: 'Synth Lead', duration: 30 }],\n * { sampleRate: 48000 }\n * );\n * ```\n */\nexport function useMidiTracks(\n configs: MidiTrackConfig[],\n options: UseMidiTracksOptions\n): 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 = options.sampleRate;\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, options.sampleRate]);\n\n return { tracks, loading, error, loadedCount, totalCount };\n}\n"],"mappings":"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","names":["duration"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waveform-playlist/midi",
3
- "version": "10.4.0",
3
+ "version": "11.0.1",
4
4
  "description": "MIDI file loading and parsing for waveform-playlist",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -36,7 +36,7 @@
36
36
  ],
37
37
  "dependencies": {
38
38
  "@tonejs/midi": "^2.0.28",
39
- "@waveform-playlist/core": "10.4.0"
39
+ "@waveform-playlist/core": "11.0.1"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@testing-library/react": "^16.0.0",