Audio2Chromatic / app.py
szili2011's picture
Update app.py
b8fc09b verified
import gradio as gr
import numpy as np
import librosa
from pydub import AudioSegment
import os
import random
import pyrubberband as rb
import soundfile as sf
from pedalboard import Pedalboard, Reverb
# --- Create dummy files for examples. This runs on server startup. ---
# This is the key fix: This code is now in the global scope.
os.makedirs("generated_music", exist_ok=True)
if not os.path.exists("example_vocal.wav"):
sf.write("example_vocal.wav", np.random.randn(44100 * 2), 44100)
if not os.path.exists("example_drums.wav"):
sf.write("example_drums.wav", np.random.randn(44100 * 2), 44100)
if not os.path.exists("example_synth.wav"):
sf.write("example_synth.wav", np.random.randn(44100 * 2), 44100)
# --- Musical Sequence Generation ---
def generate_musical_sequence(mode="Scale", scale_name="Major", root_note="C4", num_notes=50):
"""
Generates a sequence of MIDI notes for scales or arpeggios.
"""
try:
current_midi = librosa.note_to_midi(root_note)
except Exception as e:
raise ValueError(f"Invalid root note. Use scientific notation like 'C4'. Error: {e}")
intervals = {
"Chromatic": [1], "Major": [2, 2, 1, 2, 2, 2, 1], "Natural Minor": [2, 1, 2, 2, 1, 2, 2],
"Harmonic Minor": [2, 1, 2, 2, 1, 3, 1], "Pentatonic Major": [2, 2, 3, 2, 3]
}
scale_intervals = intervals.get(scale_name, intervals["Chromatic"])
scale_notes = [current_midi]
interval_index = 0
while len(scale_notes) < num_notes:
current_midi += scale_intervals[interval_index % len(scale_intervals)]
scale_notes.append(current_midi)
interval_index += 1
if mode == "Scale":
return scale_notes[:num_notes]
if mode == "Arpeggio":
arpeggio_tones = [scale_notes[i] for i in [0, 2, 4]]
if not arpeggio_tones: return []
sequence = []
octave_offset = 0
tone_index = 0
while len(sequence) < num_notes:
note = arpeggio_tones[tone_index % len(arpeggio_tones)] + (12 * octave_offset)
sequence.append(note)
tone_index += 1
if tone_index % len(arpeggio_tones) == 0:
octave_offset += 1
return sequence
# --- Core Audio Processing ---
def get_random_segment(y, sr, min_len_ms=100, max_len_ms=1000):
"""Extracts a single random segment and its energy."""
seg_len_samples = int(random.uniform(min_len_ms, max_len_ms) / 1000 * sr)
if seg_len_samples > len(y):
seg_len_samples = len(y)
start_sample = random.randint(0, len(y) - seg_len_samples)
end_sample = start_sample + seg_len_samples
segment_y = y[start_sample:end_sample]
rms = librosa.feature.rms(y=segment_y).mean()
return segment_y, rms
def audio_orchestrator(
audio_path,
mode, scale_name, root_note, num_notes,
note_duration_s, silence_s,
reverb_mix, reverb_room_size,
progress=gr.Progress()
):
"""Main function to orchestrate the entire audio transformation."""
if not audio_path:
raise gr.Error("Please upload a source audio file first.")
try:
progress(0, desc="Loading Audio...")
y, sr = librosa.load(audio_path, sr=None, mono=True)
except Exception as e:
raise gr.Error(f"Could not read the audio file. Error: {e}")
if len(y) / sr < 0.1:
raise gr.Error("Source audio is too short. Please use a file that is at least 0.1 seconds long.")
progress(0.1, desc=f"Generating {mode}...")
target_midi_notes = generate_musical_sequence(mode, scale_name, root_note, num_notes)
final_y = np.array([], dtype=np.float32)
silence_samples = np.zeros(int(silence_s * sr), dtype=np.float32)
source_segments = [get_random_segment(y, sr) for _ in range(num_notes)]
all_rms = [rms for _, rms in source_segments if rms > 0]
max_rms = max(all_rms) if all_rms else 1.0
for i, (segment_y, rms) in enumerate(source_segments):
target_note = target_midi_notes[i]
note_name = librosa.midi_to_note(target_note)
progress(0.2 + (i / num_notes) * 0.7, desc=f"Processing Note {i+1}/{num_notes}: {note_name}")
source_duration = len(segment_y) / sr
stretch_rate = source_duration / note_duration_s if note_duration_s > 0 else 1
stretched_y = rb.time_stretch(segment_y, sr, stretch_rate)
source_pitch_hz = librosa.note_to_hz('C4')
target_pitch_hz = librosa.midi_to_hz(target_note)
pitch_shift_ratio = target_pitch_hz / source_pitch_hz
shifted_y = rb.pitch_shift(stretched_y, sr, pitch_shift_ratio)
volume_factor = (rms / max_rms) * 0.9 + 0.1
shifted_y *= volume_factor
final_y = np.concatenate((final_y, shifted_y, silence_samples))
progress(0.95, desc="Applying final effects...")
if reverb_mix > 0:
board = Pedalboard([Reverb(room_size=reverb_room_size, wet_level=reverb_mix, dry_level=1.0 - reverb_mix)])
final_y = board(final_y, sr)
output_dir = "generated_music"
os.makedirs(output_dir, exist_ok=True)
output_filename = f"{mode.lower()}_{scale_name.lower().replace(' ', '_')}_{root_note}.wav"
output_path = os.path.join(output_dir, output_filename)
sf.write(output_path, final_y, sr)
progress(1, desc="Done!")
return output_path
# --- Gradio User Interface ---
with gr.Blocks(theme=gr.themes.Base(primary_hue="teal", secondary_hue="orange")) as demo:
gr.Markdown(
"""
# 🎹 Audio Orchestrator Pro 🎵
### Turn any sound into a unique musical composition.
Upload an audio file, configure the musical, rhythmic, and effects parameters, and generate a new piece of music.
"""
)
with gr.Row():
with gr.Column(scale=1):
audio_input = gr.Audio(type="filepath", label="1. Upload Your Source Audio")
with gr.Accordion("Musical Parameters", open=True):
mode = gr.Radio(["Scale", "Arpeggio"], label="Generation Mode", value="Arpeggio")
scale_name = gr.Dropdown(["Major", "Natural Minor", "Harmonic Minor", "Pentatonic Major", "Chromatic"], label="Scale", value="Major")
root_note_choices = librosa.midi_to_note(list(range(36, 73))).tolist()
root_note = gr.Dropdown(root_note_choices, label="Root Note", value="C4")
num_notes = gr.Slider(10, 200, value=70, step=1, label="Number of Notes")
with gr.Accordion("Rhythmic Parameters", open=False):
note_duration_s = gr.Slider(0.05, 1.0, value=0.2, step=0.01, label="Note Duration (seconds)")
silence_s = gr.Slider(0.0, 0.5, value=0.05, step=0.01, label="Silence Between Notes (seconds)")
with gr.Accordion("Effects Rack", open=False):
gr.Markdown("**Reverb**")
reverb_mix = gr.Slider(0, 1, value=0.3, label="Wet/Dry Mix")
reverb_room_size = gr.Slider(0, 1, value=0.6, label="Room Size")
process_button = gr.Button("✨ Generate Composition", variant="primary")
with gr.Column(scale=2):
gr.Markdown("### Your Generated Music")
audio_output = gr.Audio(type="filepath", label="Output")
process_button.click(
fn=audio_orchestrator,
inputs=[
audio_input, mode, scale_name, root_note, num_notes,
note_duration_s, silence_s, reverb_mix, reverb_room_size
],
outputs=[audio_output]
)
gr.Examples(
# FIX IS HERE: Using simple filenames because the files are now in the root directory.
examples=[
["example_vocal.wav", "Arpeggio", "Natural Minor", "A3", 80, 0.15, 0.1, 0.4, 0.7],
["example_drums.wav", "Scale", "Pentatonic Major", "C3", 50, 0.25, 0.0, 0.2, 0.8],
["example_synth.wav", "Arpeggio", "Harmonic Minor", "E4", 120, 0.1, 0.02, 0.5, 0.9],
],
inputs=[
audio_input, mode, scale_name, root_note, num_notes,
note_duration_s, silence_s, reverb_mix, reverb_room_size
],
outputs=[audio_output],
fn=audio_orchestrator,
cache_examples=True
)
if __name__ == "__main__":
demo.launch(debug=True)