File size: 5,898 Bytes
ba12afc
 
6fa67f8
 
ba12afc
 
 
 
 
 
 
1c8dc1c
ca9ab30
ba12afc
733cd52
ba12afc
 
 
 
 
6fa67f8
 
876fe7f
6fa67f8
ba12afc
 
 
d9eb128
 
 
 
 
 
 
ba12afc
 
29481bb
ba12afc
 
 
76450af
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6fa67f8
 
 
 
 
 
 
 
 
ba12afc
 
 
 
 
6fa67f8
 
ba12afc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6fa67f8
ba12afc
 
 
6fa67f8
ba12afc
 
6fa67f8
ba12afc
 
76450af
ba12afc
 
733cd52
 
 
 
 
 
9daa766
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ba12afc
 
98a1280
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
from __future__ import annotations

import spaces  # imported first, as ZeroGPU requires

import uuid
from pathlib import Path
from tempfile import gettempdir

import gradio as gr
import soundfile as sf

from pyharp import ModelCard, build_endpoint, SampleRate, Channels
from pyharp.tags import Subcategory

from microtcn_runtime import load_model, process_audio

MIN_AUDIO_SECONDS = 0.5
MAX_AUDIO_SECONDS = 30
OUTPUT_ROOT = Path(gettempdir()) / "microtcn_outputs"

# 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_card = ModelCard(
    name="micro-TCN",
    description=(
        "Emulate a Teletronix LA-2A optical compressor/limiter using a "
        "lightweight temporal convolutional network. The model is fixed-rate "
        "and mono, so the result is resampled back to the input's rate and "
        "copied across its channels: the output has the format it came in with, "
        "but no stereo image. Repository: "
        "https://github.com/csteinmetz1/micro-tcn Paper: "
        "https://arxiv.org/abs/2102.06200"
    ),
    author="Christian J. Steinmetz & Joshua D. Reiss",
    tags=[Subcategory.NEURAL_ANALOG_EFFECTS, SampleRate(44100), Channels(1), 'compressor', 'limiter', 'analog-emulation', 'domain:music'],
)


def match_input_format(output_path: str, input_path: str) -> str:
    """
    Rewrite an output file at the sample rate and channel count it came in with.

    The models here are fixed-rate and mono, and hand the writing to vendored
    upstream code, so the format is corrected afterwards rather than threaded
    through inference. Duplicated channels are copies, not a recovered stereo
    image; the model card says so.

    Args:
        output_path (str): File the model wrote. Overwritten in place.
        input_path (str): File the request supplied.

    Returns:
        output_path (str): The same path, for use in a return statement.
    """

    import librosa
    import numpy as np
    import soundfile

    want = soundfile.info(input_path)
    have = soundfile.info(output_path)

    if have.samplerate == want.samplerate and have.channels == want.channels:
        return output_path

    data, rate = soundfile.read(output_path, always_2d=True)

    if rate != want.samplerate:
        data = librosa.resample(
            data.T, orig_sr=rate, target_sr=want.samplerate
        ).T

    if data.shape[1] == 1 and want.channels > 1:
        data = np.tile(data, (1, want.channels))
    elif data.shape[1] > 1 and want.channels == 1:
        data = data.mean(axis=1, keepdims=True)

    soundfile.write(output_path, data, want.samplerate)

    return output_path


@spaces.GPU(duration=120)
def compress(audio_path: str, limit_mode: float, peak_reduction: float):
    return process_audio(
        audio_path=audio_path,
        limit_mode=limit_mode,
        peak_reduction=peak_reduction,
    )


def process_fn(
    audio_path: str | None,
    mode: str,
    peak_reduction: float,
) -> str:
    # Checked before the GPU is reserved, so a request the model cannot take spends none of the
    # caller's ZeroGPU allowance
    if not audio_path:
        raise gr.Error("Please upload an audio file.")

    try:
        duration = sf.info(audio_path).duration
    except Exception as exc:
        raise gr.Error(f"Could not read the audio file: {exc}") from exc

    if duration < MIN_AUDIO_SECONDS:
        raise gr.Error("Audio must be at least 0.5 seconds long.")

    if duration > MAX_AUDIO_SECONDS:
        raise gr.Error(
            f"Audio must be no longer than {MAX_AUDIO_SECONDS} seconds. "
            f"Received {duration:.1f} seconds."
        )

    limit_mode = 1.0 if mode == "Limit" else 0.0

    try:
        audio, sample_rate = compress(audio_path, limit_mode, peak_reduction)
    except Exception as exc:
        raise gr.Error(f"micro-TCN inference failed: {exc}") from exc

    input_ext = Path(audio_path).suffix.lower()
    output_dir = OUTPUT_ROOT / uuid.uuid4().hex
    output_dir.mkdir(parents=True, exist_ok=True)
    output_path = output_dir / f"microtcn_output{input_ext if input_ext in LOSSLESS else '.wav'}"
    sf.write(str(output_path), audio, sample_rate)

    return match_input_format(str(output_path), audio_path)


# pyharp runs process_fn in a worker, which imports this file as __mp_main__ and starts with the
# server, so the model is loaded there, before the first request rather than during it.
if __name__ != "__main__":
    load_model()


if __name__ == "__main__":
    with gr.Blocks(title="micro-TCN LA-2A Emulation") as demo:
        input_components = [
            gr.Audio(type="filepath", label="Input Audio")
            .harp_required(True)
            .set_info("Audio clip to process, up to 30 seconds."),
            gr.Dropdown(
                choices=["Compress", "Limit"],
                value="Compress",
                label="Mode",
            ).set_info(
                "Compress for gentle gain reduction, Limit for harder peak control."
            ),
            gr.Slider(
                minimum=0.0,
                maximum=1.0,
                value=0.42,
                step=0.01,
                label="Peak Reduction",
            ).set_info("Normalized peak-reduction control, from 0 to 1."),
        ]
        output_components = [
            gr.Audio(
                type="filepath",
                label="Processed Audio",
            ).set_info("Audio processed through the LA-2A emulation."),
        ]
        build_endpoint(
            model_card=model_card,
            input_components=input_components,
            output_components=output_components,
            process_fn=process_fn,
        )


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