File size: 9,271 Bytes
a257001
 
 
ff77dd0
 
2722bf3
ff77dd0
a257001
 
 
 
 
 
 
 
 
 
 
2722bf3
a257001
 
 
 
 
 
2722bf3
a257001
2722bf3
a257001
 
 
 
 
 
 
 
 
 
 
 
2722bf3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ff77dd0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2722bf3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ff77dd0
 
 
 
 
 
 
 
 
 
 
 
2722bf3
 
ff77dd0
 
 
 
2722bf3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ff77dd0
 
 
 
 
 
 
 
 
 
2722bf3
 
 
 
 
 
 
 
 
 
 
 
 
 
ff77dd0
a257001
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
import gradio as gr
import subprocess
import os
import librosa
import numpy as np
import soundfile as sf
from scipy.stats import pearsonr

def separate_audio(audio_filepath):
    # 1. Setup output directory
    output_base = "output"
    if not os.path.exists(output_base):
        os.makedirs(output_base)
    
    # 2. Run Demucs CLI
    print(f"Starting separation for: {audio_filepath}")
    subprocess.run([
        "python3", "-m", "demucs.separate", 
        "-n", "htdemucs_6s", 
        "-o", output_base, 
        audio_filepath
    ])
    
    # 3. Locate output files
    filename = os.path.splitext(os.path.basename(audio_filepath))[0]
    output_folder = os.path.join(output_base, "htdemucs_6s", filename)
    
    stems = ["vocals.wav", "drums.wav", "bass.wav", "other.wav", "piano.wav", "guitar.wav"]
    result_paths = []
    
    for stem in stems:
        path = os.path.abspath(os.path.join(output_folder, stem))
        if os.path.exists(path):
            result_paths.append(path)
        else:
            print(f"Warning: Could not find {stem} at {path}")
            result_paths.append(None)
            
    return result_paths

def analyze_advanced_metrics(audio_filepath):
    if audio_filepath is None:
        return None, {"error": "No audio file provided"}

    # Load audio
    y, sr = librosa.load(audio_filepath)
    
    # 1. Metronome Generation
    tempo, beat_frames = librosa.beat.beat_track(y=y, sr=sr)
    beat_times = librosa.frames_to_time(beat_frames, sr=sr)
    click_track = librosa.clicks(frames=beat_frames, sr=sr, length=len(y))
    
    output_base = "output"
    if not os.path.exists(output_base):
        os.makedirs(output_base)
    
    click_track_path = os.path.join(output_base, "click_track.wav")
    sf.write(click_track_path, click_track, sr)
    click_track_path = os.path.abspath(click_track_path)
    
    # 2. Structure Analysis
    # Heuristic: use novelty curve to find segment boundaries
    onset_env = librosa.onset.onset_strength(y=y, sr=sr)
    # Using a simple peak picking on the novelty curve for segment boundaries
    # A better way would be using librosa.segment, but let's keep it simple as requested
    # We want 4 to 5 major structural boundaries
    
    # Saliency-based segmentation
    hop_length = 512
    # Compute chroma features
    chroma = librosa.feature.chroma_cqt(y=y, sr=sr, hop_length=hop_length)
    # Use recurrence matrix for segmentation
    rec = librosa.segment.recurrence_matrix(chroma, mode='affinity', sym=True)
    # Compute the lag-similarity matrix
    lag_rec = librosa.segment.recurrence_to_lag(rec)
    
    # Instead of complex librosa.segment which might need more params, 
    # let's use a simpler approach to get 4-5 sections.
    duration = librosa.get_duration(y=y, sr=sr)
    
    # Find 4-5 major structural boundaries using novelty curve
    # Smooth the novelty curve
    novelty = librosa.util.normalize(onset_env)
    # We can use librosa.segment.subsegment or just pick top N peaks far apart
    # For simplicity, let's just divide the song into N chunks if novelty detection is too complex for this prompt
    # BUT the prompt says "Use librosa.segment or a novelty-curve heuristic to determine 4 to 5 major structural boundaries"
    
    boundaries = librosa.segment.agglomerative(chroma, 5) # Get 5 segments
    boundary_times = librosa.frames_to_time(boundaries, sr=sr, hop_length=hop_length)
    
    # Ensure start is 0 and end is duration
    boundary_times = np.unique(np.concatenate(([0.0], boundary_times, [duration])))
    
    structure_json = []
    for i in range(len(boundary_times) - 1):
        structure_json.append({
            "label": f"Section {i+1}",
            "start_time": round(float(boundary_times[i]), 2),
            "end_time": round(float(boundary_times[i+1]), 2)
        })
        
    return click_track_path, structure_json

def detect_key(audio_filepath):
    if audio_filepath is None:
        return {"error": "No audio file provided"}

    # Load audio
    y, sr = librosa.load(audio_filepath)
    
    # Extract chromagram
    chroma = librosa.feature.chroma_cqt(y=y, sr=sr)
    chroma_sum = np.sum(chroma, axis=1)
    
    # Krumhansl-Schmuckler profiles (Temperley)
    major_profile = [6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88]
    minor_profile = [6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17]
    
    notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
    
    results = []
    
    for i in range(12):
        # Rotate profiles to check each key
        shifted_major = np.roll(major_profile, i)
        shifted_minor = np.roll(minor_profile, i)
        
        # Pearson correlation
        corr_major, _ = pearsonr(chroma_sum, shifted_major)
        corr_minor, _ = pearsonr(chroma_sum, shifted_minor)
        
        results.append((corr_major, f"{notes[i]} Major", i))
        results.append((corr_minor, f"{notes[i]} Minor", i))
    
    # Find maximum correlation
    best_corr, best_key, root_idx = max(results, key=lambda x: x[0])
    
    return {
        "key_name": best_key,
        "root_index": int(root_idx)
    }

def extract_chords(audio_filepath):
    if audio_filepath is None:
        return {"error": "No audio file provided"}

    # Load audio
    y, sr = librosa.load(audio_filepath)
    
    # Extract chromagram
    chroma = librosa.feature.chroma_cqt(y=y, sr=sr)
    
    # Define chord templates (12 Major and 12 Minor)
    # C, C#, D, D#, E, F, F#, G, G#, A, A#, B
    maj_template = np.array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0])
    min_template = np.array([1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0])
    
    templates = []
    labels = []
    notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
    
    for i in range(12):
        templates.append(np.roll(maj_template, i))
        labels.append(f"{notes[i]} Major")
        templates.append(np.roll(min_template, i))
        labels.append(f"{notes[i]} Minor")
    
    templates = np.array(templates)
    
    # Analyze frame by frame
    chords_sequence = []
    times = librosa.frames_to_time(np.arange(chroma.shape[1]), sr=sr)
    
    for i in range(chroma.shape[1]):
        frame_chroma = chroma[:, i]
        if np.sum(frame_chroma) == 0:
            chords_sequence.append("N/A")
            continue
            
        correlations = np.dot(templates, frame_chroma)
        chord_idx = np.argmax(correlations)
        chords_sequence.append(labels[chord_idx])
        
    # Compress output: group consecutive identical chords
    compressed_chords = []
    if chords_sequence:
        current_chord = chords_sequence[0]
        compressed_chords.append({"time": round(float(times[0]), 2), "chord": current_chord})
        
        for i in range(1, len(chords_sequence)):
            if chords_sequence[i] != current_chord:
                current_chord = chords_sequence[i]
                compressed_chords.append({"time": round(float(times[i]), 2), "chord": current_chord})
                
    return compressed_chords

# Build Gradio UI with Blocks
with gr.Blocks(title="AI Stem Studio Backend") as demo:
    gr.Markdown("# AI Stem Studio Backend")
    
    with gr.Tab("Stem Separation"):
        sep_input = gr.Audio(type="filepath", label="Upload Audio")
        sep_btn = gr.Button("Separate Stems")
        with gr.Row():
            sep_vocals = gr.Audio(label="Vocals")
            sep_drums = gr.Audio(label="Drums")
            sep_bass = gr.Audio(label="Bass")
            sep_other = gr.Audio(label="Other")
            sep_piano = gr.Audio(label="Piano")
            sep_guitar = gr.Audio(label="Guitar")
        
        sep_btn.click(
            fn=separate_audio,
            inputs=sep_input,
            outputs=[sep_vocals, sep_drums, sep_bass, sep_other, sep_piano, sep_guitar],
            api_name="separate_audio"
        )

    with gr.Tab("Advanced Metrics"):
        metrics_input = gr.Audio(type="filepath", label="Upload Audio")
        metrics_btn = gr.Button("Analyze Metrics")
        with gr.Row():
            click_output = gr.Audio(label="Click Track")
            structure_output = gr.JSON(label="Song Structure")
        
        metrics_btn.click(
            fn=analyze_advanced_metrics,
            inputs=metrics_input,
            outputs=[click_output, structure_output],
            api_name="analyze_advanced_metrics"
        )
            
    with gr.Tab("Key Detection"):
        key_input = gr.Audio(type="filepath", label="Upload Audio")
        key_btn = gr.Button("Detect Key")
        key_output = gr.JSON(label="Key Analysis Result")
        
        key_btn.click(
            fn=detect_key,
            inputs=key_input,
            outputs=key_output,
            api_name="detect_key"
        )

    with gr.Tab("Chord Extraction"):
        chord_input = gr.Audio(type="filepath", label="Upload Audio")
        chord_btn = gr.Button("Extract Chords")
        chord_output = gr.JSON(label="Chord Progression")
        
        chord_btn.click(
            fn=extract_chords,
            inputs=chord_input,
            outputs=chord_output,
            api_name="extract_chords"
        )

if __name__ == "__main__":
    demo.launch()