Initial Motif project setup
- Web Audio + TypeScript procedural synthesis engine - Role-based MIDI structure extraction - Vite build tooling and development setup - Core architecture: Engine, RoleMapper, SynthesisEngine - Minimal UI for testing synthesis generation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import type { NoteEvent, StructuralFeatures, MotifConfig, SynthLayer } from '../types';
|
||||
import { MIDIProcessor } from '../midi/MIDIProcessor';
|
||||
import { RoleMapper } from './RoleMapper';
|
||||
import { SynthesisEngine } from '../synthesis/SynthesisEngine';
|
||||
|
||||
export class MotifEngine {
|
||||
private audioContext: AudioContext | null = null;
|
||||
private config: MotifConfig;
|
||||
private midiProcessor: MIDIProcessor;
|
||||
private roleMapper: RoleMapper;
|
||||
private synthesisEngine: SynthesisEngine | null = null;
|
||||
private currentFeatures: StructuralFeatures | null = null;
|
||||
|
||||
constructor() {
|
||||
this.config = {
|
||||
lookaheadTime: 0.1,
|
||||
scheduleInterval: 25,
|
||||
fadeTime: 0.05,
|
||||
maxOscillators: 8
|
||||
};
|
||||
|
||||
this.midiProcessor = new MIDIProcessor();
|
||||
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);
|
||||
|
||||
this.currentFeatures = features;
|
||||
|
||||
// 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 || !this.currentFeatures) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { NoteEvent, StructuralFeatures, RoleAssignment, Role } from '../types';
|
||||
|
||||
export class RoleMapper {
|
||||
assignRoles(features: StructuralFeatures, events: NoteEvent[]): RoleAssignment[] {
|
||||
const assignments: RoleAssignment[] = [];
|
||||
const trackEvents = this.groupEventsByTrack(events);
|
||||
|
||||
for (const [trackId, trackNotes] of trackEvents) {
|
||||
const role = this.determineRole(trackNotes, features);
|
||||
const confidence = this.calculateConfidence(trackNotes, role);
|
||||
|
||||
assignments.push({
|
||||
role,
|
||||
sourceTrack: trackId,
|
||||
events: trackNotes,
|
||||
confidence
|
||||
});
|
||||
}
|
||||
|
||||
return assignments;
|
||||
}
|
||||
|
||||
private groupEventsByTrack(events: NoteEvent[]): Map<number, NoteEvent[]> {
|
||||
const tracks = new Map<number, NoteEvent[]>();
|
||||
|
||||
for (const event of events) {
|
||||
if (!tracks.has(event.track)) {
|
||||
tracks.set(event.track, []);
|
||||
}
|
||||
tracks.get(event.track)!.push(event);
|
||||
}
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
private determineRole(events: NoteEvent[], features: StructuralFeatures): Role {
|
||||
if (events.length === 0) return 'texture';
|
||||
|
||||
const avgPitch = events.reduce((sum, e) => sum + e.pitch, 0) / events.length;
|
||||
const avgDuration = events.reduce((sum, e) => sum + e.duration, 0) / events.length;
|
||||
const avgVelocity = events.reduce((sum, e) => sum + e.velocity, 0) / events.length;
|
||||
|
||||
// Bass: low register, rhythmic
|
||||
if (avgPitch < 48 && avgDuration < 1.0) {
|
||||
return 'bass';
|
||||
}
|
||||
|
||||
// Drone: sustained notes
|
||||
if (avgDuration > 2.0) {
|
||||
return 'drone';
|
||||
}
|
||||
|
||||
// Ostinato: repetitive short notes
|
||||
if (avgDuration < 0.5 && events.length > 10) {
|
||||
return 'ostinato';
|
||||
}
|
||||
|
||||
// Accents: high velocity peaks
|
||||
if (avgVelocity > 0.8) {
|
||||
return 'accents';
|
||||
}
|
||||
|
||||
return 'texture';
|
||||
}
|
||||
|
||||
private calculateConfidence(events: NoteEvent[], role: Role): number {
|
||||
// Simple confidence calculation based on how well events fit the role
|
||||
// In a real implementation, this would be more sophisticated
|
||||
return 0.7 + Math.random() * 0.3;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user