becaliang/Music_Generation_Project
0
1from __future__ import print_function2 3import sys4import numpy as np5from music21 import midi, note, stream, tempo, instrument6from fractions import Fraction7 8import lstm9from grammar import unparse_grammar10from preprocess import get_multi_track_musical_data, get_multi_track_corpus_data11from qa import clean_up_notes, prune_grammar, prune_notes12 13# ---------------- Utility Functions ---------------- #14 15def sample_from_distribution(prob_dist, temperature=1.0):16 prob_dist = np.asarray(prob_dist).astype("float64")17 prob_dist = np.log(prob_dist + 1e-9) / temperature18 prob_dist = np.exp(prob_dist) / np.sum(np.exp(prob_dist))19 return np.argmax(np.random.multinomial(1, prob_dist, 1))20 21def predict_token(model, sequence_input, idx_to_token, temperature):22 prediction = model.predict(sequence_input, verbose=0)[0]23 index = sample_from_distribution(prediction, temperature)24 return idx_to_token[index]25 26def generate_abstract_sequence(model, corpus, grammar_data, vocab, token_to_idx, idx_to_token,27 seq_len, diversity, max_duration=32.0, max_tries=1000):28 if len(corpus) < seq_len:29 print("⚠️ Not enough tokens in corpus to generate a sequence.")30 return ""31 32 start = np.random.randint(0, len(corpus) - seq_len)33 sequence = corpus[start:start + seq_len]34 output = []35 total_duration = 0.036 37 while total_duration <= max_duration:38 x_input = np.zeros((1, seq_len, len(vocab)))39 for t, token in enumerate(sequence):40 if token in token_to_idx:41 x_input[0, t, token_to_idx[token]] = 1.042 43 next_token = predict_token(model, x_input, idx_to_token, diversity)44 45 # Filter out invalid tokens46 attempt = 047 while (next_token.startswith("R") or "," not in next_token) and attempt < max_tries:48 next_token = predict_token(model, x_input, idx_to_token, diversity)49 attempt += 150 if attempt >= max_tries:51 next_token = np.random.choice(grammar_data).split(" ")[0]52 53 try:54 duration_val = float(Fraction(next_token.split(",")[1]))55 except Exception as e:56 print(f"⚠️ Skipping token due to error: {e}")57 continue58 59 sequence = sequence[1:] + [next_token]60 output.append(next_token)61 total_duration += duration_val62 63 return " ".join(output)64 65# ---------------- Main Music Generator ---------------- #66 67def generate_music(data_path, output_path, num_epochs=128):68 SEQ_LEN = 2069 TEMP = 1.0 # Increased for more creative output70 BPM = 9071 MAX_DURATION = 32.0 # Increased length of generated music72 73 multi_chords, multi_grammar_sequences = get_multi_track_musical_data(data_path)74 multi_corpus, vocab, token_to_idx, idx_to_token = get_multi_track_corpus_data(multi_grammar_sequences)75 76 if len(vocab) == 0:77 raise ValueError("❌ No vocabulary tokens found. Check your input MIDI file.")78 79 print(f"🧠 Training model with vocab size: {len(vocab)} tokens")80 model = lstm.build_multi_instrument_model(list(multi_corpus.values()), token_to_idx, SEQ_LEN, num_epochs)81 print("✅ Model training completed.")82 83 generated_score = stream.Score()84 generated_score.insert(0.0, tempo.MetronomeMark(number=BPM))85 86 for track_name, chords in multi_chords.items():87 part = stream.Part()88 part.id = str(track_name)89 try:90 part.append(instrument.fromString(str(track_name)))91 except Exception:92 part.append(instrument.Piano())93 94 grammar_sequences = multi_grammar_sequences.get(track_name, [])95 corpus = multi_corpus.get(track_name, [])96 curr_offset = 0.097 98 if isinstance(chords, dict):99 items = sorted(chords.items())100 elif isinstance(chords, list):101 items = list(enumerate(chords))102 else:103 print(f"⚠️ Skipped track '{track_name}': chords format not supported.")104 continue105 106 for measure_idx, measure_chords in items:107 chord_voice = stream.Voice()108 109 if not isinstance(measure_chords, (list, tuple)):110 measure_chords = [measure_chords]111 112 for ch in measure_chords:113 chord_voice.insert(ch.offset % 4, ch)114 115 grammar = generate_abstract_sequence(116 model=model,117 corpus=corpus,118 grammar_data=grammar_sequences,119 vocab=vocab,120 token_to_idx=token_to_idx,121 idx_to_token=idx_to_token,122 seq_len=SEQ_LEN,123 diversity=TEMP,124 max_duration=MAX_DURATION125 )126 127 if not grammar:128 print(f"⚠️ Empty grammar for {track_name}, measure {measure_idx}.")129 continue130 131 grammar = prune_grammar(grammar.replace(" A", " C").replace(" X", " C"))132 133 try:134 notes = unparse_grammar(grammar, chord_voice)135 notes = prune_notes(notes)136 notes = clean_up_notes(notes)137 138 for n in notes:139 part.insert(curr_offset + n.offset, n)140 for ch in chord_voice:141 part.insert(curr_offset + ch.offset, ch)142 except Exception as e:143 print(f"⚠️ Skipped measure {measure_idx} due to grammar error: {e}")144 145 curr_offset += 4.0146 147 generated_score.append(part)148 149 mf = midi.translate.streamToMidiFile(generated_score)150 151 print(f"# Tracks in MIDI: {len(mf.tracks)}")152 if len(generated_score.parts) == 0 or all(len(p.flat.notes) == 0 for p in generated_score.parts):153 raise ValueError("❌ Generated score is empty. No notes were added.")154 155 mf.open(output_path, "wb")156 mf.write()157 mf.close()158 print(f"Generated MIDI saved to: {output_path}")159 160 161# ---------------- CLI Entrypoint ---------------- #162 163def main(argv):164 try:165 num_epochs = int(argv[1])166 midi_file = argv[2] if len(argv) > 2 else "midi/Demon_1.MID"167 output_file = argv[3] if len(argv) > 3 else f"midi/generated_{num_epochs}_epochs.mid"168 except Exception:169 print("⚠️ Usage: python generator.py <epochs> <input.mid> <output.mid>")170 num_epochs = 128171 midi_file = "midi/Demon_1.MID"172 output_file = f"midi/generated_default_{num_epochs}_epochs.mid"173 174 generate_music(midi_file, output_file, num_epochs)175 176if __name__ == "__main__":177 main(sys.argv)