Add complete MIDI search and synthesis pipeline
Backend (MVP): - Express API with /search and /fetch endpoints - BitMidi and Dongrays search adapters with scoring heuristics - CORS proxy with disk caching and validation - Quality assessment and file size limits Frontend Integration: - Switch to @tonejs/midi parser for reliable MIDI parsing - MIDIService for backend API communication - MotifEngine updated to try real MIDI, fallback to synthetic - Enhanced error handling and logging Development: - Concurrent frontend/backend npm scripts - Basic project documentation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+36
-5
@@ -1,5 +1,7 @@
|
||||
import type { NoteEvent, StructuralFeatures, MotifConfig, SynthLayer } from '../types';
|
||||
import { MIDIProcessor } from '../midi/MIDIProcessor';
|
||||
import { MIDIParser } from '../midi/MIDIParser';
|
||||
import { MIDIService } from '../services/MIDIService';
|
||||
import { RoleMapper } from './RoleMapper';
|
||||
import { SynthesisEngine } from '../synthesis/SynthesisEngine';
|
||||
|
||||
@@ -7,6 +9,7 @@ export class MotifEngine {
|
||||
private audioContext: AudioContext | null = null;
|
||||
private config: MotifConfig;
|
||||
private midiProcessor: MIDIProcessor;
|
||||
private midiService: MIDIService;
|
||||
private roleMapper: RoleMapper;
|
||||
private synthesisEngine: SynthesisEngine | null = null;
|
||||
private currentFeatures: StructuralFeatures | null = null;
|
||||
@@ -20,15 +23,43 @@ export class MotifEngine {
|
||||
};
|
||||
|
||||
this.midiProcessor = new MIDIProcessor();
|
||||
this.midiService = new MIDIService();
|
||||
this.roleMapper = new RoleMapper();
|
||||
}
|
||||
|
||||
async generateFromSong(songName: string): Promise<void> {
|
||||
// For now, generate synthetic structure based on song name
|
||||
// TODO: Implement MIDI search and fetching
|
||||
const mockEvents = this.generateSyntheticMIDI(songName);
|
||||
const features = this.midiProcessor.extractFeatures(mockEvents);
|
||||
const roleAssignments = this.roleMapper.assignRoles(features, mockEvents);
|
||||
let events: NoteEvent[];
|
||||
|
||||
// Try to find real MIDI first
|
||||
try {
|
||||
console.log(`Searching for MIDI: ${songName}`);
|
||||
const searchResults = await this.midiService.search(songName);
|
||||
|
||||
if (searchResults.length > 0) {
|
||||
// Try to fetch the best result
|
||||
const bestResult = searchResults[0];
|
||||
console.log(`Attempting to fetch: ${bestResult.title} (${bestResult.confidence})`);
|
||||
|
||||
const midiBuffer = await this.midiService.fetchMIDI(bestResult.midiUrl);
|
||||
|
||||
if (midiBuffer) {
|
||||
// Parse real MIDI
|
||||
events = MIDIParser.parseMIDI(midiBuffer);
|
||||
console.log(`Successfully parsed MIDI with ${events.length} events`);
|
||||
} else {
|
||||
throw new Error('Failed to fetch MIDI');
|
||||
}
|
||||
} else {
|
||||
throw new Error('No MIDI results found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`MIDI search/fetch failed: ${error}. Falling back to synthetic.`);
|
||||
events = this.generateSyntheticMIDI(songName);
|
||||
}
|
||||
|
||||
// Process events into structure
|
||||
const features = this.midiProcessor.extractFeatures(events);
|
||||
const roleAssignments = this.roleMapper.assignRoles(features, events);
|
||||
|
||||
this.currentFeatures = features;
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Midi } from '@tonejs/midi';
|
||||
import type { NoteEvent } from '../types';
|
||||
|
||||
export class MIDIParser {
|
||||
static parseMIDI(arrayBuffer: ArrayBuffer): NoteEvent[] {
|
||||
try {
|
||||
const midi = new Midi(arrayBuffer);
|
||||
const events: NoteEvent[] = [];
|
||||
|
||||
midi.tracks.forEach((track, trackIndex) => {
|
||||
track.notes.forEach(note => {
|
||||
events.push({
|
||||
time: note.time,
|
||||
duration: note.duration,
|
||||
pitch: note.midi,
|
||||
velocity: note.velocity,
|
||||
track: trackIndex
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Sort events by time
|
||||
events.sort((a, b) => a.time - b.time);
|
||||
|
||||
console.log(`Parsed ${events.length} notes from ${midi.tracks.length} tracks`);
|
||||
return events;
|
||||
} catch (error) {
|
||||
console.error('MIDI parsing error:', error);
|
||||
throw new Error(`Failed to parse MIDI: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
static getMIDIInfo(arrayBuffer: ArrayBuffer): {
|
||||
duration: number;
|
||||
trackCount: number;
|
||||
noteCount: number;
|
||||
tempo: number;
|
||||
} {
|
||||
try {
|
||||
const midi = new Midi(arrayBuffer);
|
||||
|
||||
return {
|
||||
duration: midi.duration,
|
||||
trackCount: midi.tracks.length,
|
||||
noteCount: midi.tracks.reduce((total, track) => total + track.notes.length, 0),
|
||||
tempo: midi.header.tempos[0]?.bpm || 120
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('MIDI info extraction error:', error);
|
||||
return {
|
||||
duration: 0,
|
||||
trackCount: 0,
|
||||
noteCount: 0,
|
||||
tempo: 120
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
interface MIDISearchResult {
|
||||
id: string;
|
||||
title: string;
|
||||
source: string;
|
||||
pageUrl: string;
|
||||
midiUrl: string;
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
interface MIDISearchResponse {
|
||||
results: MIDISearchResult[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export class MIDIService {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(baseUrl = 'http://localhost:3001') {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
async search(query: string): Promise<MIDISearchResult[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/midi/search?q=${encodeURIComponent(query)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Search failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: MIDISearchResponse = await response.json();
|
||||
return data.results;
|
||||
} catch (error) {
|
||||
console.error('MIDI search error:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async fetchMIDI(url: string): Promise<ArrayBuffer | null> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/midi/fetch?u=${encodeURIComponent(url)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Fetch failed: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.arrayBuffer();
|
||||
} catch (error) {
|
||||
console.error('MIDI fetch error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async checkHealth(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/health`);
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user