File size: 7,038 Bytes
d28ce35
64638ca
 
 
 
e89501c
64638ca
 
e13f695
9a89e2a
 
 
997cf76
 
64638ca
 
 
 
997cf76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53ce9f0
 
 
 
 
 
 
 
 
64638ca
9a89e2a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64638ca
 
 
e13f695
64638ca
 
 
 
9a89e2a
e13f695
53ce9f0
e13f695
2e960ad
 
 
 
 
 
 
 
 
 
 
 
 
e13f695
e89501c
53ce9f0
64638ca
e13f695
 
64638ca
 
 
e89501c
64638ca
9a89e2a
 
 
 
 
 
 
 
 
64638ca
9a89e2a
 
 
 
64638ca
 
 
 
 
 
 
 
e13f695
64638ca
 
e13f695
 
 
53ce9f0
9a89e2a
 
 
 
 
64638ca
 
 
 
 
 
 
 
 
 
 
55c1452
9a89e2a
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
import sys
import os
import tempfile
import soundfile as sf
import torch
from audiosr import build_model, super_resolution_long_audio
from pyharp import ModelCard, build_endpoint
import gradio as gr
import random
import numpy as np
import librosa
from scipy import signal
import audiosr.pipeline as _audiosr_pipeline
from audiosr.utils import _locate_cutoff_freq, lowpass as _audiosr_lowpass

os.environ["TOKENIZERS_PARALLELISM"] = "true"
torch.set_float32_matmul_precision("high")

def _patched_lowpass_filtering_prepare_inference(dl_output):
    """
    Patches a boundary bug in audiosr==0.0.7: cutoff_freq caps at exactly
    24000 Hz (both via normal detection saturating there, and via the
    "give up" fallback for quiet/narrowband audio). sampling_rate for this
    step is hardcoded to 48000 upstream, so nyquist == 24000 exactly,
    producing Wn == cutoff_freq / nyquist == 1.0 for a wide range of real
    inputs. scipy's iirfilter requires 0 < Wn < 1 strictly. Clamped with a
    small safety margin below Nyquist instead of patching the pip package.
    """
    waveform = dl_output["waveform"]
    sampling_rate = dl_output["sampling_rate"]

    cutoff_freq = (_locate_cutoff_freq(dl_output["stft"], percentile=0.985) / 1024) * 24000
    if cutoff_freq < 1000:
        cutoff_freq = 24000

    nyquist = 0.5 * sampling_rate
    if cutoff_freq >= nyquist:
        cutoff_freq = nyquist * 0.98  # safety margin

    order = 8
    ftype = np.random.choice(["butter", "cheby1", "ellip", "bessel"])
    filtered_audio = _audiosr_lowpass(
        waveform.numpy().squeeze(), highcut=cutoff_freq, fs=sampling_rate, order=order, _type=ftype,
    )
    filtered_audio = torch.FloatTensor(filtered_audio.copy()).unsqueeze(0)

    if waveform.size(-1) <= filtered_audio.size(-1):
        filtered_audio = filtered_audio[..., : waveform.size(-1)]
    else:
        filtered_audio = torch.nn.functional.pad(filtered_audio, (0, waveform.size(-1) - filtered_audio.size(-1)))

    return {"waveform_lowpass": filtered_audio}

_audiosr_pipeline.lowpass_filtering_prepare_inference = _patched_lowpass_filtering_prepare_inference

model = None

def get_model():
    global model
    if model is None:
        print("Loading AudioSR model...", flush=True)
        model = build_model(model_name="basic", device="auto")
        print("Model loaded.", flush=True)
    return model

def match_array_shapes(array_1: np.ndarray, array_2: np.ndarray):
    if (len(array_1.shape) == 1) & (len(array_2.shape) == 1):
        if array_1.shape[0] > array_2.shape[0]:
            array_1 = array_1[:array_2.shape[0]]
        elif array_1.shape[0] < array_2.shape[0]:
            array_1 = np.pad(array_1, ((array_2.shape[0] - array_1.shape[0], 0)), 'constant', constant_values=0)
    else:
        if array_1.shape[1] > array_2.shape[1]:
            array_1 = array_1[:,:array_2.shape[1]]
        elif array_1.shape[1] < array_2.shape[1]:
            padding = array_2.shape[1] - array_1.shape[1]
            array_1 = np.pad(array_1, ((0,0), (0,padding)), 'constant', constant_values=0)
    return array_1

def lr_filter(audio, cutoff, filter_type, order=12, sr=48000):
    audio = audio.T
    nyquist = 0.5 * sr
    normal_cutoff = cutoff / nyquist
    b, a = signal.butter(order//2, normal_cutoff, btype=filter_type, analog=False)
    sos = signal.tf2sos(b, a)
    filtered_audio = signal.sosfiltfilt(sos, audio)
    return filtered_audio.T

model_card = ModelCard(
    name="AudioSR",
    description="Upsample any audio to 48kHz using audio super-resolution.",
    author="Haohe Liu, Ke Chen, Qiao Tian, Wenwu Wang, Mark D. Plumbley",
    tags=["audio", "super-resolution", "upsampling", "48kHz"],
)

@torch.inference_mode()
def process_fn(input_audio_path: str, ddim_steps: int, guidance_scale: float, seed: str, multiband_ensemble: bool, input_cutoff: int) -> str:
    try:
        seed_val = int(seed) if seed and seed.strip() not in ("0", "") else random.randint(1, 2**32 - 1)
    except (TypeError, ValueError):
        seed_val = random.randint(1, 2**32 - 1) 

    # AudioSR's internal cutoff-frequency detection (lowpass_filtering_prepare_inference)
    # hardcodes a 24kHz-Nyquist assumption regardless of actual input sample rate,
    # which can produce an invalid filter Wn and crash on input below 48kHz.
    # Resample up front so that assumption always holds.
    orig_sr = sf.info(input_audio_path).samplerate
    if orig_sr < 48000:
        y, _ = librosa.load(input_audio_path, sr=None, mono=False)
        y = librosa.resample(y, orig_sr=orig_sr, target_sr=48000)
        resampled_path = tempfile.mktemp(suffix=".wav")
        sf.write(resampled_path, y.T if y.ndim > 1 else y, samplerate=48000)
        input_audio_path = resampled_path

    waveform = super_resolution_long_audio(
        get_model(),
        input_audio_path,
        seed=seed_val,
        guidance_scale=float(guidance_scale),
        ddim_steps=int(ddim_steps),
    )

    output = waveform.cpu().numpy()

    if multiband_ensemble:
        crossover_freq = int(input_cutoff) - 1000
        low, _ = librosa.load(input_audio_path, sr=48000, mono=True)
        out = output.squeeze() if output.ndim > 1 else output
        out = match_array_shapes(out, low)
        low = lr_filter(low, crossover_freq, 'lowpass', order=10)
        high = lr_filter(out, crossover_freq, 'highpass', order=10)
        high = lr_filter(high, 23000, 'lowpass', order=2)
        output = low + high
    else:
        if output.shape[0] == 1:
            output = output.squeeze(0)
        else:
            output = output.T

    output_path = tempfile.mktemp(suffix=".wav")
    sf.write(output_path, output, samplerate=48000)
    return output_path

with gr.Blocks() as demo:
    input_components = [
        gr.Audio(type="filepath", label="Input Audio").harp_required(True),
        gr.Slider(minimum=10, maximum=500, step=10, value=50,
                  label="DDIM Steps",
                  info="More steps = better quality but slower"),
        gr.Slider(minimum=1.0, maximum=20.0, step=0.5, value=3.5,
                  label="Guidance Scale",
                  info="Higher values follow the conditioning more closely"),
        gr.Textbox(value="0", label="Seed", info="0 = random seed"),
        gr.Checkbox(value=False, label="Multiband Ensemble",
                    info="Blend original low frequencies with upsampled highs"),
        gr.Slider(minimum=4000, maximum=20000, step=1000, value=12000,
                  label="Input Cutoff (Hz)",
                  info="Crossover frequency for multiband ensemble"),
    ]
    output_components = [
        gr.Audio(type="filepath", label="Output Audio (48kHz)").set_info("Audio upsampled to 48kHz."),
    ]
    app = build_endpoint(
        model_card=model_card,
        input_components=input_components,
        output_components=output_components,
        process_fn=process_fn,
    )

print("Launching Gradio...", flush=True)
demo.queue().launch(server_name="0.0.0.0", server_port=7860, show_error=True, pwa=True)