File size: 11,656 Bytes
f6e2a5f
8aee03f
a82e0c6
8aee03f
a82e0c6
9301734
e4398dd
4e02325
 
 
45077a2
 
636d339
 
 
3af2469
 
fc163b0
e4398dd
9301734
 
c108159
4e02325
9301734
 
 
4e02325
9301734
 
a82e0c6
e4398dd
a82e0c6
 
 
 
 
9301734
 
a82e0c6
 
 
 
 
9301734
 
a82e0c6
 
9301734
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e4398dd
45077a2
 
 
 
 
 
 
 
 
 
 
e4398dd
 
 
 
 
 
 
 
 
 
 
 
4e02325
 
3af2469
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
636d339
e4398dd
bf7d84f
 
 
 
 
 
 
 
 
 
 
3af2469
bf7d84f
 
 
 
e4398dd
0663ef0
 
 
636d339
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0663ef0
3af2469
 
 
 
 
 
 
 
 
 
 
e4398dd
636d339
a82e0c6
4e02325
 
 
 
 
 
 
 
 
 
 
 
 
e4398dd
 
4e02325
 
a82e0c6
 
4e02325
 
 
 
 
 
 
 
e4398dd
636d339
a82e0c6
3af2469
 
 
 
7f358cd
3af2469
7f358cd
 
 
3af2469
7f358cd
 
3af2469
 
 
7f358cd
 
 
 
3af2469
 
 
7f358cd
 
 
3af2469
 
 
 
 
 
 
 
 
 
 
 
 
 
e4398dd
4e02325
 
 
 
 
 
 
 
 
 
 
7f358cd
 
 
 
3af2469
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7f358cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3af2469
 
 
 
 
 
 
7f358cd
3af2469
 
7f358cd
3af2469
 
7f358cd
3af2469
 
 
 
 
 
 
 
 
 
 
 
7f358cd
 
 
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
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
import gradio as gr
from pydub import AudioSegment
import numpy as np
import tempfile
import os
import noisereduce as nr
import json
import torch
from demucs import pretrained
from demucs.apply import apply_model
import torchaudio
from pathlib import Path
import matplotlib.pyplot as plt
from io import BytesIO
from PIL import Image
import zipfile
import datetime

# === Helper Functions ===
def audiosegment_to_array(audio):
    return np.array(audio.get_array_of_samples()), audio.frame_rate

def array_to_audiosegment(samples, frame_rate, channels=1):
    return AudioSegment(
        samples.tobytes(),
        frame_rate=frame_rate,
        sample_width=samples.dtype.itemsize,
        channels=channels
    )

# === Effect Functions ===
def apply_normalize(audio):
    return audio.normalize()

def apply_noise_reduction(audio):
    samples, frame_rate = audiosegment_to_array(audio)
    reduced = nr.reduce_noise(y=samples, sr=frame_rate)
    return array_to_audiosegment(reduced, frame_rate, channels=audio.channels)

def apply_compression(audio):
    return audio.compress_dynamic_range()

def apply_reverb(audio):
    reverb = audio - 10
    return audio.overlay(reverb, position=1000)

def apply_pitch_shift(audio, semitones=-2):
    new_frame_rate = int(audio.frame_rate * (2 ** (semitones / 12)))
    samples = np.array(audio.get_array_of_samples())
    resampled = np.interp(
        np.arange(0, len(samples), 2 ** (semitones / 12)),
        np.arange(len(samples)),
        samples
    ).astype(np.int16)
    return AudioSegment(
        resampled.tobytes(),
        frame_rate=new_frame_rate,
        sample_width=audio.sample_width,
        channels=audio.channels
    )

def apply_echo(audio, delay_ms=500, decay=0.5):
    echo = audio - 10
    return audio.overlay(echo, position=delay_ms)

def apply_stereo_widen(audio, pan_amount=0.3):
    left = audio.pan(-pan_amount)
    right = audio.pan(pan_amount)
    return AudioSegment.from_mono_audiosegments(left, right)

def apply_bass_boost(audio, gain=10):
    return audio.low_pass_filter(100).apply_gain(gain)

def apply_treble_boost(audio, gain=10):
    return audio.high_pass_filter(4000).apply_gain(gain)

# === Vocal Isolation Helpers ===
def load_track_local(path, sample_rate, channels=2):
    sig, rate = torchaudio.load(path)
    if rate != sample_rate:
        sig = torchaudio.functional.resample(sig, rate, sample_rate)
    if channels == 1:
        sig = sig.mean(0)
    return sig

def save_track(path, wav, sample_rate):
    path = Path(path)
    torchaudio.save(str(path), wav, sample_rate)

def apply_vocal_isolation(audio_path):
    model = pretrained.get_model(name='htdemucs')
    wav = load_track_local(audio_path, model.samplerate, channels=2)
    ref = wav.mean(0)
    wav -= ref[:, None]
    sources = apply_model(model, wav[None])[0]
    wav += ref[:, None]

    vocal_track = sources[3].cpu()  # index 3 = vocals
    out_path = os.path.join(tempfile.gettempdir(), "vocals.wav")
    save_track(out_path, vocal_track, model.samplerate)
    return out_path

# === Stem Splitting (Drums, Bass, Other, Vocals) ===
def stem_split(audio_path):
    model = pretrained.get_model(name='htdemucs')
    wav = load_track_local(audio_path, model.samplerate, channels=2)
    sources = apply_model(model, wav[None])[0]

    output_dir = tempfile.mkdtemp()
    stem_paths = []

    for i, name in enumerate(['drums', 'bass', 'other', 'vocals']):
        path = os.path.join(output_dir, f"{name}.wav")
        save_track(path, sources[i].cpu(), model.samplerate)
        stem_paths.append((path, name))

    return stem_paths

# === Preset Loader with Fallback ===
def load_presets():
    try:
        preset_files = [f for f in os.listdir("presets") if f.endswith(".json")]
        presets = {}
        for f in preset_files:
            path = os.path.join("presets", f)
            try:
                with open(path, "r") as infile:
                    data = json.load(infile)
                    if "name" in data and "effects" in data:
                        presets[data["name"]] = data["effects"]
            except json.JSONDecodeError:
                print(f"Invalid JSON: {f}")
        return presets
    except FileNotFoundError:
        print("Presets folder not found")
        return {}

preset_choices = load_presets()

if not preset_choices:
    preset_choices = {
        "Default": [],
        "Clean Podcast": ["Noise Reduction", "Normalize"],
        "Music Remix": ["Bass Boost", "Stereo Widening"]
    }

preset_names = list(preset_choices.keys())

# === Waveform Generator ===
def show_waveform(audio_file):
    try:
        audio = AudioSegment.from_file(audio_file)
        samples = np.array(audio.get_array_of_samples())
        plt.figure(figsize=(10, 2))
        plt.plot(samples[:10000], color="blue")
        plt.axis("off")
        buf = BytesIO()
        plt.savefig(buf, format="png", bbox_inches="tight", dpi=100)
        plt.close()
        buf.seek(0)
        return Image.open(buf)
    except Exception as e:
        return None

# === Session Info Export ===
def generate_session_log(audio_path, effects, isolate_vocals, export_format):
    log = {
        "timestamp": str(datetime.datetime.now()),
        "filename": os.path.basename(audio_path),
        "effects_applied": effects,
        "isolate_vocals": isolate_vocals,
        "export_format": export_format
    }
    return json.dumps(log, indent=2)

# === Main Processing Function ===
def process_audio(audio_file, selected_effects, isolate_vocals, preset_name, export_format):
    audio = AudioSegment.from_file(audio_file)

    effect_map = {
        "Noise Reduction": apply_noise_reduction,
        "Compress Dynamic Range": apply_compression,
        "Add Reverb": apply_reverb,
        "Pitch Shift": lambda x: apply_pitch_shift(x),
        "Echo": apply_echo,
        "Stereo Widening": apply_stereo_widen,
        "Bass Boost": apply_bass_boost,
        "Treble Boost": apply_treble_boost,
        "Normalize": apply_normalize,
    }

    effects_to_apply = preset_choices.get(preset_name, selected_effects)
    for effect_name in effects_to_apply:
        if effect_name in effect_map:
            audio = effect_map[effect_name](audio)

    with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as f:
        if isolate_vocals:
            temp_input = os.path.join(tempfile.gettempdir(), "input.wav")
            audio.export(temp_input, format="wav")
            vocal_path = apply_vocal_isolation(temp_input)
            final_audio = AudioSegment.from_wav(vocal_path)
        else:
            final_audio = audio

        output_path = f.name
        final_audio.export(output_path, format=export_format.lower())

        waveform_image = show_waveform(output_path)
        session_log = generate_session_log(audio_file, effects_to_apply, isolate_vocals, export_format)

        return output_path, waveform_image, session_log

# === Batch Processing Function ===
def batch_process_audio(files, selected_effects, isolate_vocals, preset_name, export_format):
    output_dir = tempfile.mkdtemp()
    results = []
    session_logs = []

    for file in files:
        processed_path, _, log = process_audio(file.name, selected_effects, isolate_vocals, preset_name, export_format)
        results.append(processed_path)
        session_logs.append(log)

    zip_path = os.path.join(output_dir, "batch_output.zip")
    with zipfile.ZipFile(zip_path, 'w') as zipf:
        for i, res in enumerate(results):
            filename = f"processed_{i}.{export_format.lower()}"
            zipf.write(res, filename)
            zipf.writestr(f"session_info_{i}.json", session_logs[i])

    return zip_path

# === Custom Preset Upload Handler ===
def upload_preset(preset_file):
    try:
        with open(preset_file.name, "r") as f:
            data = json.load(f)
        if "name" in data and "effects" in data:
            preset_choices[data["name"]] = data["effects"]
            return f"βœ… Loaded custom preset: {data['name']}"
        else:
            return "❌ Invalid preset file"
    except Exception as e:
        return f"⚠️ Error loading preset: {str(e)}"

# === Gradio Interface ===
effect_options = [
    "Noise Reduction",
    "Compress Dynamic Range",
    "Add Reverb",
    "Pitch Shift",
    "Echo",
    "Stereo Widening",
    "Bass Boost",
    "Treble Boost",
    "Normalize"
]

# === Multi-Tab UI ===
with gr.Blocks(title="AI Audio Studio") as demo:
    gr.Markdown("## 🎧 AI Audio Studio\nUpload, edit, export β€” all powered by AI")

    # ----- Single File Studio Tab -----
    with gr.Tab("🎡 Single File Studio"):
        gr.Interface(
            fn=process_audio,
            inputs=[
                gr.Audio(label="Upload Audio", type="filepath"),
                gr.CheckboxGroup(choices=effect_options, label="Apply Effects in Order"),
                gr.Checkbox(label="Isolate Vocals After Effects"),
                gr.Dropdown(choices=preset_names, label="Select Preset", value=preset_names[0] if preset_names else None),
                gr.Dropdown(choices=["MP3", "WAV"], label="Export Format", value="MP3")
            ],
            outputs=[
                gr.Audio(label="Processed Audio", type="filepath"),
                gr.Image(label="Waveform Preview"),
                gr.Textbox(label="Session Log (JSON)", lines=5)
            ],
            title="Edit One File at a Time",
            description="Apply effects, preview waveform, and export as MP3 or WAV",
            allow_flagging="never"
        )

    # ----- Batch Processing Tab -----
    with gr.Tab("πŸ”Š Batch Processing"):
        gr.Interface(
            fn=batch_process_audio,
            inputs=[
                gr.File(label="Upload Multiple Audio Files", file_count="multiple"),
                gr.CheckboxGroup(choices=effect_options, label="Apply Effects in Order"),
                gr.Checkbox(label="Isolate Vocals After Effects"),
                gr.Dropdown(choices=preset_names, label="Select Preset", value=preset_names[0] if preset_names else None),
                gr.Dropdown(choices=["MP3", "WAV"], label="Export Format", value="MP3")
            ],
            outputs=gr.File(label="Download ZIP of All Processed Files"),
            title="Batch Audio Processor",
            description="Upload multiple files, apply effects in bulk, and download all results in a single ZIP.",
            allow_flagging="never",
            submit_btn="Process All Files",
            clear_btn=False
        )

    # ----- Remix Mode Tab -----
    with gr.Tab("πŸŽ› Remix Mode (Split Stems)"):
        def remix_mode(audio_file):
            stem_paths = stem_split(audio_file.name)
            return [path for path, _ in stem_paths], \
                   [name for _, name in stem_paths]

        gr.Interface(
            fn=remix_mode,
            inputs=gr.Audio(label="Upload Music Track", type="filepath"),
            outputs=[
                gr.File(label="Stem Files (Vocals, Drums, etc.)"),
                gr.Textbox(label="Stem Names")
            ],
            title="Split Into Drums, Bass, Vocals",
            description="Use AI to separate musical elements like vocals, drums, and bass."
        )

    # ----- Preset Manager Tab -----
    with gr.Tab("βš™οΈ Preset Manager"):
        gr.Interface(
            fn=upload_preset,
            inputs=gr.File(label="Upload Your Own Preset (.json)"),
            outputs=gr.Textbox(label="Preset Status"),
            title="Load Custom Presets",
            description="Upload your own `.json` preset to customize effect chains."
        )

demo.launch()