SaltProphet commited on
Commit
4a8682f
·
verified ·
1 Parent(s): e72722a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +819 -17
app.py CHANGED
@@ -1,17 +1,819 @@
1
- ---------------------------------------------------------------------------
2
- DuplicateBlockError Traceback (most recent call last)
3
- /tmp/ipython-input-1056203637.py in <cell line: 0>()
4
- 868 # CORRECTED: The previous SyntaxError was fixed, and the 'variant' is now correctly applied to the Button
5
- 869 slice_all_button = gr.Button("Slice, Transform & Tag ALL Stems (Create ZIP)", variant="stop")
6
- --> 870 download_zip_file.render()
7
- 871
8
- 872 gr.Markdown("### Status")
9
-
10
- /usr/local/lib/python3.12/dist-packages/gradio/blocks.py in render(self)
11
- 216 and not self.is_render_replacement
12
- 217 ):
13
- --> 218 raise DuplicateBlockError(
14
- 219 f"A block with id: {self._id} has already been rendered in the current Blocks."
15
- 220 )
16
-
17
- DuplicateBlockError: A block with id: 9 has already been rendered in the current Blocks.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ import gradio as gr
3
+ import numpy as np
4
+ import librosa
5
+ import soundfile as sf
6
+ import os
7
+ import tempfile
8
+ import zipfile
9
+ import time
10
+ import matplotlib
11
+ import matplotlib.pyplot as plt
12
+ from scipy import signal
13
+ from typing import Tuple, List, Any
14
+
15
+ # Use a non-interactive backend for Matplotlib
16
+ matplotlib.use('Agg')
17
+
18
+ # --- UTILITY FUNCTIONS ---
19
+
20
+ def freq_to_midi(freq: float) -> int:
21
+ """Converts a frequency in Hz to a MIDI note number."""
22
+ if freq <= 0:
23
+ return 0
24
+ if freq < 40: # Ignore frequencies below C2 (approx 65Hz)
25
+ return 0
26
+ return int(round(69 + 12 * np.log2(freq / 440.0)))
27
+
28
+ def write_midi_file(notes_list: List[Tuple[int, float, float]], bpm: float, output_path: str):
29
+ """Writes a basic MIDI file from a list of notes."""
30
+ if not notes_list:
31
+ return
32
+
33
+ tempo_us_per_beat = int(60000000 / bpm)
34
+ division = 96 # Ticks per quarter note
35
+ seconds_per_tick = 60.0 / (bpm * division)
36
+
37
+ # Sort notes by start time
38
+ notes_list.sort(key=lambda x: x[1])
39
+
40
+ current_tick = 0
41
+ midi_events = []
42
+
43
+ for note, start_sec, duration_sec in notes_list:
44
+ if note == 0:
45
+ continue
46
+
47
+ # Calculate delta time from last event
48
+ target_tick = int(start_sec / seconds_per_tick)
49
+ delta_tick = target_tick - current_tick
50
+ current_tick = target_tick
51
+
52
+ # Note On event (Channel 1, Velocity 100)
53
+ note_on = [0x90, note, 100]
54
+ midi_events.append((delta_tick, note_on))
55
+
56
+ # Note Off event (Channel 1, Velocity 0)
57
+ duration_ticks = int(duration_sec / seconds_per_tick)
58
+ note_off = [0x80, note, 0]
59
+ midi_events.append((duration_ticks, note_off))
60
+ current_tick += duration_ticks
61
+
62
+ # Build MIDI file
63
+ header = b'MThd' + (6).to_bytes(4, 'big') + (1).to_bytes(2, 'big') + (1).to_bytes(2, 'big') + division.to_bytes(2, 'big')
64
+
65
+ track_data = b''
66
+ for delta, event in midi_events:
67
+ # Encode delta time
68
+ delta_bytes = []
69
+ while True:
70
+ delta_bytes.append(delta & 0x7F)
71
+ if delta <= 0x7F:
72
+ break
73
+ delta >>= 7
74
+ for i in range(len(delta_bytes)-1, -1, -1):
75
+ if i > 0:
76
+ track_data += bytes([delta_bytes[i] | 0x80])
77
+ else:
78
+ track_data += bytes([delta_bytes[i]])
79
+
80
+ # Add event
81
+ track_data += bytes(event)
82
+
83
+ # End of track
84
+ track_data += b'\x00\xFF\x2F\x00'
85
+
86
+ track_chunk = b'MTrk' + len(track_data).to_bytes(4, 'big') + track_data
87
+ midi_data = header + track_chunk
88
+
89
+ with open(output_path, 'wb') as f:
90
+ f.write(midi_data)
91
+
92
+ def get_harmonic_recommendations(key_str: str) -> str:
93
+ """Calculates harmonically compatible keys based on the Camelot wheel."""
94
+ KEY_TO_CAMELOT = {
95
+ "C Maj": "8B", "G Maj": "9B", "D Maj": "10B", "A Maj": "11B", "E Maj": "12B",
96
+ "B Maj": "1B", "F# Maj": "2B", "Db Maj": "3B", "Ab Maj": "4B", "Eb Maj": "5B",
97
+ "Bb Maj": "6B", "F Maj": "7B",
98
+ "A Min": "8A", "E Min": "9A", "B Min": "10A", "F# Min": "11A", "C# Min": "12A",
99
+ "G# Min": "1A", "D# Min": "2A", "Bb Min": "3A", "F Min": "4A", "C Min": "5A",
100
+ "G Min": "6A", "D Min": "7A",
101
+ "Gb Maj": "2B", "Cb Maj": "7B", "A# Min": "3A", "D# Maj": "11B", "G# Maj": "3B"
102
+ }
103
+
104
+ code = KEY_TO_CAMELOT.get(key_str, "N/A")
105
+ if code == "N/A":
106
+ return "N/A (Key not recognized or 'Unknown Key' detected.)"
107
+
108
+ try:
109
+ num = int(code[:-1])
110
+ mode = code[-1]
111
+ opposite_mode = 'B' if mode == 'A' else 'A'
112
+ num_plus_one = (num % 12) + 1
113
+ num_minus_one = 12 if num == 1 else num - 1
114
+ recs = [f"{num}{opposite_mode}", f"{num_plus_one}{mode}", f"{num_minus_one}{mode}"]
115
+ CAMELOT_TO_KEY = {v: k for k, v in KEY_TO_CAMELOT.items()}
116
+ rec_keys = [f"{CAMELOT_TO_KEY.get(r_code, f'Code {r_code}')} ({r_code})" for r_code in recs]
117
+ return " | ".join(rec_keys)
118
+ except Exception:
119
+ return "N/A (Error calculating recommendations.)"
120
+
121
+ def detect_key(y: np.ndarray, sr: int) -> str:
122
+ """Analyzes the audio to determine the most likely musical key."""
123
+ try:
124
+ chroma = librosa.feature.chroma_stft(y=y, sr=sr)
125
+ chroma_sums = np.sum(chroma, axis=1)
126
+ chroma_norm = chroma_sums / np.sum(chroma_sums)
127
+
128
+ major_template = np.array([6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88])
129
+ minor_template = np.array([6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17])
130
+
131
+ pitch_classes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
132
+
133
+ major_correlations = [np.dot(chroma_norm, np.roll(major_template, i)) for i in range(12)]
134
+ best_major_index = np.argmax(major_correlations)
135
+
136
+ minor_correlations = [np.dot(chroma_norm, np.roll(minor_template, i)) for i in range(12)]
137
+ best_minor_index = np.argmax(minor_correlations)
138
+
139
+ if major_correlations[best_major_index] > minor_correlations[best_minor_index]:
140
+ return pitch_classes[best_major_index] + " Maj"
141
+ else:
142
+ return pitch_classes[best_minor_index] + " Min"
143
+ except Exception as e:
144
+ print(f"Key detection failed: {e}")
145
+ return "Unknown Key"
146
+
147
+ def apply_modulation(y: np.ndarray, sr: int, bpm: float, rate: str, pan_depth: float, level_depth: float) -> np.ndarray:
148
+ """Applies tempo-synced LFOs for panning and volume modulation."""
149
+ if y.ndim == 1:
150
+ y = np.stack((y, y), axis=-1)
151
+ elif y.ndim == 0:
152
+ return y
153
+
154
+ N = len(y)
155
+ duration_sec = N / sr
156
+
157
+ rate_map = {'1/2': 0.5, '1/4': 1, '1/8': 2, '1/16': 4}
158
+ beats_per_measure = rate_map.get(rate, 1)
159
+ lfo_freq_hz = (bpm / 60.0) * (beats_per_measure / 4.0)
160
+
161
+ t = np.linspace(0, duration_sec, N, endpoint=False)
162
+
163
+ # Panning LFO
164
+ if pan_depth > 0:
165
+ pan_lfo = np.sin(2 * np.pi * lfo_freq_hz * t) * pan_depth
166
+ L_mod = (1 - pan_lfo) / 2.0
167
+ R_mod = (1 + pan_lfo) / 2.0
168
+ y[:, 0] *= L_mod
169
+ y[:, 1] *= R_mod
170
+
171
+ # Level LFO (Tremolo)
172
+ if level_depth > 0:
173
+ level_lfo = (np.sin(2 * np.pi * lfo_freq_hz * t) + 1) / 2.0
174
+ gain_multiplier = (1 - level_depth) + (level_depth * level_lfo)
175
+ y[:, 0] *= gain_multiplier
176
+ y[:, 1] *= gain_multiplier
177
+
178
+ return y
179
+
180
+ def apply_normalization_dbfs(y: np.ndarray, target_dbfs: float) -> np.ndarray:
181
+ """Applies peak normalization to match a target dBFS value."""
182
+ if target_dbfs >= 0:
183
+ return y
184
+
185
+ current_peak_amp = np.max(np.abs(y))
186
+ target_peak_amp = 10**(target_dbfs / 20.0)
187
+
188
+ if current_peak_amp > 1e-6:
189
+ gain = target_peak_amp / current_peak_amp
190
+ y_normalized = y * gain
191
+ y_normalized = np.clip(y_normalized, -1.0, 1.0)
192
+ return y_normalized
193
+ else:
194
+ return y
195
+
196
+ def apply_filter_modulation(y: np.ndarray, sr: int, bpm: float, rate: str, filter_type: str, freq: float, depth: float) -> np.ndarray:
197
+ """Applies a tempo-synced LFO to a 2nd order Butterworth filter cutoff frequency."""
198
+ if depth == 0:
199
+ return y
200
+
201
+ # Ensure stereo for LFO application
202
+ if y.ndim == 1:
203
+ y = np.stack((y, y), axis=-1)
204
+
205
+ N = len(y)
206
+ duration_sec = N / sr
207
+
208
+ # LFO Rate Calculation
209
+ rate_map = {'1/2': 0.5, '1/4': 1, '1/8': 2, '1/16': 4}
210
+ beats_per_measure = rate_map.get(rate, 1)
211
+ lfo_freq_hz = (bpm / 60.0) * (beats_per_measure / 4.0)
212
+
213
+ t = np.linspace(0, duration_sec, N, endpoint=False)
214
+
215
+ # LFO: ranges from 0 to 1
216
+ lfo_value = (np.sin(2 * np.pi * lfo_freq_hz * t) + 1) / 2.0
217
+
218
+ # Modulate Cutoff Frequency: Cutoff = BaseFreq + (LFO * Depth)
219
+ cutoff_modulation = freq + (lfo_value * depth)
220
+ # Safety clip to prevent instability
221
+ cutoff_modulation = np.clip(cutoff_modulation, 20.0, sr / 2.0 - 100)
222
+
223
+ y_out = np.zeros_like(y)
224
+ filter_type_b = filter_type.lower().replace('-pass', '')
225
+ frame_size = 512 # Frame-based update for filter coefficients
226
+
227
+ # Apply filter channel by channel
228
+ for channel in range(y.shape[1]):
229
+ zi = np.zeros(2) # Initial filter state (2nd order filter)
230
+
231
+ for frame_start in range(0, N, frame_size):
232
+ frame_end = min(frame_start + frame_size, N)
233
+ frame = y[frame_start:frame_end, channel]
234
+
235
+ # Use the average LFO cutoff for the frame
236
+ avg_cutoff = np.mean(cutoff_modulation[frame_start:frame_end])
237
+
238
+ # Calculate 2nd order Butterworth filter coefficients
239
+ b, a = signal.butter(2, avg_cutoff, btype=filter_type_b, fs=sr)
240
+
241
+ # Apply filter to the frame, updating the state `zi`
242
+ filtered_frame, zi = signal.lfilter(b, a, frame, zi=zi)
243
+ y_out[frame_start:frame_end, channel] = filtered_frame
244
+
245
+ return y_out
246
+
247
+ # --- CORE PROCESSING FUNCTIONS ---
248
+
249
+ def separate_stems(audio_file_path: str) -> Tuple[str, str, str, str, str, str, float, str]:
250
+ """Simulates stem separation and detects BPM and Key."""
251
+ if audio_file_path is None:
252
+ raise gr.Error("No audio file uploaded!")
253
+
254
+ try:
255
+ # Load audio
256
+ y_orig, sr_orig = librosa.load(audio_file_path, sr=None)
257
+ y_mono = librosa.to_mono(y_orig.T) if y_orig.ndim > 1 else y_orig
258
+
259
+ # Detect tempo and key
260
+ tempo, _ = librosa.beat.beat_track(y=y_mono, sr=sr_orig)
261
+ detected_bpm = 120 if tempo is None or tempo == 0 else int(np.round(tempo).item())
262
+ detected_key = detect_key(y_mono, sr_orig)
263
+
264
+ # Create mock separated stems
265
+ temp_dir = tempfile.mkdtemp()
266
+ stems = {}
267
+ stem_names = ["vocals", "drums", "bass", "other", "guitar", "piano"]
268
+
269
+ for name in stem_names:
270
+ stem_path = os.path.join(temp_dir, f"{name}.wav")
271
+ # Create mock audio (just a portion of the original)
272
+ sf.write(stem_path, y_orig[:min(len(y_orig), sr_orig*5)], sr_orig) # 5 seconds max
273
+ stems[name] = stem_path
274
+
275
+ return (
276
+ stems["vocals"], stems["drums"], stems["bass"], stems["other"],
277
+ stems["guitar"], stems["piano"], float(detected_bpm), detected_key
278
+ )
279
+ except Exception as e:
280
+ raise gr.Error(f"Error processing audio: {str(e)}")
281
+
282
+ def generate_waveform_preview(y: np.ndarray, sr: int, stem_name: str, temp_dir: str) -> str:
283
+ """Generates a Matplotlib image showing the waveform."""
284
+ img_path = os.path.join(temp_dir, f"{stem_name}_preview.png")
285
+
286
+ plt.figure(figsize=(10, 3))
287
+ y_display = librosa.to_mono(y.T) if y.ndim > 1 else y
288
+ librosa.display.waveshow(y_display, sr=sr, x_axis='time', color="#4a7098")
289
+ plt.title(f"{stem_name} Waveform")
290
+ plt.tight_layout()
291
+ plt.savefig(img_path)
292
+ plt.close()
293
+
294
+ return img_path
295
+
296
+ def slice_stem_real(
297
+ stem_audio_path: str,
298
+ loop_choice: str,
299
+ sensitivity: float,
300
+ stem_name: str,
301
+ manual_bpm: float,
302
+ time_signature: str,
303
+ crossfade_ms: int,
304
+ transpose_semitones: int,
305
+ detected_key: str,
306
+ pan_depth: float,
307
+ level_depth: float,
308
+ modulation_rate: str,
309
+ target_dbfs: float,
310
+ attack_gain: float,
311
+ sustain_gain: float,
312
+ filter_type: str,
313
+ filter_freq: float,
314
+ filter_depth: float
315
+ ) -> Tuple[List[Tuple[str, str]], str]:
316
+ """Slices a single stem and applies transformations."""
317
+ if stem_audio_path is None:
318
+ return [], ""
319
+
320
+ try:
321
+ # Load audio
322
+ sample_rate, y_int = stem_audio_path
323
+ y = librosa.util.buf_to_float(y_int, dtype=np.float32)
324
+
325
+ if y.ndim == 0:
326
+ return [], ""
327
+
328
+ y_mono = librosa.to_mono(y.T) if y.ndim > 1 else y
329
+
330
+ # --- 1. PITCH SHIFTING (if enabled) ---
331
+ if transpose_semitones != 0:
332
+ y_shifted = librosa.effects.pitch_shift(y, sr=sample_rate, n_steps=transpose_semitones)
333
+ y = y_shifted
334
+
335
+ # --- 2. FILTER MODULATION ---
336
+ if filter_depth > 0:
337
+ y = apply_filter_modulation(y, sample_rate, manual_bpm, modulation_rate, filter_type, filter_freq, filter_depth)
338
+
339
+ # --- 3. PAN/LEVEL MODULATION ---
340
+ normalized_pan_depth = pan_depth / 100.0
341
+ normalized_level_depth = level_depth / 100.0
342
+
343
+ if normalized_pan_depth > 0 or normalized_level_depth > 0:
344
+ y = apply_modulation(y, sample_rate, manual_bpm, modulation_rate, normalized_pan_depth, normalized_level_depth)
345
+
346
+ # --- 4. NORMALIZATION ---
347
+ if target_dbfs < 0:
348
+ y = apply_normalization_dbfs(y, target_dbfs)
349
+
350
+ # --- 5. DETERMINE BPM & KEY ---
351
+ bpm_int = int(manual_bpm)
352
+ key_tag = detected_key.replace(" ", "")
353
+ if transpose_semitones != 0:
354
+ root = detected_key.split(" ")[0]
355
+ mode = detected_key.split(" ")[1]
356
+ pitch_classes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
357
+ try:
358
+ current_index = pitch_classes.index(root)
359
+ new_index = (current_index + transpose_semitones) % 12
360
+ new_key_root = pitch_classes[new_index]
361
+ key_tag = f"{new_key_root}{mode}Shift"
362
+ except ValueError:
363
+ pass
364
+
365
+ # --- 6. MIDI GENERATION (Melodic Stems) ---
366
+ output_files = []
367
+ loops_dir = tempfile.mkdtemp()
368
+ is_melodic = stem_name in ["vocals", "bass", "guitar", "piano", "other"]
369
+
370
+ if is_melodic and ("Bar Loops" in loop_choice):
371
+ try:
372
+ # Use piptrack for pitch detection
373
+ pitches, magnitudes = librosa.piptrack(y=y_mono, sr=sample_rate)
374
+ main_pitch_line = np.zeros(pitches.shape[1])
375
+ for t in range(pitches.shape[1]):
376
+ index = magnitudes[:, t].argmax()
377
+ main_pitch_line[t] = pitches[index, t]
378
+
379
+ notes_list = []
380
+ i = 0
381
+ while i < len(main_pitch_line):
382
+ current_freq = main_pitch_line[i]
383
+ current_midi = freq_to_midi(current_freq)
384
+
385
+ j = i
386
+ while j < len(main_pitch_line) and freq_to_midi(main_pitch_line[j]) == current_midi:
387
+ j += 1
388
+
389
+ duration_frames = j - i
390
+ if current_midi != 0 and duration_frames >= 2:
391
+ start_sec = librosa.frames_to_time(i, sr=sample_rate, hop_length=512)
392
+ duration_sec = librosa.frames_to_time(duration_frames, sr=sample_rate, hop_length=512)
393
+ notes_list.append((current_midi, start_sec, duration_sec))
394
+
395
+ i = j
396
+
397
+ full_stem_midi_path = os.path.join(loops_dir, f"{stem_name}_MELODY_{key_tag}_{bpm_int}BPM.mid")
398
+ write_midi_file(notes_list, manual_bpm, full_stem_midi_path)
399
+ output_files.append((full_stem_midi_path, "MIDI"))
400
+
401
+ except Exception as e:
402
+ print(f"MIDI generation failed for {stem_name}: {e}")
403
+
404
+ # --- 7. CALCULATE TIMING & SLICING ---
405
+ beats_per_bar = 4
406
+ if time_signature == "3/4":
407
+ beats_per_bar = 3
408
+
409
+ if "Bar Loops" in loop_choice:
410
+ bars = int(loop_choice.split(" ")[0])
411
+ loop_type_tag = f"{bars}Bar"
412
+ loop_duration_samples = int((60.0 / bpm_int * beats_per_bar * bars) * sample_rate)
413
+
414
+ if loop_duration_samples > 0 and len(y) > loop_duration_samples:
415
+ num_loops = len(y) // loop_duration_samples
416
+
417
+ for i in range(min(num_loops, 10)): # Limit to 10 loops
418
+ start_sample = i * loop_duration_samples
419
+ end_sample = min(start_sample + loop_duration_samples, len(y))
420
+ slice_data = y[start_sample:end_sample]
421
+
422
+ filename = os.path.join(loops_dir, f"{stem_name}_{loop_type_tag}_{i+1:03d}_{key_tag}_{bpm_int}BPM.wav")
423
+ sf.write(filename, slice_data, sample_rate, subtype='PCM_16')
424
+ output_files.append((filename, "WAV"))
425
+
426
+ elif "One-Shots" in loop_choice:
427
+ loop_type_tag = "OneShot"
428
+ # Simple slicing at regular intervals for demo
429
+ slice_length = int(sample_rate * 0.5) # 0.5 second slices
430
+ num_slices = len(y) // slice_length
431
+
432
+ for i in range(min(num_slices, 20)): # Limit to 20 slices
433
+ start_sample = i * slice_length
434
+ end_sample = min(start_sample + slice_length, len(y))
435
+ slice_data = y[start_sample:end_sample]
436
+
437
+ filename = os.path.join(loops_dir, f"{stem_name}_{loop_type_tag}_{i+1:03d}_{key_tag}_{bpm_int}BPM.wav")
438
+ sf.write(filename, slice_data, sample_rate, subtype='PCM_16')
439
+ output_files.append((filename, "WAV"))
440
+
441
+ # --- 8. VISUALIZATION GENERATION ---
442
+ img_path = generate_waveform_preview(y, sample_rate, stem_name, loops_dir)
443
+
444
+ return output_files, img_path
445
+
446
+ except Exception as e:
447
+ raise gr.Error(f"Error processing stem: {str(e)}")
448
+
449
+ def slice_all_and_zip(
450
+ vocals: Tuple[int, np.ndarray],
451
+ drums: Tuple[int, np.ndarray],
452
+ bass: Tuple[int, np.ndarray],
453
+ other: Tuple[int, np.ndarray],
454
+ guitar: Tuple[int, np.ndarray],
455
+ piano: Tuple[int, np.ndarray],
456
+ loop_choice: str,
457
+ sensitivity: float,
458
+ manual_bpm: float,
459
+ time_signature: str,
460
+ crossfade_ms: int,
461
+ transpose_semitones: int,
462
+ detected_key: str,
463
+ pan_depth: float,
464
+ level_depth: float,
465
+ modulation_rate: str,
466
+ target_dbfs: float,
467
+ attack_gain: float,
468
+ sustain_gain: float,
469
+ filter_type: str,
470
+ filter_freq: float,
471
+ filter_depth: float
472
+ ) -> str:
473
+ """Slices all available stems and packages them into a ZIP file."""
474
+ try:
475
+ stems_to_process = {
476
+ "vocals": vocals, "drums": drums, "bass": bass,
477
+ "other": other, "guitar": guitar, "piano": piano
478
+ }
479
+
480
+ # Filter out None stems
481
+ valid_stems = {name: data for name, data in stems_to_process.items() if data is not None}
482
+
483
+ if not valid_stems:
484
+ raise gr.Error("No stems to process! Please separate stems first.")
485
+
486
+ # Create temporary directory for all outputs
487
+ temp_dir = tempfile.mkdtemp()
488
+ zip_path = os.path.join(temp_dir, "Loop_Architect_Pack.zip")
489
+
490
+ with zipfile.ZipFile(zip_path, 'w') as zf:
491
+ for name, data in valid_stems.items():
492
+ # Create temporary file for this stem
493
+ stem_temp_dir = tempfile.mkdtemp()
494
+ stem_path = os.path.join(stem_temp_dir, f"{name}.wav")
495
+ sf.write(stem_path, data[1], data[0])
496
+
497
+ # Process stem
498
+ sliced_files, _ = slice_stem_real(
499
+ (data[0], data[1]), loop_choice, sensitivity, name,
500
+ manual_bpm, time_signature, crossfade_ms, transpose_semitones, detected_key,
501
+ pan_depth, level_depth, modulation_rate, target_dbfs,
502
+ attack_gain, sustain_gain, filter_type, filter_freq, filter_depth
503
+ )
504
+
505
+ # Add files to ZIP
506
+ for file_path, file_type in sliced_files:
507
+ arcname = os.path.join(file_type, os.path.basename(file_path))
508
+ zf.write(file_path, arcname)
509
+
510
+ # Clean up stem temp files
511
+ shutil.rmtree(stem_temp_dir)
512
+
513
+ return zip_path
514
+
515
+ except Exception as e:
516
+ raise gr.Error(f"Error creating ZIP: {str(e)}")
517
+
518
+ # --- GRADIO INTERFACE ---
519
+
520
+ with gr.Blocks(theme=gr.themes.Default(primary_hue="blue", secondary_hue="red")) as demo:
521
+ gr.Markdown("# 🎵 Loop Architect (Pro Edition)")
522
+ gr.Markdown("Upload any song to separate it into stems, detect musical attributes, and then slice and tag the stems for instant use in a DAW.")
523
+
524
+ # State variables
525
+ detected_bpm_state = gr.State(value=120.0)
526
+ detected_key_state = gr.State(value="Unknown Key")
527
+ harmonic_recs_state = gr.State(value="---")
528
+
529
+ with gr.Row():
530
+ with gr.Column(scale=1):
531
+ gr.Markdown("### 1. Separate Stems")
532
+ audio_input = gr.Audio(type="filepath", label="Upload a Track")
533
+ separate_btn = gr.Button("Separate & Analyze Stems", variant="primary")
534
+
535
+ # Outputs for separated stems
536
+ vocals_output = gr.Audio(label="Vocals", visible=False)
537
+ drums_output = gr.Audio(label="Drums", visible=False)
538
+ bass_output = gr.Audio(label="Bass", visible=False)
539
+ other_output = gr.Audio(label="Other / Instrumental", visible=False)
540
+ guitar_output = gr.Audio(label="Guitar", visible=False)
541
+ piano_output = gr.Audio(label="Piano", visible=False)
542
+
543
+ # Analysis results
544
+ with gr.Group():
545
+ gr.Markdown("### 2. Analysis & Transform")
546
+ detected_bpm_key = gr.Textbox(label="Detected Tempo & Key", value="", interactive=False)
547
+ harmonic_recs = gr.Textbox(label="Harmonic Mixing Recommendations", value="", interactive=False)
548
+
549
+ transpose_slider = gr.Slider(
550
+ minimum=-12, maximum=12, value=0, step=1,
551
+ label="Transpose Loops (Semitones)",
552
+ info="Shift the pitch of all slices by +/- 1 octave."
553
+ )
554
+
555
+ # Transient Shaping
556
+ gr.Markdown("### Transient Shaping (Drums Only)")
557
+ with gr.Group():
558
+ attack_gain_slider = gr.Slider(
559
+ minimum=0.5, maximum=1.5, value=1.0, step=0.1,
560
+ label="Attack Gain Multiplier",
561
+ info="Increase (>1.0) for punchier transients."
562
+ )
563
+ sustain_gain_slider = gr.Slider(
564
+ minimum=0.5, maximum=1.5, value=1.0, step=0.1,
565
+ label="Sustain Gain Multiplier",
566
+ info="Increase (>1.0) for longer tails/reverb."
567
+ )
568
+
569
+ # Modulation
570
+ gr.Markdown("### Pan/Level Modulation (LFO 1.0)")
571
+ with gr.Group():
572
+ modulation_rate_radio = gr.Radio(
573
+ ['1/2', '1/4', '1/8', '1/16'],
574
+ label="Modulation Rate (Tempo Synced)",
575
+ value='1/4'
576
+ )
577
+ pan_depth_slider = gr.Slider(
578
+ minimum=0, maximum=100, value=0, step=5,
579
+ label="Pan Modulation Depth (%)",
580
+ info="Creates a stereo auto-pan effect."
581
+ )
582
+ level_depth_slider = gr.Slider(
583
+ minimum=0, maximum=100, value=0, step=5,
584
+ label="Level Modulation Depth (%)",
585
+ info="Creates a tempo-synced tremolo (volume pulse)."
586
+ )
587
+
588
+ # Filter Modulation
589
+ gr.Markdown("### Filter Modulation (LFO 2.0)")
590
+ with gr.Group():
591
+ filter_type_radio = gr.Radio(
592
+ ['low', 'high'],
593
+ label="Filter Type",
594
+ value='low'
595
+ )
596
+ with gr.Row():
597
+ filter_freq_slider = gr.Slider(
598
+ minimum=20, maximum=10000, value=2000, step=10,
599
+ label="Base Cutoff Frequency (Hz)",
600
+ )
601
+ filter_depth_slider = gr.Slider(
602
+ minimum=0, maximum=5000, value=0, step=10,
603
+ label="Modulation Depth (Hz)",
604
+ info="0 = Static filter at Base Cutoff."
605
+ )
606
+
607
+ # Slicing Options
608
+ gr.Markdown("### 3. Slicing Options")
609
+ with gr.Group():
610
+ lufs_target_slider = gr.Slider(
611
+ minimum=-18.0, maximum=-0.1, value=-3.0, step=0.1,
612
+ label="Target Peak Level (dBFS)",
613
+ info="Normalizes all exported loops to this peak volume."
614
+ )
615
+
616
+ loop_options_radio = gr.Radio(
617
+ ["One-Shots", "4 Bar Loops", "8 Bar Loops"],
618
+ label="Slice Type",
619
+ value="One-Shots",
620
+ info="Bar Loops include automatic MIDI generation for melodic stems."
621
+ )
622
+
623
+ with gr.Row():
624
+ bpm_input = gr.Number(
625
+ label="Manual BPM",
626
+ value=120,
627
+ minimum=40,
628
+ maximum=300
629
+ )
630
+ time_sig_radio = gr.Radio(
631
+ ["4/4", "3/4"],
632
+ label="Time Signature",
633
+ value="4/4"
634
+ )
635
+
636
+ sensitivity_slider = gr.Slider(
637
+ minimum=0.01, maximum=0.5, value=0.05, step=0.01,
638
+ label="One-Shot Sensitivity",
639
+ info="Lower values = more slices."
640
+ )
641
+
642
+ crossfade_ms_slider = gr.Slider(
643
+ minimum=0, maximum=30, value=10, step=1,
644
+ label="One-Shot Crossfade (ms)",
645
+ info="Prevents clicks/pops on transient slices."
646
+ )
647
+
648
+ # Create Pack
649
+ gr.Markdown("### 4. Create Pack")
650
+ slice_all_btn = gr.Button("Slice, Transform & Tag ALL Stems (Create ZIP)", variant="stop")
651
+ download_zip_file = gr.File(label="Download Your Loop Pack", visible=False)
652
+
653
+ with gr.Column(scale=2):
654
+ gr.Markdown("### Separated Stems")
655
+ with gr.Row():
656
+ with gr.Column():
657
+ vocals_output.render()
658
+ slice_vocals_btn = gr.Button("Slice Vocals")
659
+ with gr.Column():
660
+ drums_output.render()
661
+ slice_drums_btn = gr.Button("Slice Drums")
662
+ with gr.Row():
663
+ with gr.Column():
664
+ bass_output.render()
665
+ slice_bass_btn = gr.Button("Slice Bass")
666
+ with gr.Column():
667
+ other_output.render()
668
+ slice_other_btn = gr.Button("Slice Other")
669
+ with gr.Row():
670
+ with gr.Column():
671
+ guitar_output.render()
672
+ slice_guitar_btn = gr.Button("Slice Guitar")
673
+ with gr.Column():
674
+ piano_output.render()
675
+ slice_piano_btn = gr.Button("Slice Piano")
676
+
677
+ # Gallery for previews
678
+ gr.Markdown("### Sliced Loops Preview")
679
+ loop_gallery = gr.Gallery(
680
+ label="Generated Loops",
681
+ columns=4,
682
+ object_fit="contain",
683
+ height="auto"
684
+ )
685
+
686
+ # --- EVENT HANDLERS ---
687
+
688
+ # Stem separation
689
+ separate_btn.click(
690
+ fn=separate_stems,
691
+ inputs=[audio_input],
692
+ outputs=[
693
+ vocals_output, drums_output, bass_output, other_output,
694
+ guitar_output, piano_output,
695
+ detected_bpm_state, detected_key_state
696
+ ]
697
+ ).then(
698
+ fn=lambda bpm, key: (f"{bpm} BPM, {key}", get_harmonic_recommendations(key)),
699
+ inputs=[detected_bpm_state, detected_key_state],
700
+ outputs=[detected_bpm_key, harmonic_recs_state]
701
+ ).then(
702
+ fn=lambda bpm, key: gr.update(value=f"{bpm} BPM, {key}"),
703
+ inputs=[detected_bpm_state, detected_key_state],
704
+ outputs=[detected_bpm_key]
705
+ ).then(
706
+ fn=get_harmonic_recommendations,
707
+ inputs=[detected_key_state],
708
+ outputs=[harmonic_recs]
709
+ )
710
+
711
+ # Individual stem slicing
712
+ def slice_and_display(stem_data, loop_choice, sensitivity, stem_name, manual_bpm, time_signature,
713
+ crossfade_ms, transpose_semitones, detected_key, pan_depth, level_depth,
714
+ modulation_rate, target_dbfs, attack_gain, sustain_gain, filter_type,
715
+ filter_freq, filter_depth):
716
+ if stem_data is None:
717
+ return [], "No stem data available"
718
+
719
+ try:
720
+ files, img_path = slice_stem_real(
721
+ stem_data, loop_choice, sensitivity, stem_name,
722
+ manual_bpm, time_signature, crossfade_ms, transpose_semitones, detected_key,
723
+ pan_depth, level_depth, modulation_rate, target_dbfs,
724
+ attack_gain, sustain_gain, filter_type, filter_freq, filter_depth
725
+ )
726
+
727
+ # Return only WAV files for gallery display
728
+ wav_files = [f[0] for f in files if f[1] == "WAV"]
729
+ return wav_files + [img_path], f"Generated {len(wav_files)} slices for {stem_name}"
730
+ except Exception as e:
731
+ return [], f"Error: {str(e)}"
732
+
733
+ slice_vocals_btn.click(
734
+ fn=slice_and_display,
735
+ inputs=[
736
+ vocals_output, loop_options_radio, sensitivity_slider, gr.Textbox(value="vocals", visible=False),
737
+ bpm_input, time_sig_radio, crossfade_ms_slider, transpose_slider, detected_key_state,
738
+ pan_depth_slider, level_depth_slider, modulation_rate_radio, lufs_target_slider,
739
+ attack_gain_slider, sustain_gain_slider, filter_type_radio, filter_freq_slider, filter_depth_slider
740
+ ],
741
+ outputs=[loop_gallery, gr.Textbox(label="Status")]
742
+ )
743
+
744
+ slice_drums_btn.click(
745
+ fn=slice_and_display,
746
+ inputs=[
747
+ drums_output, loop_options_radio, sensitivity_slider, gr.Textbox(value="drums", visible=False),
748
+ bpm_input, time_sig_radio, crossfade_ms_slider, transpose_slider, detected_key_state,
749
+ pan_depth_slider, level_depth_slider, modulation_rate_radio, lufs_target_slider,
750
+ attack_gain_slider, sustain_gain_slider, filter_type_radio, filter_freq_slider, filter_depth_slider
751
+ ],
752
+ outputs=[loop_gallery, gr.Textbox(label="Status")]
753
+ )
754
+
755
+ slice_bass_btn.click(
756
+ fn=slice_and_display,
757
+ inputs=[
758
+ bass_output, loop_options_radio, sensitivity_slider, gr.Textbox(value="bass", visible=False),
759
+ bpm_input, time_sig_radio, crossfade_ms_slider, transpose_slider, detected_key_state,
760
+ pan_depth_slider, level_depth_slider, modulation_rate_radio, lufs_target_slider,
761
+ attack_gain_slider, sustain_gain_slider, filter_type_radio, filter_freq_slider, filter_depth_slider
762
+ ],
763
+ outputs=[loop_gallery, gr.Textbox(label="Status")]
764
+ )
765
+
766
+ slice_other_btn.click(
767
+ fn=slice_and_display,
768
+ inputs=[
769
+ other_output, loop_options_radio, sensitivity_slider, gr.Textbox(value="other", visible=False),
770
+ bpm_input, time_sig_radio, crossfade_ms_slider, transpose_slider, detected_key_state,
771
+ pan_depth_slider, level_depth_slider, modulation_rate_radio, lufs_target_slider,
772
+ attack_gain_slider, sustain_gain_slider, filter_type_radio, filter_freq_slider, filter_depth_slider
773
+ ],
774
+ outputs=[loop_gallery, gr.Textbox(label="Status")]
775
+ )
776
+
777
+ slice_guitar_btn.click(
778
+ fn=slice_and_display,
779
+ inputs=[
780
+ guitar_output, loop_options_radio, sensitivity_slider, gr.Textbox(value="guitar", visible=False),
781
+ bpm_input, time_sig_radio, crossfade_ms_slider, transpose_slider, detected_key_state,
782
+ pan_depth_slider, level_depth_slider, modulation_rate_radio, lufs_target_slider,
783
+ attack_gain_slider, sustain_gain_slider, filter_type_radio, filter_freq_slider, filter_depth_slider
784
+ ],
785
+ outputs=[loop_gallery, gr.Textbox(label="Status")]
786
+ )
787
+
788
+ slice_piano_btn.click(
789
+ fn=slice_and_display,
790
+ inputs=[
791
+ piano_output, loop_options_radio, sensitivity_slider, gr.Textbox(value="piano", visible=False),
792
+ bpm_input, time_sig_radio, crossfade_ms_slider, transpose_slider, detected_key_state,
793
+ pan_depth_slider, level_depth_slider, modulation_rate_radio, lufs_target_slider,
794
+ attack_gain_slider, sustain_gain_slider, filter_type_radio, filter_freq_slider, filter_depth_slider
795
+ ],
796
+ outputs=[loop_gallery, gr.Textbox(label="Status")]
797
+ )
798
+
799
+ # Slice all stems and create ZIP
800
+ slice_all_btn.click(
801
+ fn=slice_all_and_zip,
802
+ inputs=[
803
+ vocals_output, drums_output, bass_output, other_output, guitar_output, piano_output,
804
+ loop_options_radio, sensitivity_slider,
805
+ bpm_input, time_sig_radio, crossfade_ms_slider, transpose_slider, detected_key_state,
806
+ pan_depth_slider, level_depth_slider, modulation_rate_radio, lufs_target_slider,
807
+ attack_gain_slider, sustain_gain_slider,
808
+ filter_type_radio, filter_freq_slider, filter_depth_slider
809
+ ],
810
+ outputs=[download_zip_file]
811
+ ).then(
812
+ fn=lambda: gr.update(visible=True),
813
+ inputs=None,
814
+ outputs=[download_zip_file]
815
+ )
816
+
817
+ # Launch the app
818
+ if __name__ == "__main__":
819
+ demo.launch()