master / app.py
namelessai's picture
add stereo imaging and neural pan + fix error
536aac5 verified
import io
import json
from pydub import AudioSegment
from pydub.effects import normalize, compress_dynamic_range, low_pass_filter, high_pass_filter
import numpy as np
import gradio as gr
# Default presets for common genres
PRESETS = {
"rock": {"gain": 6, "compress_threshold": -20, "compress_ratio": 4, "low_pass": 12000, "high_pass": 80, "stereo_width": 100, "pan": 0},
"pop": {"gain": 4, "compress_threshold": -18, "compress_ratio": 3, "low_pass": 15000, "high_pass": 100, "stereo_width": 100, "pan": 0},
"jazz": {"gain": 3, "compress_threshold": -22, "compress_ratio": 2, "low_pass": 14000, "high_pass": 60, "stereo_width": 90, "pan": 0},
"electronic": {"gain": 8, "compress_threshold": -16, "compress_ratio": 5, "low_pass": 20000, "high_pass": 120, "stereo_width": 120, "pan": 0},
}
# JSON export extension
EXPORT_EXT = ".master"
# Custom CSS for Gradio
CUSTOM_CSS = """
body { background-color: #1e1e1e; color: #f0f0f0; }
.gradio-container { border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.5); }
#title { font-size: 2rem; margin-bottom: 1rem; }
"""
def apply_mastering(audio: AudioSegment, params: dict) -> AudioSegment:
# Gain
processed = audio + params["gain"]
# Compression
processed = compress_dynamic_range(
processed,
threshold=params["compress_threshold"],
ratio=params["compress_ratio"]
)
# EQ filters
processed = high_pass_filter(processed, params["high_pass"])
processed = low_pass_filter(processed, params["low_pass"])
# Stereo Imaging (Width)
processed = apply_stereo_width(processed, params.get("stereo_width", 100))
# Neural Pan
processed = apply_pan(processed, params.get("pan", 0))
# Normalization
processed = normalize(processed)
return processed
def apply_stereo_width(audio: AudioSegment, width: float) -> AudioSegment:
# Width in percent: 0 = mono, 100 = original, >100 = enhanced
left, right = audio.split_to_mono()
mid = (left + right) / 2
side = (left - right) / 2
# scale side for width control
side = side * (width / 100)
new_left = mid + side
new_right = mid - side
return AudioSegment.from_mono_audiosegments(new_left, new_right)
def apply_pan(audio: AudioSegment, pan: float) -> AudioSegment:
# Pan from -100 (left) to 100 (right)
left, right = audio.split_to_mono()
pan = max(-100, min(100, pan)) / 100
if pan > 0:
new_left = left * (1 - pan)
new_right = right
else:
new_left = left
new_right = right * (1 + pan)
return AudioSegment.from_mono_audiosegments(new_left, new_right)
def process_and_export(audio_file, preset_name, gain, threshold, ratio, lp, hp, width, pan):
audio = AudioSegment.from_file(audio_file)
if preset_name != "custom":
params = PRESETS[preset_name]
else:
params = {"gain": gain, "compress_threshold": threshold, "compress_ratio": ratio,
"low_pass": lp, "high_pass": hp, "stereo_width": width, "pan": pan}
out = apply_mastering(audio, params)
buf = io.BytesIO()
out.export(buf, format="wav")
buf.seek(0)
preset = {"preset": preset_name, "params": params}
preset_buf = io.BytesIO(json.dumps(preset, indent=2).encode())
preset_buf.name = "preset" + EXPORT_EXT
return (buf, "processed.wav"), (preset_buf, preset_buf.name)
def load_preset_json(file_obj):
data = json.load(file_obj)
name = data.get("preset", "custom")
params = data.get("params", {})
return name, params.get("gain", 0), params.get("compress_threshold", 0), \
params.get("compress_ratio", 1), params.get("low_pass", 20000), \
params.get("high_pass", 20), params.get("stereo_width", 100), params.get("pan", 0)
with gr.Blocks(css=CUSTOM_CSS) as demo:
gr.Markdown("<div id='title'>Advanced Browser-based Audio Mastering Suite</div>")
with gr.Row():
with gr.Column():
# Use 'filepath' to get the file path for AudioSegment
audio_input = gr.Audio(label="Upload Audio", type="filepath")
preset_dropdown = gr.Dropdown(choices=list(PRESETS.keys()) + ["custom"], value="rock", label="Preset")
gain = gr.Slider(-10, 20, value=PRESETS["rock"]["gain"], label="Gain (dB)")
threshold = gr.Slider(-60, 0, value=PRESETS["rock"]["compress_threshold"], label="Compress Threshold (dB)")
ratio = gr.Slider(1, 10, value=PRESETS["rock"]["compress_ratio"], label="Compress Ratio")
lp = gr.Slider(1000, 20000, value=PRESETS["rock"]["low_pass"], label="Low-pass Frequency (Hz)")
hp = gr.Slider(20, 500, value=PRESETS["rock"]["high_pass"], label="High-pass Frequency (Hz)")
width = gr.Slider(0, 200, value=PRESETS["rock"]["stereo_width"], label="Stereo Width (%)")
pan = gr.Slider(-100, 100, value=PRESETS["rock"]["pan"], label="Pan (-100 Left to 100 Right)")
load_preset = gr.File(label="Load .master Preset", file_types=[".master"])
export_button = gr.Button("Process & Export")
with gr.Column():
# Set output to filepath to serve downloadable file
output_audio = gr.Audio(label="Processed Audio", type="filepath")
export_preset_file = gr.File(label="Download Preset (.master)")
def sync_sliders(preset):
if preset != "custom":
p = PRESETS[preset]
return (gr.update(value=p["gain"]), gr.update(value=p["compress_threshold"]), \
gr.update(value=p["compress_ratio"]), gr.update(value=p["low_pass"]), \
gr.update(value=p["high_pass"]), gr.update(value=p["stereo_width"]), \
gr.update(value=p["pan"]))
return None
preset_dropdown.change(sync_sliders, inputs=[preset_dropdown], outputs=[gain, threshold, ratio, lp, hp, width, pan])
load_preset.upload(load_preset_json, inputs=[load_preset], outputs=[preset_dropdown, gain, threshold, ratio, lp, hp, width, pan])
export_button.click(process_and_export, inputs=[audio_input, preset_dropdown, gain, threshold, ratio, lp, hp, width, pan], outputs=[output_audio, export_preset_file])
if __name__ == "__main__":
demo.launch()