File size: 5,496 Bytes
e0e580e
 
 
9c8283e
e0e580e
 
 
df33f32
e0e580e
 
df33f32
e0e580e
 
 
 
 
aee65c3
fb99548
04bf897
df33f32
 
 
04bf897
fb99548
eabe107
aeeb010
e0e580e
 
df33f32
 
3089692
df33f32
e0e580e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
df33f32
 
 
e0e580e
df33f32
 
 
 
 
 
 
 
 
e0e580e
df33f32
e0e580e
 
 
 
 
 
 
 
 
 
 
df33f32
 
 
 
 
e0e580e
df33f32
 
 
 
 
e0e580e
 
df33f32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e0e580e
df33f32
 
 
 
 
 
 
 
 
e0e580e
df33f32
 
 
 
 
 
e0e580e
df33f32
 
 
 
 
 
e0e580e
df33f32
 
 
e0e580e
245156a
 
 
 
 
 
 
 
 
028b789
245156a
 
 
 
2404181
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
from audiotools import AudioSignal
from pyharp.core import ModelCard, build_endpoint
from pyharp.media.audio import load_audio, save_audio
from pyharp.tags import Subcategory

import gradio as gr
import librosa
import soundfile as sf
import torch

import tempfile
from pathlib import Path

# ModelCard

model_card = ModelCard(
    name="Harmonic-Percussive Source Separation",
    description=(
        "Splits a track into its harmonic and percussive components and remixes them at an "
        "adjustable balance, using librosa's harmonic-percussive source separation. Each channel "
        "is separated on its own, at the rate of the input, and a remix that would exceed full "
        "scale is turned down to fit rather than clipped. Repository: "
        "https://github.com/librosa/librosa"
    ),
    author="Brian McFee, librosa developers",
    tags=[Subcategory.STEM_SEPARATION, 'example', 'domain:music']
)

# A lossless input keeps its container. A lossy one comes back as WAV, since encoding the result
# lossily again would expose the artifacts of the first encode and add a second set.
LOSSLESS = {".wav", ".flac"}


# Model Logic
def hpss(signal: AudioSignal, **kwargs):
    h, p = librosa.effects.hpss(signal.audio_data.squeeze().numpy(), **kwargs)

    if h.ndim == 1:
        h = h[None, None, :]
        p = p[None, None, :]
    elif h.ndim == 2:
        h = h[None, :, :]
        p = p[None, :, :]
    else:
        raise ValueError("Unexpected audio shape")

    harmonic_signal = signal.clone()
    harmonic_signal.audio_data = torch.from_numpy(h)

    percussive_signal = signal.clone()
    percussive_signal.audio_data = torch.from_numpy(p)

    return harmonic_signal, percussive_signal

MIN_DB = -120

def process_fn(audio_file_path,
               harmonic_db: float,
               percussive_db: float,
               kernel_size: int = 31,
               margin: float = 1.0):
    if not audio_file_path:
        raise gr.Error("Provide the audio to split.")
    try:
        info = sf.info(audio_file_path)
    except Exception as exc:
        raise gr.Error(f"Could not read the audio file: {exc}") from exc
    if info.frames == 0:
        raise gr.Error("The audio file is empty.")

    sig = load_audio(audio_file_path)

    harmonic, percussive = hpss(sig, kernel_size=int(kernel_size), margin=margin)

    def clip(db):
        return -float("inf") if db == MIN_DB else db

    # Mix signals with gain
    output_sig = (
        harmonic.volume_change(clip(harmonic_db)) +
        percussive.volume_change(clip(percussive_db))
    )

    # Boosting either part can carry the remix past full scale, which the file writer would clip,
    # so the whole remix is turned down instead, keeping the requested balance
    peak = output_sig.audio_data.abs().max().item()
    if peak > 1.0:
        output_sig.audio_data = output_sig.audio_data / peak

    input_ext = Path(audio_file_path).suffix.lower()
    output_ext = input_ext if input_ext in LOSSLESS else ".wav"
    output_audio_path = str(Path(tempfile.mkdtemp()) / f"{Path(audio_file_path).stem}_hpss{output_ext}")
    save_audio(output_sig, output_audio_path)
    return output_audio_path


# Launch the App
if __name__ == "__main__":
    with gr.Blocks() as demo:
        input_audio = gr.Audio(type="filepath", label="Input Audio").harp_required(True).set_info((
            "The audio to split. Harmonic content is the sustained, pitched "
            "part and percussive content is the transient part."
        ))

        harmonic_slider = gr.Slider(
            minimum=MIN_DB, maximum=24,
            step=1, value=0,
            label="Harmonic Level (dB)",
            info=(
                "Boost or reduce tonal parts like chords and pads. A boost that would push the "
                "remix past full scale turns the whole remix down instead."
            )
        )

        percussive_slider = gr.Slider(
            minimum=MIN_DB, maximum=24,
            step=1, value=0,
            label="Percussive Level (dB)",
            info=(
                "Control the loudness of drums and other rhythmic hits. A boost that would push "
                "the remix past full scale turns the whole remix down instead."
            )
        )

        kernel_slider = gr.Slider(
            minimum=1, maximum=101,
            step=1, value=31,
            label="Time Resolution",
            info="How much time/spectral context is used to classify energy as harmonic or percussive. \n Lower = more detailed, reacts faster to rhythm, higher = smoother and slower"
        )

        margin_slider = gr.Slider(
            minimum=0.5, maximum=5.0,
            step=0.1, value=1.0,
            label="Separation Strength",
            info="How strongly harmonic & percussive components must differ to be separated. \n Higher = cleaner isolation, lower = more natural blend."
        )

        output_audio = gr.Audio(type="filepath", label="Remixed Audio").set_info((
            "The harmonic and percussive parts, remixed by the balance control."
        ))

        endpoint = build_endpoint(
            model_card=model_card,
            input_components=[
                input_audio,
                harmonic_slider,
                percussive_slider,
                kernel_slider,
                margin_slider
            ],
            output_components=[output_audio],
            process_fn=process_fn
        )

    demo.queue()
    demo.launch(pwa=True, show_error=True)