Spaces:
Sleeping
Sleeping
add stereo imaging and neural pan + fix error
Browse files
app.py
CHANGED
|
@@ -7,10 +7,10 @@ import gradio as gr
|
|
| 7 |
|
| 8 |
# Default presets for common genres
|
| 9 |
PRESETS = {
|
| 10 |
-
"rock": {"gain": 6, "compress_threshold": -20, "compress_ratio": 4, "low_pass": 12000, "high_pass": 80},
|
| 11 |
-
"pop": {"gain": 4, "compress_threshold": -18, "compress_ratio": 3, "low_pass": 15000, "high_pass": 100},
|
| 12 |
-
"jazz": {"gain": 3, "compress_threshold": -22, "compress_ratio": 2, "low_pass": 14000, "high_pass": 60},
|
| 13 |
-
"electronic": {"gain": 8, "compress_threshold": -16, "compress_ratio": 5, "low_pass": 20000, "high_pass": 120},
|
| 14 |
}
|
| 15 |
|
| 16 |
# JSON export extension
|
|
@@ -27,33 +27,59 @@ def apply_mastering(audio: AudioSegment, params: dict) -> AudioSegment:
|
|
| 27 |
# Gain
|
| 28 |
processed = audio + params["gain"]
|
| 29 |
# Compression
|
| 30 |
-
processed = compress_dynamic_range(
|
|
|
|
| 31 |
threshold=params["compress_threshold"],
|
| 32 |
-
ratio=params["compress_ratio"]
|
|
|
|
| 33 |
# EQ filters
|
| 34 |
processed = high_pass_filter(processed, params["high_pass"])
|
| 35 |
processed = low_pass_filter(processed, params["low_pass"])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
# Normalization
|
| 37 |
processed = normalize(processed)
|
| 38 |
return processed
|
| 39 |
|
| 40 |
|
| 41 |
-
def
|
| 42 |
-
#
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
if preset_name != "custom":
|
| 46 |
params = PRESETS[preset_name]
|
| 47 |
else:
|
| 48 |
params = {"gain": gain, "compress_threshold": threshold, "compress_ratio": ratio,
|
| 49 |
-
"low_pass": lp, "high_pass": hp}
|
| 50 |
-
# Process
|
| 51 |
out = apply_mastering(audio, params)
|
| 52 |
-
# Export audio
|
| 53 |
buf = io.BytesIO()
|
| 54 |
out.export(buf, format="wav")
|
| 55 |
buf.seek(0)
|
| 56 |
-
# Create preset JSON
|
| 57 |
preset = {"preset": preset_name, "params": params}
|
| 58 |
preset_buf = io.BytesIO(json.dumps(preset, indent=2).encode())
|
| 59 |
preset_buf.name = "preset" + EXPORT_EXT
|
|
@@ -65,40 +91,40 @@ def load_preset_json(file_obj):
|
|
| 65 |
name = data.get("preset", "custom")
|
| 66 |
params = data.get("params", {})
|
| 67 |
return name, params.get("gain", 0), params.get("compress_threshold", 0), \
|
| 68 |
-
params.get("compress_ratio", 1), params.get("low_pass", 20000),
|
|
|
|
| 69 |
|
| 70 |
with gr.Blocks(css=CUSTOM_CSS) as demo:
|
| 71 |
gr.Markdown("<div id='title'>Advanced Browser-based Audio Mastering Suite</div>")
|
| 72 |
with gr.Row():
|
| 73 |
with gr.Column():
|
| 74 |
-
|
|
|
|
| 75 |
preset_dropdown = gr.Dropdown(choices=list(PRESETS.keys()) + ["custom"], value="rock", label="Preset")
|
| 76 |
gain = gr.Slider(-10, 20, value=PRESETS["rock"]["gain"], label="Gain (dB)")
|
| 77 |
threshold = gr.Slider(-60, 0, value=PRESETS["rock"]["compress_threshold"], label="Compress Threshold (dB)")
|
| 78 |
ratio = gr.Slider(1, 10, value=PRESETS["rock"]["compress_ratio"], label="Compress Ratio")
|
| 79 |
lp = gr.Slider(1000, 20000, value=PRESETS["rock"]["low_pass"], label="Low-pass Frequency (Hz)")
|
| 80 |
hp = gr.Slider(20, 500, value=PRESETS["rock"]["high_pass"], label="High-pass Frequency (Hz)")
|
|
|
|
|
|
|
| 81 |
load_preset = gr.File(label="Load .master Preset", file_types=[".master"])
|
| 82 |
export_button = gr.Button("Process & Export")
|
| 83 |
with gr.Column():
|
| 84 |
-
|
|
|
|
| 85 |
export_preset_file = gr.File(label="Download Preset (.master)")
|
| 86 |
-
|
| 87 |
-
# Interactivity
|
| 88 |
def sync_sliders(preset):
|
| 89 |
if preset != "custom":
|
| 90 |
p = PRESETS[preset]
|
| 91 |
-
return gr.update(value=p["gain"]), gr.update(value=p["compress_threshold"]), \
|
| 92 |
-
|
| 93 |
-
|
|
|
|
| 94 |
return None
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
outputs=[preset_dropdown, gain, threshold, ratio, lp, hp])
|
| 99 |
-
export_button.click(process_and_export,
|
| 100 |
-
inputs=[audio_input, preset_dropdown, gain, threshold, ratio, lp, hp],
|
| 101 |
-
outputs=[output_audio, export_preset_file])
|
| 102 |
|
| 103 |
if __name__ == "__main__":
|
| 104 |
demo.launch()
|
|
|
|
| 7 |
|
| 8 |
# Default presets for common genres
|
| 9 |
PRESETS = {
|
| 10 |
+
"rock": {"gain": 6, "compress_threshold": -20, "compress_ratio": 4, "low_pass": 12000, "high_pass": 80, "stereo_width": 100, "pan": 0},
|
| 11 |
+
"pop": {"gain": 4, "compress_threshold": -18, "compress_ratio": 3, "low_pass": 15000, "high_pass": 100, "stereo_width": 100, "pan": 0},
|
| 12 |
+
"jazz": {"gain": 3, "compress_threshold": -22, "compress_ratio": 2, "low_pass": 14000, "high_pass": 60, "stereo_width": 90, "pan": 0},
|
| 13 |
+
"electronic": {"gain": 8, "compress_threshold": -16, "compress_ratio": 5, "low_pass": 20000, "high_pass": 120, "stereo_width": 120, "pan": 0},
|
| 14 |
}
|
| 15 |
|
| 16 |
# JSON export extension
|
|
|
|
| 27 |
# Gain
|
| 28 |
processed = audio + params["gain"]
|
| 29 |
# Compression
|
| 30 |
+
processed = compress_dynamic_range(
|
| 31 |
+
processed,
|
| 32 |
threshold=params["compress_threshold"],
|
| 33 |
+
ratio=params["compress_ratio"]
|
| 34 |
+
)
|
| 35 |
# EQ filters
|
| 36 |
processed = high_pass_filter(processed, params["high_pass"])
|
| 37 |
processed = low_pass_filter(processed, params["low_pass"])
|
| 38 |
+
# Stereo Imaging (Width)
|
| 39 |
+
processed = apply_stereo_width(processed, params.get("stereo_width", 100))
|
| 40 |
+
# Neural Pan
|
| 41 |
+
processed = apply_pan(processed, params.get("pan", 0))
|
| 42 |
# Normalization
|
| 43 |
processed = normalize(processed)
|
| 44 |
return processed
|
| 45 |
|
| 46 |
|
| 47 |
+
def apply_stereo_width(audio: AudioSegment, width: float) -> AudioSegment:
|
| 48 |
+
# Width in percent: 0 = mono, 100 = original, >100 = enhanced
|
| 49 |
+
left, right = audio.split_to_mono()
|
| 50 |
+
mid = (left + right) / 2
|
| 51 |
+
side = (left - right) / 2
|
| 52 |
+
# scale side for width control
|
| 53 |
+
side = side * (width / 100)
|
| 54 |
+
new_left = mid + side
|
| 55 |
+
new_right = mid - side
|
| 56 |
+
return AudioSegment.from_mono_audiosegments(new_left, new_right)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def apply_pan(audio: AudioSegment, pan: float) -> AudioSegment:
|
| 60 |
+
# Pan from -100 (left) to 100 (right)
|
| 61 |
+
left, right = audio.split_to_mono()
|
| 62 |
+
pan = max(-100, min(100, pan)) / 100
|
| 63 |
+
if pan > 0:
|
| 64 |
+
new_left = left * (1 - pan)
|
| 65 |
+
new_right = right
|
| 66 |
+
else:
|
| 67 |
+
new_left = left
|
| 68 |
+
new_right = right * (1 + pan)
|
| 69 |
+
return AudioSegment.from_mono_audiosegments(new_left, new_right)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def process_and_export(audio_file, preset_name, gain, threshold, ratio, lp, hp, width, pan):
|
| 73 |
+
audio = AudioSegment.from_file(audio_file)
|
| 74 |
if preset_name != "custom":
|
| 75 |
params = PRESETS[preset_name]
|
| 76 |
else:
|
| 77 |
params = {"gain": gain, "compress_threshold": threshold, "compress_ratio": ratio,
|
| 78 |
+
"low_pass": lp, "high_pass": hp, "stereo_width": width, "pan": pan}
|
|
|
|
| 79 |
out = apply_mastering(audio, params)
|
|
|
|
| 80 |
buf = io.BytesIO()
|
| 81 |
out.export(buf, format="wav")
|
| 82 |
buf.seek(0)
|
|
|
|
| 83 |
preset = {"preset": preset_name, "params": params}
|
| 84 |
preset_buf = io.BytesIO(json.dumps(preset, indent=2).encode())
|
| 85 |
preset_buf.name = "preset" + EXPORT_EXT
|
|
|
|
| 91 |
name = data.get("preset", "custom")
|
| 92 |
params = data.get("params", {})
|
| 93 |
return name, params.get("gain", 0), params.get("compress_threshold", 0), \
|
| 94 |
+
params.get("compress_ratio", 1), params.get("low_pass", 20000), \
|
| 95 |
+
params.get("high_pass", 20), params.get("stereo_width", 100), params.get("pan", 0)
|
| 96 |
|
| 97 |
with gr.Blocks(css=CUSTOM_CSS) as demo:
|
| 98 |
gr.Markdown("<div id='title'>Advanced Browser-based Audio Mastering Suite</div>")
|
| 99 |
with gr.Row():
|
| 100 |
with gr.Column():
|
| 101 |
+
# Use 'filepath' to get the file path for AudioSegment
|
| 102 |
+
audio_input = gr.Audio(label="Upload Audio", type="filepath")
|
| 103 |
preset_dropdown = gr.Dropdown(choices=list(PRESETS.keys()) + ["custom"], value="rock", label="Preset")
|
| 104 |
gain = gr.Slider(-10, 20, value=PRESETS["rock"]["gain"], label="Gain (dB)")
|
| 105 |
threshold = gr.Slider(-60, 0, value=PRESETS["rock"]["compress_threshold"], label="Compress Threshold (dB)")
|
| 106 |
ratio = gr.Slider(1, 10, value=PRESETS["rock"]["compress_ratio"], label="Compress Ratio")
|
| 107 |
lp = gr.Slider(1000, 20000, value=PRESETS["rock"]["low_pass"], label="Low-pass Frequency (Hz)")
|
| 108 |
hp = gr.Slider(20, 500, value=PRESETS["rock"]["high_pass"], label="High-pass Frequency (Hz)")
|
| 109 |
+
width = gr.Slider(0, 200, value=PRESETS["rock"]["stereo_width"], label="Stereo Width (%)")
|
| 110 |
+
pan = gr.Slider(-100, 100, value=PRESETS["rock"]["pan"], label="Pan (-100 Left to 100 Right)")
|
| 111 |
load_preset = gr.File(label="Load .master Preset", file_types=[".master"])
|
| 112 |
export_button = gr.Button("Process & Export")
|
| 113 |
with gr.Column():
|
| 114 |
+
# Set output to filepath to serve downloadable file
|
| 115 |
+
output_audio = gr.Audio(label="Processed Audio", type="filepath")
|
| 116 |
export_preset_file = gr.File(label="Download Preset (.master)")
|
|
|
|
|
|
|
| 117 |
def sync_sliders(preset):
|
| 118 |
if preset != "custom":
|
| 119 |
p = PRESETS[preset]
|
| 120 |
+
return (gr.update(value=p["gain"]), gr.update(value=p["compress_threshold"]), \
|
| 121 |
+
gr.update(value=p["compress_ratio"]), gr.update(value=p["low_pass"]), \
|
| 122 |
+
gr.update(value=p["high_pass"]), gr.update(value=p["stereo_width"]), \
|
| 123 |
+
gr.update(value=p["pan"]))
|
| 124 |
return None
|
| 125 |
+
preset_dropdown.change(sync_sliders, inputs=[preset_dropdown], outputs=[gain, threshold, ratio, lp, hp, width, pan])
|
| 126 |
+
load_preset.upload(load_preset_json, inputs=[load_preset], outputs=[preset_dropdown, gain, threshold, ratio, lp, hp, width, pan])
|
| 127 |
+
export_button.click(process_and_export, inputs=[audio_input, preset_dropdown, gain, threshold, ratio, lp, hp, width, pan], outputs=[output_audio, export_preset_file])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
if __name__ == "__main__":
|
| 130 |
demo.launch()
|