import os import sys import json import random import math import struct import wave import tempfile import hashlib from collections import defaultdict from dataclasses import dataclass, field from typing import List, Dict, Tuple, Optional from enum import Enum # ============================================================================= # MUSIC THEORY CONSTANTS # ============================================================================= NOTES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] NOTE_TO_MIDI = {note + str(octave): i + 12 * (octave - 1) for octave in range(0, 9) for i, note in enumerate(NOTES)} MIDI_TO_NOTE = {v: k for k, v in NOTE_TO_MIDI.items()} MAJOR_SCALE = [0, 2, 4, 5, 7, 9, 11] MINOR_SCALE = [0, 2, 3, 5, 7, 8, 10] CHORD_TYPES = { 'major': [0, 4, 7], 'minor': [0, 3, 7], 'diminished': [0, 3, 6], 'augmented': [0, 4, 8], 'major7': [0, 4, 7, 11], 'minor7': [0, 3, 7, 10], 'dominant7': [0, 4, 7, 10], } # ============================================================================= # DATA CLASSES # ============================================================================= @dataclass class Note: """Represents a musical note""" pitch: int # MIDI note number duration: float # in beats velocity: int = 64 # 0-127 start_time: float = 0.0 def get_frequency(self) -> float: """Convert MIDI note to frequency in Hz""" return 440.0 * (2 ** ((self.pitch - 69) / 12)) def get_note_name(self) -> str: """Get note name (e.g., C4)""" return MIDI_TO_NOTE.get(self.pitch, f"Note{self.pitch}") @dataclass class Chord: """Represents a chord""" root: int # MIDI note number of root chord_type: str # e.g., 'major', 'minor' duration: float inversion: int = 0 # 0 = root position def get_notes(self) -> List[int]: """Get all notes in the chord""" intervals = CHORD_TYPES[self.chord_type] notes = [(self.root + interval) % 12 for interval in intervals] return notes @dataclass class Track: """Represents a musical track with notes""" notes: List[Note] = field(default_factory=list) instrument: str = "piano" tempo: float = 120.0 # BPM def add_note(self, note: Note): self.notes.append(note) self.notes.sort(key=lambda n: n.start_time) def get_duration(self) -> float: if not self.notes: return 0 return max(n.start_time + n.duration for n in self.notes) @dataclass class Composition: """Complete musical composition""" tracks: Dict[str, Track] = field(default_factory=dict) title: str = "Untitled" tempo: float = 120.0 def add_track(self, name: str, track: Track): self.tracks[name] = track def total_duration(self) -> float: return max(t.get_duration() for t in self.tracks.values()) if self.tracks else 0 # ============================================================================= # MARKOV CHAIN MELODY GENERATOR # ============================================================================= class MarkovMelodyGenerator: """Generates melodies using Markov chains""" def __init__(self, scale: List[int] = MAJOR_SCALE, order: int = 2): self.scale = scale self.order = order self.transitions = defaultdict(lambda: defaultdict(float)) def train(self, sequences: List[List[int]]): """Train on sequences of notes (MIDI numbers)""" for seq in sequences: for i in range(len(seq) - self.order): context = tuple(seq[i:i + self.order]) next_note = seq[i + self.order] self.transitions[context][next_note] += 1 # Normalize to probabilities for context in self.transitions: total = sum(self.transitions[context].values()) for note in self.transitions[context]: self.transitions[context][note] /= total def generate(self, length: int = 16, seed: Optional[List[int]] = None) -> List[int]: """Generate a new melody sequence""" if seed is None: seed = [random.choice(list(self.transitions.keys())[0][0])] sequence = list(seed) for _ in range(length - len(seed)): context = tuple(sequence[-self.order:]) if context in self.transitions: # Weighted random choice notes = list(self.transitions[context].keys()) probs = list(self.transitions[context].values()) next_note = random.choices(notes, weights=probs, k=1)[0] else: # Fallback: random note from scale next_note = random.choice(self.scale) sequence.append(next_note) return sequence # ============================================================================= # CHORD PROGRESSION GENERATOR # ============================================================================= class ChordProgressionGenerator: """Generates chord progressions based on music theory rules""" PROGRESSIONS = { 'pop': ['I', 'V', 'vi', 'IV'], 'jazz': ['ii', 'V', 'I', 'vi'], 'blues': ['I', 'IV', 'I', 'V', 'IV', 'I'], 'emotional': ['vi', 'IV', 'I', 'V'], 'epic': ['i', 'VI', 'III', 'VII'], 'ambient': ['I', 'vi', 'IV', 'V'], } def __init__(self, key: str = 'C', mode: str = 'major'): self.key = key self.mode = mode self.key_index = NOTES.index(key) self.scale = MAJOR_SCALE if mode == 'major' else MINOR_SCALE def get_chord_root(self, roman_numeral: str) -> int: """Convert Roman numeral to MIDI root note""" roman = roman_numeral.upper() if roman == 'I': degree = 0 elif roman == 'II': degree = 1 elif roman == 'III': degree = 2 elif roman == 'IV': degree = 3 elif roman == 'V': degree = 4 elif roman == 'VI': degree = 5 elif roman == 'VII': degree = 6 else: degree = 0 root_note = self.scale[degree % len(self.scale)] return 60 + self.key_index + root_note def generate_progression(self, style: str = 'pop', bars: int = 4) -> List[Chord]: """Generate a chord progression""" progression = self.PROGRESSIONS.get(style, self.PROGRESSIONS['pop']) chords = [] for i, roman in enumerate(progression * (bars // len(progression) + 1)): if i >= bars: break root = self.get_chord_root(roman) chord_type = 'minor' if roman.islower() else 'major' chords.append(Chord(root=root, chord_type=chord_type, duration=1.0)) return chords[:bars] # ============================================================================= # AUDIO SYNTHESIZER # ============================================================================= class SimpleSynth: """Simple synthesizer for generating audio from MIDI data""" def __init__(self, sample_rate: int = 44100): self.sample_rate = sample_rate def generate_sine_wave(self, frequency: float, duration: float, amplitude: float = 0.3) -> bytes: """Generate a sine wave""" samples = int(duration * self.sample_rate) wave_data = bytearray() for i in range(samples): t = i / self.sample_rate value = amplitude * math.sin(2 * math.pi * frequency * t) pcm_value = int(value * 32767) wave_data.extend(struct.pack(' bytes: """Generate a saw wave""" samples = int(duration * self.sample_rate) wave_data = bytearray() for i in range(samples): t = i / self.sample_rate value = amplitude * (2 * (t * frequency % 1) - 1) pcm_value = int(value * 32767) wave_data.extend(struct.pack(' bytes: """Apply ADSR envelope to audio""" samples = len(audio_data) // 2 envelope = bytearray() attack_samples = int(attack * self.sample_rate) decay_samples = int(decay * self.sample_rate) release_samples = int(release * self.sample_rate) for i in range(samples): pcm_value = struct.unpack(' Track: """Generate a melody track""" track = Track(instrument='piano', tempo=self.tempo) melody = self.markov_gen.generate(length=duration_bars * 4) current_time = 0.0 for i, pitch in enumerate(melody): if random.random() < 0.3: continue # Skip some notes for rests duration = random.choice([0.25, 0.5, 0.5, 1.0]) velocity = random.randint(60, 100) track.add_note(Note( pitch=pitch, duration=duration, velocity=velocity, start_time=current_time )) current_time += duration return track def generate_chord_track(self, duration_bars: int = 8, style: str = 'pop') -> Track: """Generate a chord accompaniment track""" track = Track(instrument='strings', tempo=self.tempo) progressions = self.chord_gen.generate_progression( style=style, bars=duration_bars) current_time = 0.0 for chord in progressions: # Play chord as arpeggio notes = chord.get_notes() for j, note_pitch in enumerate(notes): track.add_note(Note( pitch=note_pitch + 48, duration=0.5, velocity=random.randint(40, 70), start_time=current_time + j * 0.25 )) current_time += chord.duration return track def generate_bass_track(self, duration_bars: int = 8) -> Track: """Generate a bass line""" track = Track(instrument='bass', tempo=self.tempo) progressions = self.chord_gen.generate_progression( style='pop', bars=duration_bars) current_time = 0.0 for chord in progressions: track.add_note(Note( pitch=chord.root - 12, duration=chord.duration, velocity=random.randint(70, 90), start_time=current_time )) current_time += chord.duration return track def generate_composition(self, melody_style: str = 'melodic', chord_style: str = 'pop', duration_bars: int = 8) -> Composition: """Generate a complete composition""" composition = Composition( title=f"AI Generated - {chord_style}", tempo=self.tempo ) melody_track = self.generate_melody_track(duration_bars, melody_style) chord_track = self.generate_chord_track(duration_bars, chord_style) bass_track = self.generate_bass_track(duration_bars) composition.add_track('melody', melody_track) composition.add_track('chords', chord_track) composition.add_track('bass', bass_track) return composition def render_to_audio(self, composition: Composition, output_path: str) -> str: """Render composition to audio file""" total_duration = composition.total_duration() samples = int(total_duration * 60 / self.tempo * self.synth.sample_rate) master_audio = bytearray(samples * 2) for i in range(samples): struct.pack_into(' Composition: """Generate a complete song""" if name is None: name = f"Song_{len(self.compositions) + 1}" composition = self.generator.generate_composition( melody_style=style, chord_style=style, duration_bars=duration_bars ) composition.title = name self.compositions[name] = composition return composition def export_to_midi(self, composition: Composition, filename: str) -> str: """Export composition to MIDI file""" filepath = os.path.join(self.output_dir, f"{filename}.mid") with open(filepath, 'wb') as f: # Header chunk f.write(b'MThd') f.write(struct.pack('>I', 6)) f.write(struct.pack('>H', 1)) f.write(struct.pack('>H', len(composition.tracks))) f.write(struct.pack('>H', 480)) # Track chunks for track_name, track in composition.tracks.items(): events = [] # Tempo event tempo_microsec = int(60000000 / track.tempo) events.append((0, 0xFF, 0x51, struct.pack('>I', tempo_microsec)[1:])) # Note events for note in track.notes: events.append((int(note.start_time * 480), 0x90, note.pitch, note.velocity)) events.append((int((note.start_time + note.duration) * 480), 0x80, note.pitch, 0)) events.sort(key=lambda x: x[0]) track_data = bytearray() for delta, status, data1, data2 in events: while delta >= 0x80: track_data.append((delta & 0x7F) | 0x80) delta >>= 7 track_data.append(delta) track_data.append(status) track_data.append(data1) if status != 0xFF: track_data.append(data2) track_data.extend([0x00, 0xFF, 0x2F, 0x00]) f.write(b'MTrk') f.write(struct.pack('>I', len(track_data))) f.write(track_data) return filepath def export_to_wav(self, composition: Composition, filename: str) -> str: """Export composition to WAV audio file""" filepath = os.path.join(self.output_dir, f"{filename}.wav") return self.generator.render_to_audio(composition, filepath) def print_composition(self, composition: Composition): """Print composition details""" print(f"\n{'='*60}") print(f"Composition: {composition.title}") print(f"Tempo: {composition.tempo} BPM") print(f"Duration: {composition.total_duration():.2f} beats") print(f"{'='*60}") for track_name, track in composition.tracks.items(): print(f"\nTrack: {track_name} ({track.instrument})") print(f" Notes: {len(track.notes)}") if track.notes: print(f" First note: {track.notes[0].get_note_name()}") print(f" Last note: {track.notes[-1].get_note_name()}") # ============================================================================= # DEMONSTRATION # ============================================================================= print("=" * 60) print("AI MUSIC GENERATION APP") print("=" * 60) app = MusicGenerationApp() styles = ['pop', 'jazz', 'emotional', 'ambient'] for style in styles: print(f"\nGenerating {style} style composition...") composition = app.generate_song(style=style, duration_bars=8, name=style) app.print_composition(composition) first_composition = app.compositions['pop'] print("\n" + "=" * 60) print("EXPORTING FILES") print("=" * 60) midi_file = app.export_to_midi(first_composition, 'ai_generated_song') print(f"MIDI file saved: {midi_file}") wav_file = app.export_to_wav(first_composition, 'ai_generated_song') print(f"WAV file saved: {wav_file}") print(f"\nMIDI file size: {os.path.getsize(midi_file)} bytes") print(f"WAV file size: {os.path.getsize(wav_file)} bytes") print("\n" + "=" * 60) print("MUSIC GENERATION COMPLETE!") print("=" * 60) print(f"\nOutput directory: {app.output_dir}") print("Files created:") for f in os.listdir(app.output_dir): filepath = os.path.join(app.output_dir, f) print(f" - {f} ({os.path.getsize(filepath)} bytes)")