File size: 5,620 Bytes
25169f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Add reverb to an audio file using Pedalboard.

The audio file is read in chunks, using nearly no memory.
This should be one of the fastest possible ways to add reverb to a file
while also using as little memory as possible.

On my laptop, this runs about 58x faster than real-time
(i.e.: processes a 60-second file in ~1 second.)

Requirements: `pip install PySoundFile tqdm pedalboard`
Note that PySoundFile requires a working libsndfile installation.
"""

import argparse
import os
import sys
import warnings

import numpy as np
import soundfile as sf
from tqdm import tqdm
from tqdm.std import TqdmWarning

from pedalboard import Reverb

BUFFER_SIZE_SAMPLES = 1024 * 16
NOISE_FLOOR = 1e-4


def get_num_frames(f: sf.SoundFile) -> int:
    # On some platforms and formats, f.frames == -1L.
    # Check for this bug and work around it:
    if f.frames > 2 ** 32:
        f.seek(0)
        last_position = f.tell()
        while True:
            # Seek through the file in chunks, returning
            # if the file pointer stops advancing.
            f.seek(1024 * 1024 * 1024, sf.SEEK_CUR)
            new_position = f.tell()
            if new_position == last_position:
                f.seek(0)
                return new_position
            else:
                last_position = new_position
    else:
        return f.frames


def main():
    warnings.filterwarnings("ignore", category=TqdmWarning)

    parser = argparse.ArgumentParser(description="Add reverb to an audio file.")
    parser.add_argument("input_file", help="The input file to add reverb to.")
    parser.add_argument(
        "--output-file",
        help=(
            "The name of the output file to write to. If not provided, {input_file}.reverb.wav will"
            " be used."
        ),
        default=None,
    )

    # Instantiate the Reverb object early so we can read its defaults for the argparser --help:
    reverb = Reverb()

    parser.add_argument("--room-size", type=float, default=reverb.room_size)
    parser.add_argument("--damping", type=float, default=reverb.damping)
    parser.add_argument("--wet-level", type=float, default=reverb.wet_level)
    parser.add_argument("--dry-level", type=float, default=reverb.dry_level)
    parser.add_argument("--width", type=float, default=reverb.width)
    parser.add_argument("--freeze-mode", type=float, default=reverb.freeze_mode)

    parser.add_argument(
        "-y",
        "--overwrite",
        action="store_true",
        help="If passed, overwrite the output file if it already exists.",
    )

    parser.add_argument(
        "--cut-reverb-tail",
        action="store_true",
        help=(
            "If passed, remove the reverb tail to the end of the file. "
            "The output file will be identical in length to the input file."
        ),
    )
    args = parser.parse_args()

    for arg in ('room_size', 'damping', 'wet_level', 'dry_level', 'width', 'freeze_mode'):
        setattr(reverb, arg, getattr(args, arg))

    if not args.output_file:
        args.output_file = args.input_file + ".reverb.wav"

    sys.stderr.write(f"Opening {args.input_file}...\n")

    with sf.SoundFile(args.input_file) as input_file:
        sys.stderr.write(f"Writing to {args.output_file}...\n")
        if os.path.isfile(args.output_file) and not args.overwrite:
            raise ValueError(
                f"Output file {args.output_file} already exists! (Pass -y to overwrite.)"
            )
        with sf.SoundFile(
            args.output_file,
            'w',
            samplerate=input_file.samplerate,
            channels=input_file.channels,
        ) as output_file:
            length = get_num_frames(input_file)
            length_seconds = length / input_file.samplerate
            sys.stderr.write(f"Adding reverb to {length_seconds:.2f} seconds of audio...\n")
            with tqdm(
                total=length_seconds,
                desc="Adding reverb...",
                bar_format=(
                    "{percentage:.0f}%|{bar}| {n:.2f}/{total:.2f} seconds processed"
                    " [{elapsed}<{remaining}, {rate:.2f}x]"
                ),
                # Avoid a formatting error that occurs if
                # TQDM tries to print before we've processed a block
                delay=1000,
            ) as t:
                for dry_chunk in input_file.blocks(BUFFER_SIZE_SAMPLES, frames=length):
                    # Actually call Pedalboard here:
                    # (reset=False is necessary to allow the reverb tail to
                    # continue from one chunk to the next.)
                    effected_chunk = reverb.process(
                        dry_chunk, sample_rate=input_file.samplerate, reset=False
                    )
                    # print(effected_chunk.shape, np.amax(np.abs(effected_chunk)))
                    output_file.write(effected_chunk)
                    t.update(len(dry_chunk) / input_file.samplerate)
                    t.refresh()
            if not args.cut_reverb_tail:
                while True:
                    # Pull audio from the effect until there's nothing left:
                    effected_chunk = reverb.process(
                        np.zeros((BUFFER_SIZE_SAMPLES, input_file.channels), np.float32),
                        sample_rate=input_file.samplerate,
                        reset=False,
                    )
                    if np.amax(np.abs(effected_chunk)) < NOISE_FLOOR:
                        break
                    output_file.write(effected_chunk)
    sys.stderr.write("Done!\n")


if __name__ == "__main__":
    main()