Files
motif/src/core/MotifEngine.ts
T
b1rdmania 3031614914 Add progress bar and seek controls to Motif player
- Add interactive progress bar with visual fill indicator
- Show current time and total duration
- Enable seeking/scrubbing through the synthesis
- Improve Motif player UX with cleaner layout
- Add progress tracking that updates 10x per second
- Format time display as MM:SS
- Progress bar appears when playback starts
- Seeking updates event indices for smooth playback

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 19:17:02 +00:00

218 lines
6.5 KiB
TypeScript

import type { NoteEvent, MotifConfig } 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';
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;
constructor() {
this.config = {
lookaheadTime: 0.1,
scheduleInterval: 25,
fadeTime: 0.05,
maxOscillators: 8
};
this.midiProcessor = new MIDIProcessor();
this.midiService = new MIDIService();
this.roleMapper = new RoleMapper();
}
async generateFromMIDI(events: NoteEvent[], transformMode: 'passthrough' | 'procedural' = 'passthrough'): Promise<void> {
// Initialize audio context and synthesis engine
if (!this.audioContext) {
this.audioContext = new AudioContext();
}
this.synthesisEngine = new SynthesisEngine(this.audioContext, this.config);
if (transformMode === 'passthrough') {
// Direct playback mode - play MIDI as-is without transformations
// Create a single "melody" role assignment with all original events
const passthroughAssignment = [{
role: 'melody' as const,
sourceTrack: 0,
events: events,
chords: [], // No chord processing
confidence: 1.0,
features: {
medianPitch: 60,
pitchRange: 48,
noteDensity: 1.0,
polyphonyRatio: 0.5,
averageDuration: 0.5,
repetitionScore: 0.5,
isMonophonic: false,
hasPhraseContinuity: true,
register: 'mid' as const
}
}];
this.synthesisEngine.setupLayers(passthroughAssignment);
console.log('Motif: Passthrough mode - playing original MIDI patterns');
} else {
// Procedural mode - transform the MIDI with role mapping
const features = this.midiProcessor.extractFeatures(events);
const roleAssignments = this.roleMapper.assignRoles(features, events);
this.synthesisEngine.setupLayers(roleAssignments);
console.log('Motif: Procedural mode - transforming MIDI with role mapping');
}
}
async generateFromSong(songName: string): Promise<void> {
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);
// Initialize audio context and synthesis engine
if (!this.audioContext) {
this.audioContext = new AudioContext();
}
this.synthesisEngine = new SynthesisEngine(this.audioContext, this.config);
this.synthesisEngine.setupLayers(roleAssignments);
}
async play(): Promise<void> {
if (!this.audioContext || !this.synthesisEngine) {
throw new Error('No audio generated yet');
}
if (this.audioContext.state === 'suspended') {
await this.audioContext.resume();
}
this.synthesisEngine.start();
}
stop(): void {
if (this.synthesisEngine) {
this.synthesisEngine.stop();
}
}
setVolume(volume: number): void {
if (this.synthesisEngine) {
this.synthesisEngine.setVolume(volume);
}
}
seek(progress: number): void {
if (this.synthesisEngine) {
this.synthesisEngine.seek(progress);
}
}
getProgress(): number {
if (this.synthesisEngine) {
return this.synthesisEngine.getProgress();
}
return 0;
}
getCurrentTime(): number {
if (this.synthesisEngine) {
return this.synthesisEngine.getCurrentTime();
}
return 0;
}
getDuration(): number {
if (this.synthesisEngine) {
return this.synthesisEngine.getDuration();
}
return 0;
}
private generateSyntheticMIDI(songName: string): NoteEvent[] {
// Generate procedural MIDI based on song name hash
const hash = this.simpleHash(songName);
const events: NoteEvent[] = [];
// Create a simple 4/4 pattern with bass, harmony, and texture
const duration = 32; // 32 seconds
const beatsPerSecond = (120 + (hash % 60)) / 60; // Tempo 120-180 BPM
// Bass pattern (track 0)
for (let beat = 0; beat < duration * beatsPerSecond; beat += 1) {
if (beat % 4 === 0) { // On beat
events.push({
time: beat / beatsPerSecond,
duration: 0.5,
pitch: 36 + (hash % 12), // C2 + random root
velocity: 0.7 + (hash % 3) * 0.1,
track: 0
});
}
}
// Harmonic drone (track 1)
events.push({
time: 0,
duration: duration,
pitch: 48 + ((hash * 3) % 12), // C3 + harmonic interval
velocity: 0.3,
track: 1
});
// Textural elements (track 2)
for (let i = 0; i < 20; i++) {
events.push({
time: (hash * i) % duration,
duration: 0.2 + ((hash * i) % 10) * 0.1,
pitch: 60 + ((hash * i) % 24), // C4 + 2 octaves
velocity: 0.2 + ((hash * i) % 5) * 0.1,
track: 2
});
}
return events.sort((a, b) => a.time - b.time);
}
private simpleHash(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return Math.abs(hash);
}
}