OpenSound commited on
Commit
71de706
1 Parent(s): 306c48e

Upload 33 files

Browse files
audiotools/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ __version__ = "0.7.3"
2
+ from .core import AudioSignal
3
+ from .core import STFTParams
4
+ from .core import Meter
5
+ from .core import util
6
+ from . import metrics
7
+ from . import data
8
+ from . import ml
9
+ from .data import datasets
10
+ from .data import transforms
audiotools/core/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from . import util
2
+ from .audio_signal import AudioSignal
3
+ from .audio_signal import STFTParams
4
+ from .loudness import Meter
audiotools/core/audio_signal.py ADDED
@@ -0,0 +1,1682 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ import functools
3
+ import hashlib
4
+ import math
5
+ import pathlib
6
+ import tempfile
7
+ import typing
8
+ import warnings
9
+ from collections import namedtuple
10
+ from pathlib import Path
11
+
12
+ import julius
13
+ import numpy as np
14
+ import soundfile
15
+ import torch
16
+
17
+ from . import util
18
+ from .display import DisplayMixin
19
+ from .dsp import DSPMixin
20
+ from .effects import EffectMixin
21
+ from .effects import ImpulseResponseMixin
22
+ from .ffmpeg import FFMPEGMixin
23
+ from .loudness import LoudnessMixin
24
+ from .playback import PlayMixin
25
+ from .whisper import WhisperMixin
26
+
27
+
28
+ STFTParams = namedtuple(
29
+ "STFTParams",
30
+ ["window_length", "hop_length", "window_type", "match_stride", "padding_type"],
31
+ )
32
+ """
33
+ STFTParams object is a container that holds STFT parameters - window_length,
34
+ hop_length, and window_type. Not all parameters need to be specified. Ones that
35
+ are not specified will be inferred by the AudioSignal parameters.
36
+
37
+ Parameters
38
+ ----------
39
+ window_length : int, optional
40
+ Window length of STFT, by default ``0.032 * self.sample_rate``.
41
+ hop_length : int, optional
42
+ Hop length of STFT, by default ``window_length // 4``.
43
+ window_type : str, optional
44
+ Type of window to use, by default ``sqrt\_hann``.
45
+ match_stride : bool, optional
46
+ Whether to match the stride of convolutional layers, by default False
47
+ padding_type : str, optional
48
+ Type of padding to use, by default 'reflect'
49
+ """
50
+ STFTParams.__new__.__defaults__ = (None, None, None, None, None)
51
+
52
+
53
+ class AudioSignal(
54
+ EffectMixin,
55
+ LoudnessMixin,
56
+ PlayMixin,
57
+ ImpulseResponseMixin,
58
+ DSPMixin,
59
+ DisplayMixin,
60
+ FFMPEGMixin,
61
+ WhisperMixin,
62
+ ):
63
+ """This is the core object of this library. Audio is always
64
+ loaded into an AudioSignal, which then enables all the features
65
+ of this library, including audio augmentations, I/O, playback,
66
+ and more.
67
+
68
+ The structure of this object is that the base functionality
69
+ is defined in ``core/audio_signal.py``, while extensions to
70
+ that functionality are defined in the other ``core/*.py``
71
+ files. For example, all the display-based functionality
72
+ (e.g. plot spectrograms, waveforms, write to tensorboard)
73
+ are in ``core/display.py``.
74
+
75
+ Parameters
76
+ ----------
77
+ audio_path_or_array : typing.Union[torch.Tensor, str, Path, np.ndarray]
78
+ Object to create AudioSignal from. Can be a tensor, numpy array,
79
+ or a path to a file. The file is always reshaped to
80
+ sample_rate : int, optional
81
+ Sample rate of the audio. If different from underlying file, resampling is
82
+ performed. If passing in an array or tensor, this must be defined,
83
+ by default None
84
+ stft_params : STFTParams, optional
85
+ Parameters of STFT to use. , by default None
86
+ offset : float, optional
87
+ Offset in seconds to read from file, by default 0
88
+ duration : float, optional
89
+ Duration in seconds to read from file, by default None
90
+ device : str, optional
91
+ Device to load audio onto, by default None
92
+
93
+ Examples
94
+ --------
95
+ Loading an AudioSignal from an array, at a sample rate of
96
+ 44100.
97
+
98
+ >>> signal = AudioSignal(torch.randn(5*44100), 44100)
99
+
100
+ Note, the signal is reshaped to have a batch size, and one
101
+ audio channel:
102
+
103
+ >>> print(signal.shape)
104
+ (1, 1, 44100)
105
+
106
+ You can treat AudioSignals like tensors, and many of the same
107
+ functions you might use on tensors are defined for AudioSignals
108
+ as well:
109
+
110
+ >>> signal.to("cuda")
111
+ >>> signal.cuda()
112
+ >>> signal.clone()
113
+ >>> signal.detach()
114
+
115
+ Indexing AudioSignals returns an AudioSignal:
116
+
117
+ >>> signal[..., 3*44100:4*44100]
118
+
119
+ The above signal is 1 second long, and is also an AudioSignal.
120
+ """
121
+
122
+ def __init__(
123
+ self,
124
+ audio_path_or_array: typing.Union[torch.Tensor, str, Path, np.ndarray],
125
+ sample_rate: int = None,
126
+ stft_params: STFTParams = None,
127
+ offset: float = 0,
128
+ duration: float = None,
129
+ device: str = None,
130
+ ):
131
+ audio_path = None
132
+ audio_array = None
133
+
134
+ if isinstance(audio_path_or_array, str):
135
+ audio_path = audio_path_or_array
136
+ elif isinstance(audio_path_or_array, pathlib.Path):
137
+ audio_path = audio_path_or_array
138
+ elif isinstance(audio_path_or_array, np.ndarray):
139
+ audio_array = audio_path_or_array
140
+ elif torch.is_tensor(audio_path_or_array):
141
+ audio_array = audio_path_or_array
142
+ else:
143
+ raise ValueError(
144
+ "audio_path_or_array must be either a Path, "
145
+ "string, numpy array, or torch Tensor!"
146
+ )
147
+
148
+ self.path_to_file = None
149
+
150
+ self.audio_data = None
151
+ self.sources = None # List of AudioSignal objects.
152
+ self.stft_data = None
153
+ if audio_path is not None:
154
+ self.load_from_file(
155
+ audio_path, offset=offset, duration=duration, device=device
156
+ )
157
+ elif audio_array is not None:
158
+ assert sample_rate is not None, "Must set sample rate!"
159
+ self.load_from_array(audio_array, sample_rate, device=device)
160
+
161
+ self.window = None
162
+ self.stft_params = stft_params
163
+
164
+ self.metadata = {
165
+ "offset": offset,
166
+ "duration": duration,
167
+ }
168
+
169
+ @property
170
+ def path_to_input_file(
171
+ self,
172
+ ):
173
+ """
174
+ Path to input file, if it exists.
175
+ Alias to ``path_to_file`` for backwards compatibility
176
+ """
177
+ return self.path_to_file
178
+
179
+ @classmethod
180
+ def excerpt(
181
+ cls,
182
+ audio_path: typing.Union[str, Path],
183
+ offset: float = None,
184
+ duration: float = None,
185
+ state: typing.Union[np.random.RandomState, int] = None,
186
+ **kwargs,
187
+ ):
188
+ """Randomly draw an excerpt of ``duration`` seconds from an
189
+ audio file specified at ``audio_path``, between ``offset`` seconds
190
+ and end of file. ``state`` can be used to seed the random draw.
191
+
192
+ Parameters
193
+ ----------
194
+ audio_path : typing.Union[str, Path]
195
+ Path to audio file to grab excerpt from.
196
+ offset : float, optional
197
+ Lower bound for the start time, in seconds drawn from
198
+ the file, by default None.
199
+ duration : float, optional
200
+ Duration of excerpt, in seconds, by default None
201
+ state : typing.Union[np.random.RandomState, int], optional
202
+ RandomState or seed of random state, by default None
203
+
204
+ Returns
205
+ -------
206
+ AudioSignal
207
+ AudioSignal containing excerpt.
208
+
209
+ Examples
210
+ --------
211
+ >>> signal = AudioSignal.excerpt("path/to/audio", duration=5)
212
+ """
213
+ info = util.info(audio_path)
214
+ total_duration = info.duration
215
+
216
+ state = util.random_state(state)
217
+ lower_bound = 0 if offset is None else offset
218
+ upper_bound = max(total_duration - duration, 0)
219
+ offset = state.uniform(lower_bound, upper_bound)
220
+
221
+ signal = cls(audio_path, offset=offset, duration=duration, **kwargs)
222
+ signal.metadata["offset"] = offset
223
+ signal.metadata["duration"] = duration
224
+
225
+ return signal
226
+
227
+ @classmethod
228
+ def salient_excerpt(
229
+ cls,
230
+ audio_path: typing.Union[str, Path],
231
+ loudness_cutoff: float = None,
232
+ num_tries: int = 8,
233
+ state: typing.Union[np.random.RandomState, int] = None,
234
+ **kwargs,
235
+ ):
236
+ """Similar to AudioSignal.excerpt, except it extracts excerpts only
237
+ if they are above a specified loudness threshold, which is computed via
238
+ a fast LUFS routine.
239
+
240
+ Parameters
241
+ ----------
242
+ audio_path : typing.Union[str, Path]
243
+ Path to audio file to grab excerpt from.
244
+ loudness_cutoff : float, optional
245
+ Loudness threshold in dB. Typical values are ``-40, -60``,
246
+ etc, by default None
247
+ num_tries : int, optional
248
+ Number of tries to grab an excerpt above the threshold
249
+ before giving up, by default 8.
250
+ state : typing.Union[np.random.RandomState, int], optional
251
+ RandomState or seed of random state, by default None
252
+ kwargs : dict
253
+ Keyword arguments to AudioSignal.excerpt
254
+
255
+ Returns
256
+ -------
257
+ AudioSignal
258
+ AudioSignal containing excerpt.
259
+
260
+
261
+ .. warning::
262
+ if ``num_tries`` is set to None, ``salient_excerpt`` may try forever, which can
263
+ result in an infinite loop if ``audio_path`` does not have
264
+ any loud enough excerpts.
265
+
266
+ Examples
267
+ --------
268
+ >>> signal = AudioSignal.salient_excerpt(
269
+ "path/to/audio",
270
+ loudness_cutoff=-40,
271
+ duration=5
272
+ )
273
+ """
274
+ state = util.random_state(state)
275
+ if loudness_cutoff is None:
276
+ excerpt = cls.excerpt(audio_path, state=state, **kwargs)
277
+ else:
278
+ loudness = -np.inf
279
+ num_try = 0
280
+ while loudness <= loudness_cutoff:
281
+ excerpt = cls.excerpt(audio_path, state=state, **kwargs)
282
+ loudness = excerpt.loudness()
283
+ num_try += 1
284
+ if num_tries is not None and num_try >= num_tries:
285
+ break
286
+ return excerpt
287
+
288
+ @classmethod
289
+ def zeros(
290
+ cls,
291
+ duration: float,
292
+ sample_rate: int,
293
+ num_channels: int = 1,
294
+ batch_size: int = 1,
295
+ **kwargs,
296
+ ):
297
+ """Helper function create an AudioSignal of all zeros.
298
+
299
+ Parameters
300
+ ----------
301
+ duration : float
302
+ Duration of AudioSignal
303
+ sample_rate : int
304
+ Sample rate of AudioSignal
305
+ num_channels : int, optional
306
+ Number of channels, by default 1
307
+ batch_size : int, optional
308
+ Batch size, by default 1
309
+
310
+ Returns
311
+ -------
312
+ AudioSignal
313
+ AudioSignal containing all zeros.
314
+
315
+ Examples
316
+ --------
317
+ Generate 5 seconds of all zeros at a sample rate of 44100.
318
+
319
+ >>> signal = AudioSignal.zeros(5.0, 44100)
320
+ """
321
+ n_samples = int(duration * sample_rate)
322
+ return cls(
323
+ torch.zeros(batch_size, num_channels, n_samples), sample_rate, **kwargs
324
+ )
325
+
326
+ @classmethod
327
+ def wave(
328
+ cls,
329
+ frequency: float,
330
+ duration: float,
331
+ sample_rate: int,
332
+ num_channels: int = 1,
333
+ shape: str = "sine",
334
+ **kwargs,
335
+ ):
336
+ """
337
+ Generate a waveform of a given frequency and shape.
338
+
339
+ Parameters
340
+ ----------
341
+ frequency : float
342
+ Frequency of the waveform
343
+ duration : float
344
+ Duration of the waveform
345
+ sample_rate : int
346
+ Sample rate of the waveform
347
+ num_channels : int, optional
348
+ Number of channels, by default 1
349
+ shape : str, optional
350
+ Shape of the waveform, by default "saw"
351
+ One of "sawtooth", "square", "sine", "triangle"
352
+ kwargs : dict
353
+ Keyword arguments to AudioSignal
354
+ """
355
+ n_samples = int(duration * sample_rate)
356
+ t = torch.linspace(0, duration, n_samples)
357
+ if shape == "sawtooth":
358
+ from scipy.signal import sawtooth
359
+
360
+ wave_data = sawtooth(2 * np.pi * frequency * t, 0.5)
361
+ elif shape == "square":
362
+ from scipy.signal import square
363
+
364
+ wave_data = square(2 * np.pi * frequency * t)
365
+ elif shape == "sine":
366
+ wave_data = np.sin(2 * np.pi * frequency * t)
367
+ elif shape == "triangle":
368
+ from scipy.signal import sawtooth
369
+
370
+ # frequency is doubled by the abs call, so omit the 2 in 2pi
371
+ wave_data = sawtooth(np.pi * frequency * t, 0.5)
372
+ wave_data = -np.abs(wave_data) * 2 + 1
373
+ else:
374
+ raise ValueError(f"Invalid shape {shape}")
375
+
376
+ wave_data = torch.tensor(wave_data, dtype=torch.float32)
377
+ wave_data = wave_data.unsqueeze(0).unsqueeze(0).repeat(1, num_channels, 1)
378
+ return cls(wave_data, sample_rate, **kwargs)
379
+
380
+ @classmethod
381
+ def batch(
382
+ cls,
383
+ audio_signals: list,
384
+ pad_signals: bool = False,
385
+ truncate_signals: bool = False,
386
+ resample: bool = False,
387
+ dim: int = 0,
388
+ ):
389
+ """Creates a batched AudioSignal from a list of AudioSignals.
390
+
391
+ Parameters
392
+ ----------
393
+ audio_signals : list[AudioSignal]
394
+ List of AudioSignal objects
395
+ pad_signals : bool, optional
396
+ Whether to pad signals to length of the maximum length
397
+ AudioSignal in the list, by default False
398
+ truncate_signals : bool, optional
399
+ Whether to truncate signals to length of shortest length
400
+ AudioSignal in the list, by default False
401
+ resample : bool, optional
402
+ Whether to resample AudioSignal to the sample rate of
403
+ the first AudioSignal in the list, by default False
404
+ dim : int, optional
405
+ Dimension along which to batch the signals.
406
+
407
+ Returns
408
+ -------
409
+ AudioSignal
410
+ Batched AudioSignal.
411
+
412
+ Raises
413
+ ------
414
+ RuntimeError
415
+ If not all AudioSignals are the same sample rate, and
416
+ ``resample=False``, an error is raised.
417
+ RuntimeError
418
+ If not all AudioSignals are the same the length, and
419
+ both ``pad_signals=False`` and ``truncate_signals=False``,
420
+ an error is raised.
421
+
422
+ Examples
423
+ --------
424
+ Batching a bunch of random signals:
425
+
426
+ >>> signal_list = [AudioSignal(torch.randn(44100), 44100) for _ in range(10)]
427
+ >>> signal = AudioSignal.batch(signal_list)
428
+ >>> print(signal.shape)
429
+ (10, 1, 44100)
430
+
431
+ """
432
+ signal_lengths = [x.signal_length for x in audio_signals]
433
+ sample_rates = [x.sample_rate for x in audio_signals]
434
+
435
+ if len(set(sample_rates)) != 1:
436
+ if resample:
437
+ for x in audio_signals:
438
+ x.resample(sample_rates[0])
439
+ else:
440
+ raise RuntimeError(
441
+ f"Not all signals had the same sample rate! Got {sample_rates}. "
442
+ f"All signals must have the same sample rate, or resample must be True. "
443
+ )
444
+
445
+ if len(set(signal_lengths)) != 1:
446
+ if pad_signals:
447
+ max_length = max(signal_lengths)
448
+ for x in audio_signals:
449
+ pad_len = max_length - x.signal_length
450
+ x.zero_pad(0, pad_len)
451
+ elif truncate_signals:
452
+ min_length = min(signal_lengths)
453
+ for x in audio_signals:
454
+ x.truncate_samples(min_length)
455
+ else:
456
+ raise RuntimeError(
457
+ f"Not all signals had the same length! Got {signal_lengths}. "
458
+ f"All signals must be the same length, or pad_signals/truncate_signals "
459
+ f"must be True. "
460
+ )
461
+ # Concatenate along the specified dimension (default 0)
462
+ audio_data = torch.cat([x.audio_data for x in audio_signals], dim=dim)
463
+ audio_paths = [x.path_to_file for x in audio_signals]
464
+
465
+ batched_signal = cls(
466
+ audio_data,
467
+ sample_rate=audio_signals[0].sample_rate,
468
+ )
469
+ batched_signal.path_to_file = audio_paths
470
+ return batched_signal
471
+
472
+ # I/O
473
+ def load_from_file(
474
+ self,
475
+ audio_path: typing.Union[str, Path],
476
+ offset: float,
477
+ duration: float,
478
+ device: str = "cpu",
479
+ ):
480
+ """Loads data from file. Used internally when AudioSignal
481
+ is instantiated with a path to a file.
482
+
483
+ Parameters
484
+ ----------
485
+ audio_path : typing.Union[str, Path]
486
+ Path to file
487
+ offset : float
488
+ Offset in seconds
489
+ duration : float
490
+ Duration in seconds
491
+ device : str, optional
492
+ Device to put AudioSignal on, by default "cpu"
493
+
494
+ Returns
495
+ -------
496
+ AudioSignal
497
+ AudioSignal loaded from file
498
+ """
499
+ import librosa
500
+
501
+ data, sample_rate = librosa.load(
502
+ audio_path,
503
+ offset=offset,
504
+ duration=duration,
505
+ sr=None,
506
+ mono=False,
507
+ )
508
+ data = util.ensure_tensor(data)
509
+ if data.shape[-1] == 0:
510
+ raise RuntimeError(
511
+ f"Audio file {audio_path} with offset {offset} and duration {duration} is empty!"
512
+ )
513
+
514
+ if data.ndim < 2:
515
+ data = data.unsqueeze(0)
516
+ if data.ndim < 3:
517
+ data = data.unsqueeze(0)
518
+ self.audio_data = data
519
+
520
+ self.original_signal_length = self.signal_length
521
+
522
+ self.sample_rate = sample_rate
523
+ self.path_to_file = audio_path
524
+ return self.to(device)
525
+
526
+ def load_from_array(
527
+ self,
528
+ audio_array: typing.Union[torch.Tensor, np.ndarray],
529
+ sample_rate: int,
530
+ device: str = "cpu",
531
+ ):
532
+ """Loads data from array, reshaping it to be exactly 3
533
+ dimensions. Used internally when AudioSignal is called
534
+ with a tensor or an array.
535
+
536
+ Parameters
537
+ ----------
538
+ audio_array : typing.Union[torch.Tensor, np.ndarray]
539
+ Array/tensor of audio of samples.
540
+ sample_rate : int
541
+ Sample rate of audio
542
+ device : str, optional
543
+ Device to move audio onto, by default "cpu"
544
+
545
+ Returns
546
+ -------
547
+ AudioSignal
548
+ AudioSignal loaded from array
549
+ """
550
+ audio_data = util.ensure_tensor(audio_array)
551
+
552
+ if audio_data.dtype == torch.double:
553
+ audio_data = audio_data.float()
554
+
555
+ if audio_data.ndim < 2:
556
+ audio_data = audio_data.unsqueeze(0)
557
+ if audio_data.ndim < 3:
558
+ audio_data = audio_data.unsqueeze(0)
559
+ self.audio_data = audio_data
560
+
561
+ self.original_signal_length = self.signal_length
562
+
563
+ self.sample_rate = sample_rate
564
+ return self.to(device)
565
+
566
+ def write(self, audio_path: typing.Union[str, Path]):
567
+ """Writes audio to a file. Only writes the audio
568
+ that is in the very first item of the batch. To write other items
569
+ in the batch, index the signal along the batch dimension
570
+ before writing. After writing, the signal's ``path_to_file``
571
+ attribute is updated to the new path.
572
+
573
+ Parameters
574
+ ----------
575
+ audio_path : typing.Union[str, Path]
576
+ Path to write audio to.
577
+
578
+ Returns
579
+ -------
580
+ AudioSignal
581
+ Returns original AudioSignal, so you can use this in a fluent
582
+ interface.
583
+
584
+ Examples
585
+ --------
586
+ Creating and writing a signal to disk:
587
+
588
+ >>> signal = AudioSignal(torch.randn(10, 1, 44100), 44100)
589
+ >>> signal.write("/tmp/out.wav")
590
+
591
+ Writing a different element of the batch:
592
+
593
+ >>> signal[5].write("/tmp/out.wav")
594
+
595
+ Using this in a fluent interface:
596
+
597
+ >>> signal.write("/tmp/original.wav").low_pass(4000).write("/tmp/lowpass.wav")
598
+
599
+ """
600
+ if self.audio_data[0].abs().max() > 1:
601
+ warnings.warn("Audio amplitude > 1 clipped when saving")
602
+ soundfile.write(str(audio_path), self.audio_data[0].numpy().T, self.sample_rate)
603
+
604
+ self.path_to_file = audio_path
605
+ return self
606
+
607
+ def deepcopy(self):
608
+ """Copies the signal and all of its attributes.
609
+
610
+ Returns
611
+ -------
612
+ AudioSignal
613
+ Deep copy of the audio signal.
614
+ """
615
+ return copy.deepcopy(self)
616
+
617
+ def copy(self):
618
+ """Shallow copy of signal.
619
+
620
+ Returns
621
+ -------
622
+ AudioSignal
623
+ Shallow copy of the audio signal.
624
+ """
625
+ return copy.copy(self)
626
+
627
+ def clone(self):
628
+ """Clones all tensors contained in the AudioSignal,
629
+ and returns a copy of the signal with everything
630
+ cloned. Useful when using AudioSignal within autograd
631
+ computation graphs.
632
+
633
+ Relevant attributes are the stft data, the audio data,
634
+ and the loudness of the file.
635
+
636
+ Returns
637
+ -------
638
+ AudioSignal
639
+ Clone of AudioSignal.
640
+ """
641
+ clone = type(self)(
642
+ self.audio_data.clone(),
643
+ self.sample_rate,
644
+ stft_params=self.stft_params,
645
+ )
646
+ if self.stft_data is not None:
647
+ clone.stft_data = self.stft_data.clone()
648
+ if self._loudness is not None:
649
+ clone._loudness = self._loudness.clone()
650
+ clone.path_to_file = copy.deepcopy(self.path_to_file)
651
+ clone.metadata = copy.deepcopy(self.metadata)
652
+ return clone
653
+
654
+ def detach(self):
655
+ """Detaches tensors contained in AudioSignal.
656
+
657
+ Relevant attributes are the stft data, the audio data,
658
+ and the loudness of the file.
659
+
660
+ Returns
661
+ -------
662
+ AudioSignal
663
+ Same signal, but with all tensors detached.
664
+ """
665
+ if self._loudness is not None:
666
+ self._loudness = self._loudness.detach()
667
+ if self.stft_data is not None:
668
+ self.stft_data = self.stft_data.detach()
669
+
670
+ self.audio_data = self.audio_data.detach()
671
+ return self
672
+
673
+ def hash(self):
674
+ """Writes the audio data to a temporary file, and then
675
+ hashes it using hashlib. Useful for creating a file
676
+ name based on the audio content.
677
+
678
+ Returns
679
+ -------
680
+ str
681
+ Hash of audio data.
682
+
683
+ Examples
684
+ --------
685
+ Creating a signal, and writing it to a unique file name:
686
+
687
+ >>> signal = AudioSignal(torch.randn(44100), 44100)
688
+ >>> hash = signal.hash()
689
+ >>> signal.write(f"{hash}.wav")
690
+
691
+ """
692
+ with tempfile.NamedTemporaryFile(suffix=".wav") as f:
693
+ self.write(f.name)
694
+ h = hashlib.sha256()
695
+ b = bytearray(128 * 1024)
696
+ mv = memoryview(b)
697
+ with open(f.name, "rb", buffering=0) as f:
698
+ for n in iter(lambda: f.readinto(mv), 0):
699
+ h.update(mv[:n])
700
+ file_hash = h.hexdigest()
701
+ return file_hash
702
+
703
+ # Signal operations
704
+ def to_mono(self):
705
+ """Converts audio data to mono audio, by taking the mean
706
+ along the channels dimension.
707
+
708
+ Returns
709
+ -------
710
+ AudioSignal
711
+ AudioSignal with mean of channels.
712
+ """
713
+ self.audio_data = self.audio_data.mean(1, keepdim=True)
714
+ return self
715
+
716
+ def resample(self, sample_rate: int):
717
+ """Resamples the audio, using sinc interpolation. This works on both
718
+ cpu and gpu, and is much faster on gpu.
719
+
720
+ Parameters
721
+ ----------
722
+ sample_rate : int
723
+ Sample rate to resample to.
724
+
725
+ Returns
726
+ -------
727
+ AudioSignal
728
+ Resampled AudioSignal
729
+ """
730
+ if sample_rate == self.sample_rate:
731
+ return self
732
+ self.audio_data = julius.resample_frac(
733
+ self.audio_data, self.sample_rate, sample_rate
734
+ )
735
+ self.sample_rate = sample_rate
736
+ return self
737
+
738
+ # Tensor operations
739
+ def to(self, device: str):
740
+ """Moves all tensors contained in signal to the specified device.
741
+
742
+ Parameters
743
+ ----------
744
+ device : str
745
+ Device to move AudioSignal onto. Typical values are
746
+ "cuda", "cpu", or "cuda:n" to specify the nth gpu.
747
+
748
+ Returns
749
+ -------
750
+ AudioSignal
751
+ AudioSignal with all tensors moved to specified device.
752
+ """
753
+ if self._loudness is not None:
754
+ self._loudness = self._loudness.to(device)
755
+ if self.stft_data is not None:
756
+ self.stft_data = self.stft_data.to(device)
757
+ if self.audio_data is not None:
758
+ self.audio_data = self.audio_data.to(device)
759
+ return self
760
+
761
+ def float(self):
762
+ """Calls ``.float()`` on ``self.audio_data``.
763
+
764
+ Returns
765
+ -------
766
+ AudioSignal
767
+ """
768
+ self.audio_data = self.audio_data.float()
769
+ return self
770
+
771
+ def cpu(self):
772
+ """Moves AudioSignal to cpu.
773
+
774
+ Returns
775
+ -------
776
+ AudioSignal
777
+ """
778
+ return self.to("cpu")
779
+
780
+ def cuda(self): # pragma: no cover
781
+ """Moves AudioSignal to cuda.
782
+
783
+ Returns
784
+ -------
785
+ AudioSignal
786
+ """
787
+ return self.to("cuda")
788
+
789
+ def numpy(self):
790
+ """Detaches ``self.audio_data``, moves to cpu, and converts to numpy.
791
+
792
+ Returns
793
+ -------
794
+ np.ndarray
795
+ Audio data as a numpy array.
796
+ """
797
+ return self.audio_data.detach().cpu().numpy()
798
+
799
+ def zero_pad(self, before: int, after: int):
800
+ """Zero pads the audio_data tensor before and after.
801
+
802
+ Parameters
803
+ ----------
804
+ before : int
805
+ How many zeros to prepend to audio.
806
+ after : int
807
+ How many zeros to append to audio.
808
+
809
+ Returns
810
+ -------
811
+ AudioSignal
812
+ AudioSignal with padding applied.
813
+ """
814
+ self.audio_data = torch.nn.functional.pad(self.audio_data, (before, after))
815
+ return self
816
+
817
+ def zero_pad_to(self, length: int, mode: str = "after"):
818
+ """Pad with zeros to a specified length, either before or after
819
+ the audio data.
820
+
821
+ Parameters
822
+ ----------
823
+ length : int
824
+ Length to pad to
825
+ mode : str, optional
826
+ Whether to prepend or append zeros to signal, by default "after"
827
+
828
+ Returns
829
+ -------
830
+ AudioSignal
831
+ AudioSignal with padding applied.
832
+ """
833
+ if mode == "before":
834
+ self.zero_pad(max(length - self.signal_length, 0), 0)
835
+ elif mode == "after":
836
+ self.zero_pad(0, max(length - self.signal_length, 0))
837
+ return self
838
+
839
+ def trim(self, before: int, after: int):
840
+ """Trims the audio_data tensor before and after.
841
+
842
+ Parameters
843
+ ----------
844
+ before : int
845
+ How many samples to trim from beginning.
846
+ after : int
847
+ How many samples to trim from end.
848
+
849
+ Returns
850
+ -------
851
+ AudioSignal
852
+ AudioSignal with trimming applied.
853
+ """
854
+ if after == 0:
855
+ self.audio_data = self.audio_data[..., before:]
856
+ else:
857
+ self.audio_data = self.audio_data[..., before:-after]
858
+ return self
859
+
860
+ def truncate_samples(self, length_in_samples: int):
861
+ """Truncate signal to specified length.
862
+
863
+ Parameters
864
+ ----------
865
+ length_in_samples : int
866
+ Truncate to this many samples.
867
+
868
+ Returns
869
+ -------
870
+ AudioSignal
871
+ AudioSignal with truncation applied.
872
+ """
873
+ self.audio_data = self.audio_data[..., :length_in_samples]
874
+ return self
875
+
876
+ @property
877
+ def device(self):
878
+ """Get device that AudioSignal is on.
879
+
880
+ Returns
881
+ -------
882
+ torch.device
883
+ Device that AudioSignal is on.
884
+ """
885
+ if self.audio_data is not None:
886
+ device = self.audio_data.device
887
+ elif self.stft_data is not None:
888
+ device = self.stft_data.device
889
+ return device
890
+
891
+ # Properties
892
+ @property
893
+ def audio_data(self):
894
+ """Returns the audio data tensor in the object.
895
+
896
+ Audio data is always of the shape
897
+ (batch_size, num_channels, num_samples). If value has less
898
+ than 3 dims (e.g. is (num_channels, num_samples)), then it will
899
+ be reshaped to (1, num_channels, num_samples) - a batch size of 1.
900
+
901
+ Parameters
902
+ ----------
903
+ data : typing.Union[torch.Tensor, np.ndarray]
904
+ Audio data to set.
905
+
906
+ Returns
907
+ -------
908
+ torch.Tensor
909
+ Audio samples.
910
+ """
911
+ return self._audio_data
912
+
913
+ @audio_data.setter
914
+ def audio_data(self, data: typing.Union[torch.Tensor, np.ndarray]):
915
+ if data is not None:
916
+ assert torch.is_tensor(data), "audio_data should be torch.Tensor"
917
+ assert data.ndim == 3, "audio_data should be 3-dim (B, C, T)"
918
+ self._audio_data = data
919
+ # Old loudness value not guaranteed to be right, reset it.
920
+ self._loudness = None
921
+ return
922
+
923
+ # alias for audio_data
924
+ samples = audio_data
925
+
926
+ @property
927
+ def stft_data(self):
928
+ """Returns the STFT data inside the signal. Shape is
929
+ (batch, channels, frequencies, time).
930
+
931
+ Returns
932
+ -------
933
+ torch.Tensor
934
+ Complex spectrogram data.
935
+ """
936
+ return self._stft_data
937
+
938
+ @stft_data.setter
939
+ def stft_data(self, data: typing.Union[torch.Tensor, np.ndarray]):
940
+ if data is not None:
941
+ assert torch.is_tensor(data) and torch.is_complex(data)
942
+ if self.stft_data is not None and self.stft_data.shape != data.shape:
943
+ warnings.warn("stft_data changed shape")
944
+ self._stft_data = data
945
+ return
946
+
947
+ @property
948
+ def batch_size(self):
949
+ """Batch size of audio signal.
950
+
951
+ Returns
952
+ -------
953
+ int
954
+ Batch size of signal.
955
+ """
956
+ return self.audio_data.shape[0]
957
+
958
+ @property
959
+ def signal_length(self):
960
+ """Length of audio signal.
961
+
962
+ Returns
963
+ -------
964
+ int
965
+ Length of signal in samples.
966
+ """
967
+ return self.audio_data.shape[-1]
968
+
969
+ # alias for signal_length
970
+ length = signal_length
971
+
972
+ @property
973
+ def shape(self):
974
+ """Shape of audio data.
975
+
976
+ Returns
977
+ -------
978
+ tuple
979
+ Shape of audio data.
980
+ """
981
+ return self.audio_data.shape
982
+
983
+ @property
984
+ def signal_duration(self):
985
+ """Length of audio signal in seconds.
986
+
987
+ Returns
988
+ -------
989
+ float
990
+ Length of signal in seconds.
991
+ """
992
+ return self.signal_length / self.sample_rate
993
+
994
+ # alias for signal_duration
995
+ duration = signal_duration
996
+
997
+ @property
998
+ def num_channels(self):
999
+ """Number of audio channels.
1000
+
1001
+ Returns
1002
+ -------
1003
+ int
1004
+ Number of audio channels.
1005
+ """
1006
+ return self.audio_data.shape[1]
1007
+
1008
+ # STFT
1009
+ @staticmethod
1010
+ @functools.lru_cache(None)
1011
+ def get_window(window_type: str, window_length: int, device: str):
1012
+ """Wrapper around scipy.signal.get_window so one can also get the
1013
+ popular sqrt-hann window. This function caches for efficiency
1014
+ using functools.lru\_cache.
1015
+
1016
+ Parameters
1017
+ ----------
1018
+ window_type : str
1019
+ Type of window to get
1020
+ window_length : int
1021
+ Length of the window
1022
+ device : str
1023
+ Device to put window onto.
1024
+
1025
+ Returns
1026
+ -------
1027
+ torch.Tensor
1028
+ Window returned by scipy.signal.get_window, as a tensor.
1029
+ """
1030
+ from scipy import signal
1031
+
1032
+ if window_type == "average":
1033
+ window = np.ones(window_length) / window_length
1034
+ elif window_type == "sqrt_hann":
1035
+ window = np.sqrt(signal.get_window("hann", window_length))
1036
+ else:
1037
+ window = signal.get_window(window_type, window_length)
1038
+ window = torch.from_numpy(window).to(device).float()
1039
+ return window
1040
+
1041
+ @property
1042
+ def stft_params(self):
1043
+ """Returns STFTParams object, which can be re-used to other
1044
+ AudioSignals.
1045
+
1046
+ This property can be set as well. If values are not defined in STFTParams,
1047
+ they are inferred automatically from the signal properties. The default is to use
1048
+ 32ms windows, with 8ms hop length, and the square root of the hann window.
1049
+
1050
+ Returns
1051
+ -------
1052
+ STFTParams
1053
+ STFT parameters for the AudioSignal.
1054
+
1055
+ Examples
1056
+ --------
1057
+ >>> stft_params = STFTParams(128, 32)
1058
+ >>> signal1 = AudioSignal(torch.randn(44100), 44100, stft_params=stft_params)
1059
+ >>> signal2 = AudioSignal(torch.randn(44100), 44100, stft_params=signal1.stft_params)
1060
+ >>> signal1.stft_params = STFTParams() # Defaults
1061
+ """
1062
+ return self._stft_params
1063
+
1064
+ @stft_params.setter
1065
+ def stft_params(self, value: STFTParams):
1066
+ default_win_len = int(2 ** (np.ceil(np.log2(0.032 * self.sample_rate))))
1067
+ default_hop_len = default_win_len // 4
1068
+ default_win_type = "hann"
1069
+ default_match_stride = False
1070
+ default_padding_type = "reflect"
1071
+
1072
+ default_stft_params = STFTParams(
1073
+ window_length=default_win_len,
1074
+ hop_length=default_hop_len,
1075
+ window_type=default_win_type,
1076
+ match_stride=default_match_stride,
1077
+ padding_type=default_padding_type,
1078
+ )._asdict()
1079
+
1080
+ value = value._asdict() if value else default_stft_params
1081
+
1082
+ for key in default_stft_params:
1083
+ if value[key] is None:
1084
+ value[key] = default_stft_params[key]
1085
+
1086
+ self._stft_params = STFTParams(**value)
1087
+ self.stft_data = None
1088
+
1089
+ def compute_stft_padding(
1090
+ self, window_length: int, hop_length: int, match_stride: bool
1091
+ ):
1092
+ """Compute how the STFT should be padded, based on match\_stride.
1093
+
1094
+ Parameters
1095
+ ----------
1096
+ window_length : int
1097
+ Window length of STFT.
1098
+ hop_length : int
1099
+ Hop length of STFT.
1100
+ match_stride : bool
1101
+ Whether or not to match stride, making the STFT have the same alignment as
1102
+ convolutional layers.
1103
+
1104
+ Returns
1105
+ -------
1106
+ tuple
1107
+ Amount to pad on either side of audio.
1108
+ """
1109
+ length = self.signal_length
1110
+
1111
+ if match_stride:
1112
+ assert (
1113
+ hop_length == window_length // 4
1114
+ ), "For match_stride, hop must equal n_fft // 4"
1115
+ right_pad = math.ceil(length / hop_length) * hop_length - length
1116
+ pad = (window_length - hop_length) // 2
1117
+ else:
1118
+ right_pad = 0
1119
+ pad = 0
1120
+
1121
+ return right_pad, pad
1122
+
1123
+ def stft(
1124
+ self,
1125
+ window_length: int = None,
1126
+ hop_length: int = None,
1127
+ window_type: str = None,
1128
+ match_stride: bool = None,
1129
+ padding_type: str = None,
1130
+ ):
1131
+ """Computes the short-time Fourier transform of the audio data,
1132
+ with specified STFT parameters.
1133
+
1134
+ Parameters
1135
+ ----------
1136
+ window_length : int, optional
1137
+ Window length of STFT, by default ``0.032 * self.sample_rate``.
1138
+ hop_length : int, optional
1139
+ Hop length of STFT, by default ``window_length // 4``.
1140
+ window_type : str, optional
1141
+ Type of window to use, by default ``sqrt\_hann``.
1142
+ match_stride : bool, optional
1143
+ Whether to match the stride of convolutional layers, by default False
1144
+ padding_type : str, optional
1145
+ Type of padding to use, by default 'reflect'
1146
+
1147
+ Returns
1148
+ -------
1149
+ torch.Tensor
1150
+ STFT of audio data.
1151
+
1152
+ Examples
1153
+ --------
1154
+ Compute the STFT of an AudioSignal:
1155
+
1156
+ >>> signal = AudioSignal(torch.randn(44100), 44100)
1157
+ >>> signal.stft()
1158
+
1159
+ Vary the window and hop length:
1160
+
1161
+ >>> stft_params = [STFTParams(128, 32), STFTParams(512, 128)]
1162
+ >>> for stft_param in stft_params:
1163
+ >>> signal.stft_params = stft_params
1164
+ >>> signal.stft()
1165
+
1166
+ """
1167
+ window_length = (
1168
+ self.stft_params.window_length
1169
+ if window_length is None
1170
+ else int(window_length)
1171
+ )
1172
+ hop_length = (
1173
+ self.stft_params.hop_length if hop_length is None else int(hop_length)
1174
+ )
1175
+ window_type = (
1176
+ self.stft_params.window_type if window_type is None else window_type
1177
+ )
1178
+ match_stride = (
1179
+ self.stft_params.match_stride if match_stride is None else match_stride
1180
+ )
1181
+ padding_type = (
1182
+ self.stft_params.padding_type if padding_type is None else padding_type
1183
+ )
1184
+
1185
+ window = self.get_window(window_type, window_length, self.audio_data.device)
1186
+ window = window.to(self.audio_data.device)
1187
+
1188
+ audio_data = self.audio_data
1189
+ right_pad, pad = self.compute_stft_padding(
1190
+ window_length, hop_length, match_stride
1191
+ )
1192
+ audio_data = torch.nn.functional.pad(
1193
+ audio_data, (pad, pad + right_pad), padding_type
1194
+ )
1195
+ stft_data = torch.stft(
1196
+ audio_data.reshape(-1, audio_data.shape[-1]),
1197
+ n_fft=window_length,
1198
+ hop_length=hop_length,
1199
+ window=window,
1200
+ return_complex=True,
1201
+ center=True,
1202
+ )
1203
+ _, nf, nt = stft_data.shape
1204
+ stft_data = stft_data.reshape(self.batch_size, self.num_channels, nf, nt)
1205
+
1206
+ if match_stride:
1207
+ # Drop first two and last two frames, which are added
1208
+ # because of padding. Now num_frames * hop_length = num_samples.
1209
+ stft_data = stft_data[..., 2:-2]
1210
+ self.stft_data = stft_data
1211
+
1212
+ return stft_data
1213
+
1214
+ def istft(
1215
+ self,
1216
+ window_length: int = None,
1217
+ hop_length: int = None,
1218
+ window_type: str = None,
1219
+ match_stride: bool = None,
1220
+ length: int = None,
1221
+ ):
1222
+ """Computes inverse STFT and sets it to audio\_data.
1223
+
1224
+ Parameters
1225
+ ----------
1226
+ window_length : int, optional
1227
+ Window length of STFT, by default ``0.032 * self.sample_rate``.
1228
+ hop_length : int, optional
1229
+ Hop length of STFT, by default ``window_length // 4``.
1230
+ window_type : str, optional
1231
+ Type of window to use, by default ``sqrt\_hann``.
1232
+ match_stride : bool, optional
1233
+ Whether to match the stride of convolutional layers, by default False
1234
+ length : int, optional
1235
+ Original length of signal, by default None
1236
+
1237
+ Returns
1238
+ -------
1239
+ AudioSignal
1240
+ AudioSignal with istft applied.
1241
+
1242
+ Raises
1243
+ ------
1244
+ RuntimeError
1245
+ Raises an error if stft was not called prior to istft on the signal,
1246
+ or if stft_data is not set.
1247
+ """
1248
+ if self.stft_data is None:
1249
+ raise RuntimeError("Cannot do inverse STFT without self.stft_data!")
1250
+
1251
+ window_length = (
1252
+ self.stft_params.window_length
1253
+ if window_length is None
1254
+ else int(window_length)
1255
+ )
1256
+ hop_length = (
1257
+ self.stft_params.hop_length if hop_length is None else int(hop_length)
1258
+ )
1259
+ window_type = (
1260
+ self.stft_params.window_type if window_type is None else window_type
1261
+ )
1262
+ match_stride = (
1263
+ self.stft_params.match_stride if match_stride is None else match_stride
1264
+ )
1265
+
1266
+ window = self.get_window(window_type, window_length, self.stft_data.device)
1267
+
1268
+ nb, nch, nf, nt = self.stft_data.shape
1269
+ stft_data = self.stft_data.reshape(nb * nch, nf, nt)
1270
+ right_pad, pad = self.compute_stft_padding(
1271
+ window_length, hop_length, match_stride
1272
+ )
1273
+
1274
+ if length is None:
1275
+ length = self.original_signal_length
1276
+ length = length + 2 * pad + right_pad
1277
+
1278
+ if match_stride:
1279
+ # Zero-pad the STFT on either side, putting back the frames that were
1280
+ # dropped in stft().
1281
+ stft_data = torch.nn.functional.pad(stft_data, (2, 2))
1282
+
1283
+ audio_data = torch.istft(
1284
+ stft_data,
1285
+ n_fft=window_length,
1286
+ hop_length=hop_length,
1287
+ window=window,
1288
+ length=length,
1289
+ center=True,
1290
+ )
1291
+ audio_data = audio_data.reshape(nb, nch, -1)
1292
+ if match_stride:
1293
+ audio_data = audio_data[..., pad : -(pad + right_pad)]
1294
+ self.audio_data = audio_data
1295
+
1296
+ return self
1297
+
1298
+ @staticmethod
1299
+ @functools.lru_cache(None)
1300
+ def get_mel_filters(
1301
+ sr: int, n_fft: int, n_mels: int, fmin: float = 0.0, fmax: float = None
1302
+ ):
1303
+ """Create a Filterbank matrix to combine FFT bins into Mel-frequency bins.
1304
+
1305
+ Parameters
1306
+ ----------
1307
+ sr : int
1308
+ Sample rate of audio
1309
+ n_fft : int
1310
+ Number of FFT bins
1311
+ n_mels : int
1312
+ Number of mels
1313
+ fmin : float, optional
1314
+ Lowest frequency, in Hz, by default 0.0
1315
+ fmax : float, optional
1316
+ Highest frequency, by default None
1317
+
1318
+ Returns
1319
+ -------
1320
+ np.ndarray [shape=(n_mels, 1 + n_fft/2)]
1321
+ Mel transform matrix
1322
+ """
1323
+ from librosa.filters import mel as librosa_mel_fn
1324
+
1325
+ return librosa_mel_fn(
1326
+ sr=sr,
1327
+ n_fft=n_fft,
1328
+ n_mels=n_mels,
1329
+ fmin=fmin,
1330
+ fmax=fmax,
1331
+ )
1332
+
1333
+ def mel_spectrogram(
1334
+ self, n_mels: int = 80, mel_fmin: float = 0.0, mel_fmax: float = None, **kwargs
1335
+ ):
1336
+ """Computes a Mel spectrogram.
1337
+
1338
+ Parameters
1339
+ ----------
1340
+ n_mels : int, optional
1341
+ Number of mels, by default 80
1342
+ mel_fmin : float, optional
1343
+ Lowest frequency, in Hz, by default 0.0
1344
+ mel_fmax : float, optional
1345
+ Highest frequency, by default None
1346
+ kwargs : dict, optional
1347
+ Keyword arguments to self.stft().
1348
+
1349
+ Returns
1350
+ -------
1351
+ torch.Tensor [shape=(batch, channels, mels, time)]
1352
+ Mel spectrogram.
1353
+ """
1354
+ stft = self.stft(**kwargs)
1355
+ magnitude = torch.abs(stft)
1356
+
1357
+ nf = magnitude.shape[2]
1358
+ mel_basis = self.get_mel_filters(
1359
+ sr=self.sample_rate,
1360
+ n_fft=2 * (nf - 1),
1361
+ n_mels=n_mels,
1362
+ fmin=mel_fmin,
1363
+ fmax=mel_fmax,
1364
+ )
1365
+ mel_basis = torch.from_numpy(mel_basis).to(self.device)
1366
+
1367
+ mel_spectrogram = magnitude.transpose(2, -1) @ mel_basis.T
1368
+ mel_spectrogram = mel_spectrogram.transpose(-1, 2)
1369
+ return mel_spectrogram
1370
+
1371
+ @staticmethod
1372
+ @functools.lru_cache(None)
1373
+ def get_dct(n_mfcc: int, n_mels: int, norm: str = "ortho", device: str = None):
1374
+ """Create a discrete cosine transform (DCT) transformation matrix with shape (``n_mels``, ``n_mfcc``),
1375
+ it can be normalized depending on norm. For more information about dct:
1376
+ http://en.wikipedia.org/wiki/Discrete_cosine_transform#DCT-II
1377
+
1378
+ Parameters
1379
+ ----------
1380
+ n_mfcc : int
1381
+ Number of mfccs
1382
+ n_mels : int
1383
+ Number of mels
1384
+ norm : str
1385
+ Use "ortho" to get a orthogonal matrix or None, by default "ortho"
1386
+ device : str, optional
1387
+ Device to load the transformation matrix on, by default None
1388
+
1389
+ Returns
1390
+ -------
1391
+ torch.Tensor [shape=(n_mels, n_mfcc)] T
1392
+ The dct transformation matrix.
1393
+ """
1394
+ from torchaudio.functional import create_dct
1395
+
1396
+ return create_dct(n_mfcc, n_mels, norm).to(device)
1397
+
1398
+ def mfcc(
1399
+ self, n_mfcc: int = 40, n_mels: int = 80, log_offset: float = 1e-6, **kwargs
1400
+ ):
1401
+ """Computes mel-frequency cepstral coefficients (MFCCs).
1402
+
1403
+ Parameters
1404
+ ----------
1405
+ n_mfcc : int, optional
1406
+ Number of mels, by default 40
1407
+ n_mels : int, optional
1408
+ Number of mels, by default 80
1409
+ log_offset: float, optional
1410
+ Small value to prevent numerical issues when trying to compute log(0), by default 1e-6
1411
+ kwargs : dict, optional
1412
+ Keyword arguments to self.mel_spectrogram(), note that some of them will be used for self.stft()
1413
+
1414
+ Returns
1415
+ -------
1416
+ torch.Tensor [shape=(batch, channels, mfccs, time)]
1417
+ MFCCs.
1418
+ """
1419
+
1420
+ mel_spectrogram = self.mel_spectrogram(n_mels, **kwargs)
1421
+ mel_spectrogram = torch.log(mel_spectrogram + log_offset)
1422
+ dct_mat = self.get_dct(n_mfcc, n_mels, "ortho", self.device)
1423
+
1424
+ mfcc = mel_spectrogram.transpose(-1, -2) @ dct_mat
1425
+ mfcc = mfcc.transpose(-1, -2)
1426
+ return mfcc
1427
+
1428
+ @property
1429
+ def magnitude(self):
1430
+ """Computes and returns the absolute value of the STFT, which
1431
+ is the magnitude. This value can also be set to some tensor.
1432
+ When set, ``self.stft_data`` is manipulated so that its magnitude
1433
+ matches what this is set to, and modulated by the phase.
1434
+
1435
+ Returns
1436
+ -------
1437
+ torch.Tensor
1438
+ Magnitude of STFT.
1439
+
1440
+ Examples
1441
+ --------
1442
+ >>> signal = AudioSignal(torch.randn(44100), 44100)
1443
+ >>> magnitude = signal.magnitude # Computes stft if not computed
1444
+ >>> magnitude[magnitude < magnitude.mean()] = 0
1445
+ >>> signal.magnitude = magnitude
1446
+ >>> signal.istft()
1447
+ """
1448
+ if self.stft_data is None:
1449
+ self.stft()
1450
+ return torch.abs(self.stft_data)
1451
+
1452
+ @magnitude.setter
1453
+ def magnitude(self, value):
1454
+ self.stft_data = value * torch.exp(1j * self.phase)
1455
+ return
1456
+
1457
+ def log_magnitude(
1458
+ self, ref_value: float = 1.0, amin: float = 1e-5, top_db: float = 80.0
1459
+ ):
1460
+ """Computes the log-magnitude of the spectrogram.
1461
+
1462
+ Parameters
1463
+ ----------
1464
+ ref_value : float, optional
1465
+ The magnitude is scaled relative to ``ref``: ``20 * log10(S / ref)``.
1466
+ Zeros in the output correspond to positions where ``S == ref``,
1467
+ by default 1.0
1468
+ amin : float, optional
1469
+ Minimum threshold for ``S`` and ``ref``, by default 1e-5
1470
+ top_db : float, optional
1471
+ Threshold the output at ``top_db`` below the peak:
1472
+ ``max(10 * log10(S/ref)) - top_db``, by default -80.0
1473
+
1474
+ Returns
1475
+ -------
1476
+ torch.Tensor
1477
+ Log-magnitude spectrogram
1478
+ """
1479
+ magnitude = self.magnitude
1480
+
1481
+ amin = amin**2
1482
+ log_spec = 10.0 * torch.log10(magnitude.pow(2).clamp(min=amin))
1483
+ log_spec -= 10.0 * np.log10(np.maximum(amin, ref_value))
1484
+
1485
+ if top_db is not None:
1486
+ log_spec = torch.maximum(log_spec, log_spec.max() - top_db)
1487
+ return log_spec
1488
+
1489
+ @property
1490
+ def phase(self):
1491
+ """Computes and returns the phase of the STFT.
1492
+ This value can also be set to some tensor.
1493
+ When set, ``self.stft_data`` is manipulated so that its phase
1494
+ matches what this is set to, we original magnitudeith th.
1495
+
1496
+ Returns
1497
+ -------
1498
+ torch.Tensor
1499
+ Phase of STFT.
1500
+
1501
+ Examples
1502
+ --------
1503
+ >>> signal = AudioSignal(torch.randn(44100), 44100)
1504
+ >>> phase = signal.phase # Computes stft if not computed
1505
+ >>> phase[phase < phase.mean()] = 0
1506
+ >>> signal.phase = phase
1507
+ >>> signal.istft()
1508
+ """
1509
+ if self.stft_data is None:
1510
+ self.stft()
1511
+ return torch.angle(self.stft_data)
1512
+
1513
+ @phase.setter
1514
+ def phase(self, value):
1515
+ self.stft_data = self.magnitude * torch.exp(1j * value)
1516
+ return
1517
+
1518
+ # Operator overloading
1519
+ def __add__(self, other):
1520
+ new_signal = self.clone()
1521
+ new_signal.audio_data += util._get_value(other)
1522
+ return new_signal
1523
+
1524
+ def __iadd__(self, other):
1525
+ self.audio_data += util._get_value(other)
1526
+ return self
1527
+
1528
+ def __radd__(self, other):
1529
+ return self + other
1530
+
1531
+ def __sub__(self, other):
1532
+ new_signal = self.clone()
1533
+ new_signal.audio_data -= util._get_value(other)
1534
+ return new_signal
1535
+
1536
+ def __isub__(self, other):
1537
+ self.audio_data -= util._get_value(other)
1538
+ return self
1539
+
1540
+ def __mul__(self, other):
1541
+ new_signal = self.clone()
1542
+ new_signal.audio_data *= util._get_value(other)
1543
+ return new_signal
1544
+
1545
+ def __imul__(self, other):
1546
+ self.audio_data *= util._get_value(other)
1547
+ return self
1548
+
1549
+ def __rmul__(self, other):
1550
+ return self * other
1551
+
1552
+ # Representation
1553
+ def _info(self):
1554
+ dur = f"{self.signal_duration:0.3f}" if self.signal_duration else "[unknown]"
1555
+ info = {
1556
+ "duration": f"{dur} seconds",
1557
+ "batch_size": self.batch_size,
1558
+ "path": self.path_to_file if self.path_to_file else "path unknown",
1559
+ "sample_rate": self.sample_rate,
1560
+ "num_channels": self.num_channels if self.num_channels else "[unknown]",
1561
+ "audio_data.shape": self.audio_data.shape,
1562
+ "stft_params": self.stft_params,
1563
+ "device": self.device,
1564
+ }
1565
+
1566
+ return info
1567
+
1568
+ def markdown(self):
1569
+ """Produces a markdown representation of AudioSignal, in a markdown table.
1570
+
1571
+ Returns
1572
+ -------
1573
+ str
1574
+ Markdown representation of AudioSignal.
1575
+
1576
+ Examples
1577
+ --------
1578
+ >>> signal = AudioSignal(torch.randn(44100), 44100)
1579
+ >>> print(signal.markdown())
1580
+ | Key | Value
1581
+ |---|---
1582
+ | duration | 1.000 seconds |
1583
+ | batch_size | 1 |
1584
+ | path | path unknown |
1585
+ | sample_rate | 44100 |
1586
+ | num_channels | 1 |
1587
+ | audio_data.shape | torch.Size([1, 1, 44100]) |
1588
+ | stft_params | STFTParams(window_length=2048, hop_length=512, window_type='sqrt_hann', match_stride=False) |
1589
+ | device | cpu |
1590
+ """
1591
+ info = self._info()
1592
+
1593
+ FORMAT = "| Key | Value \n" "|---|--- \n"
1594
+ for k, v in info.items():
1595
+ row = f"| {k} | {v} |\n"
1596
+ FORMAT += row
1597
+ return FORMAT
1598
+
1599
+ def __str__(self):
1600
+ info = self._info()
1601
+
1602
+ desc = ""
1603
+ for k, v in info.items():
1604
+ desc += f"{k}: {v}\n"
1605
+ return desc
1606
+
1607
+ def __rich__(self):
1608
+ from rich.table import Table
1609
+
1610
+ info = self._info()
1611
+
1612
+ table = Table(title=f"{self.__class__.__name__}")
1613
+ table.add_column("Key", style="green")
1614
+ table.add_column("Value", style="cyan")
1615
+
1616
+ for k, v in info.items():
1617
+ table.add_row(k, str(v))
1618
+ return table
1619
+
1620
+ # Comparison
1621
+ def __eq__(self, other):
1622
+ for k, v in list(self.__dict__.items()):
1623
+ if torch.is_tensor(v):
1624
+ if not torch.allclose(v, other.__dict__[k], atol=1e-6):
1625
+ max_error = (v - other.__dict__[k]).abs().max()
1626
+ print(f"Max abs error for {k}: {max_error}")
1627
+ return False
1628
+ return True
1629
+
1630
+ # Indexing
1631
+ def __getitem__(self, key):
1632
+ if torch.is_tensor(key) and key.ndim == 0 and key.item() is True:
1633
+ assert self.batch_size == 1
1634
+ audio_data = self.audio_data
1635
+ _loudness = self._loudness
1636
+ stft_data = self.stft_data
1637
+
1638
+ elif isinstance(key, (bool, int, list, slice, tuple)) or (
1639
+ torch.is_tensor(key) and key.ndim <= 1
1640
+ ):
1641
+ # Indexing only on the batch dimension.
1642
+ # Then let's copy over relevant stuff.
1643
+ # Future work: make this work for time-indexing
1644
+ # as well, using the hop length.
1645
+ audio_data = self.audio_data[key]
1646
+ _loudness = self._loudness[key] if self._loudness is not None else None
1647
+ stft_data = self.stft_data[key] if self.stft_data is not None else None
1648
+
1649
+ sources = None
1650
+
1651
+ copy = type(self)(audio_data, self.sample_rate, stft_params=self.stft_params)
1652
+ copy._loudness = _loudness
1653
+ copy._stft_data = stft_data
1654
+ copy.sources = sources
1655
+
1656
+ return copy
1657
+
1658
+ def __setitem__(self, key, value):
1659
+ if not isinstance(value, type(self)):
1660
+ self.audio_data[key] = value
1661
+ return
1662
+
1663
+ if torch.is_tensor(key) and key.ndim == 0 and key.item() is True:
1664
+ assert self.batch_size == 1
1665
+ self.audio_data = value.audio_data
1666
+ self._loudness = value._loudness
1667
+ self.stft_data = value.stft_data
1668
+ return
1669
+
1670
+ elif isinstance(key, (bool, int, list, slice, tuple)) or (
1671
+ torch.is_tensor(key) and key.ndim <= 1
1672
+ ):
1673
+ if self.audio_data is not None and value.audio_data is not None:
1674
+ self.audio_data[key] = value.audio_data
1675
+ if self._loudness is not None and value._loudness is not None:
1676
+ self._loudness[key] = value._loudness
1677
+ if self.stft_data is not None and value.stft_data is not None:
1678
+ self.stft_data[key] = value.stft_data
1679
+ return
1680
+
1681
+ def __ne__(self, other):
1682
+ return not self == other
audiotools/core/display.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import inspect
2
+ import typing
3
+ from functools import wraps
4
+
5
+ from . import util
6
+
7
+
8
+ def format_figure(func):
9
+ """Decorator for formatting figures produced by the code below.
10
+ See :py:func:`audiotools.core.util.format_figure` for more.
11
+
12
+ Parameters
13
+ ----------
14
+ func : Callable
15
+ Plotting function that is decorated by this function.
16
+
17
+ """
18
+
19
+ @wraps(func)
20
+ def wrapper(*args, **kwargs):
21
+ f_keys = inspect.signature(util.format_figure).parameters.keys()
22
+ f_kwargs = {}
23
+ for k, v in list(kwargs.items()):
24
+ if k in f_keys:
25
+ kwargs.pop(k)
26
+ f_kwargs[k] = v
27
+ func(*args, **kwargs)
28
+ util.format_figure(**f_kwargs)
29
+
30
+ return wrapper
31
+
32
+
33
+ class DisplayMixin:
34
+ @format_figure
35
+ def specshow(
36
+ self,
37
+ preemphasis: bool = False,
38
+ x_axis: str = "time",
39
+ y_axis: str = "linear",
40
+ n_mels: int = 128,
41
+ **kwargs,
42
+ ):
43
+ """Displays a spectrogram, using ``librosa.display.specshow``.
44
+
45
+ Parameters
46
+ ----------
47
+ preemphasis : bool, optional
48
+ Whether or not to apply preemphasis, which makes high
49
+ frequency detail easier to see, by default False
50
+ x_axis : str, optional
51
+ How to label the x axis, by default "time"
52
+ y_axis : str, optional
53
+ How to label the y axis, by default "linear"
54
+ n_mels : int, optional
55
+ If displaying a mel spectrogram with ``y_axis = "mel"``,
56
+ this controls the number of mels, by default 128.
57
+ kwargs : dict, optional
58
+ Keyword arguments to :py:func:`audiotools.core.util.format_figure`.
59
+ """
60
+ import librosa
61
+ import librosa.display
62
+
63
+ # Always re-compute the STFT data before showing it, in case
64
+ # it changed.
65
+ signal = self.clone()
66
+ signal.stft_data = None
67
+
68
+ if preemphasis:
69
+ signal.preemphasis()
70
+
71
+ ref = signal.magnitude.max()
72
+ log_mag = signal.log_magnitude(ref_value=ref)
73
+
74
+ if y_axis == "mel":
75
+ log_mag = 20 * signal.mel_spectrogram(n_mels).clamp(1e-5).log10()
76
+ log_mag -= log_mag.max()
77
+
78
+ librosa.display.specshow(
79
+ log_mag.numpy()[0].mean(axis=0),
80
+ x_axis=x_axis,
81
+ y_axis=y_axis,
82
+ sr=signal.sample_rate,
83
+ **kwargs,
84
+ )
85
+
86
+ @format_figure
87
+ def waveplot(self, x_axis: str = "time", **kwargs):
88
+ """Displays a waveform plot, using ``librosa.display.waveshow``.
89
+
90
+ Parameters
91
+ ----------
92
+ x_axis : str, optional
93
+ How to label the x axis, by default "time"
94
+ kwargs : dict, optional
95
+ Keyword arguments to :py:func:`audiotools.core.util.format_figure`.
96
+ """
97
+ import librosa
98
+ import librosa.display
99
+
100
+ audio_data = self.audio_data[0].mean(dim=0)
101
+ audio_data = audio_data.cpu().numpy()
102
+
103
+ plot_fn = "waveshow" if hasattr(librosa.display, "waveshow") else "waveplot"
104
+ wave_plot_fn = getattr(librosa.display, plot_fn)
105
+ wave_plot_fn(audio_data, x_axis=x_axis, sr=self.sample_rate, **kwargs)
106
+
107
+ @format_figure
108
+ def wavespec(self, x_axis: str = "time", **kwargs):
109
+ """Displays a waveform plot, using ``librosa.display.waveshow``.
110
+
111
+ Parameters
112
+ ----------
113
+ x_axis : str, optional
114
+ How to label the x axis, by default "time"
115
+ kwargs : dict, optional
116
+ Keyword arguments to :py:func:`audiotools.core.display.DisplayMixin.specshow`.
117
+ """
118
+ import matplotlib.pyplot as plt
119
+ from matplotlib.gridspec import GridSpec
120
+
121
+ gs = GridSpec(6, 1)
122
+ plt.subplot(gs[0, :])
123
+ self.waveplot(x_axis=x_axis)
124
+ plt.subplot(gs[1:, :])
125
+ self.specshow(x_axis=x_axis, **kwargs)
126
+
127
+ def write_audio_to_tb(
128
+ self,
129
+ tag: str,
130
+ writer,
131
+ step: int = None,
132
+ plot_fn: typing.Union[typing.Callable, str] = "specshow",
133
+ **kwargs,
134
+ ):
135
+ """Writes a signal and its spectrogram to Tensorboard. Will show up
136
+ under the Audio and Images tab in Tensorboard.
137
+
138
+ Parameters
139
+ ----------
140
+ tag : str
141
+ Tag to write signal to (e.g. ``clean/sample_0.wav``). The image will be
142
+ written to the corresponding ``.png`` file (e.g. ``clean/sample_0.png``).
143
+ writer : SummaryWriter
144
+ A SummaryWriter object from PyTorch library.
145
+ step : int, optional
146
+ The step to write the signal to, by default None
147
+ plot_fn : typing.Union[typing.Callable, str], optional
148
+ How to create the image. Set to ``None`` to avoid plotting, by default "specshow"
149
+ kwargs : dict, optional
150
+ Keyword arguments to :py:func:`audiotools.core.display.DisplayMixin.specshow` or
151
+ whatever ``plot_fn`` is set to.
152
+ """
153
+ import matplotlib.pyplot as plt
154
+
155
+ audio_data = self.audio_data[0, 0].detach().cpu()
156
+ sample_rate = self.sample_rate
157
+ writer.add_audio(tag, audio_data, step, sample_rate)
158
+
159
+ if plot_fn is not None:
160
+ if isinstance(plot_fn, str):
161
+ plot_fn = getattr(self, plot_fn)
162
+ fig = plt.figure()
163
+ plt.clf()
164
+ plot_fn(**kwargs)
165
+ writer.add_figure(tag.replace("wav", "png"), fig, step)
166
+
167
+ def save_image(
168
+ self,
169
+ image_path: str,
170
+ plot_fn: typing.Union[typing.Callable, str] = "specshow",
171
+ **kwargs,
172
+ ):
173
+ """Save AudioSignal spectrogram (or whatever ``plot_fn`` is set to) to
174
+ a specified file.
175
+
176
+ Parameters
177
+ ----------
178
+ image_path : str
179
+ Where to save the file to.
180
+ plot_fn : typing.Union[typing.Callable, str], optional
181
+ How to create the image. Set to ``None`` to avoid plotting, by default "specshow"
182
+ kwargs : dict, optional
183
+ Keyword arguments to :py:func:`audiotools.core.display.DisplayMixin.specshow` or
184
+ whatever ``plot_fn`` is set to.
185
+ """
186
+ import matplotlib.pyplot as plt
187
+
188
+ if isinstance(plot_fn, str):
189
+ plot_fn = getattr(self, plot_fn)
190
+
191
+ plt.clf()
192
+ plot_fn(**kwargs)
193
+ plt.savefig(image_path, bbox_inches="tight", pad_inches=0)
194
+ plt.close()
audiotools/core/dsp.py ADDED
@@ -0,0 +1,390 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import typing
2
+
3
+ import julius
4
+ import numpy as np
5
+ import torch
6
+
7
+ from . import util
8
+
9
+
10
+ class DSPMixin:
11
+ _original_batch_size = None
12
+ _original_num_channels = None
13
+ _padded_signal_length = None
14
+
15
+ def _preprocess_signal_for_windowing(self, window_duration, hop_duration):
16
+ self._original_batch_size = self.batch_size
17
+ self._original_num_channels = self.num_channels
18
+
19
+ window_length = int(window_duration * self.sample_rate)
20
+ hop_length = int(hop_duration * self.sample_rate)
21
+
22
+ if window_length % hop_length != 0:
23
+ factor = window_length // hop_length
24
+ window_length = factor * hop_length
25
+
26
+ self.zero_pad(hop_length, hop_length)
27
+ self._padded_signal_length = self.signal_length
28
+
29
+ return window_length, hop_length
30
+
31
+ def windows(
32
+ self, window_duration: float, hop_duration: float, preprocess: bool = True
33
+ ):
34
+ """Generator which yields windows of specified duration from signal with a specified
35
+ hop length.
36
+
37
+ Parameters
38
+ ----------
39
+ window_duration : float
40
+ Duration of every window in seconds.
41
+ hop_duration : float
42
+ Hop between windows in seconds.
43
+ preprocess : bool, optional
44
+ Whether to preprocess the signal, so that the first sample is in
45
+ the middle of the first window, by default True
46
+
47
+ Yields
48
+ ------
49
+ AudioSignal
50
+ Each window is returned as an AudioSignal.
51
+ """
52
+ if preprocess:
53
+ window_length, hop_length = self._preprocess_signal_for_windowing(
54
+ window_duration, hop_duration
55
+ )
56
+
57
+ self.audio_data = self.audio_data.reshape(-1, 1, self.signal_length)
58
+
59
+ for b in range(self.batch_size):
60
+ i = 0
61
+ start_idx = i * hop_length
62
+ while True:
63
+ start_idx = i * hop_length
64
+ i += 1
65
+ end_idx = start_idx + window_length
66
+ if end_idx > self.signal_length:
67
+ break
68
+ yield self[b, ..., start_idx:end_idx]
69
+
70
+ def collect_windows(
71
+ self, window_duration: float, hop_duration: float, preprocess: bool = True
72
+ ):
73
+ """Reshapes signal into windows of specified duration from signal with a specified
74
+ hop length. Window are placed along the batch dimension. Use with
75
+ :py:func:`audiotools.core.dsp.DSPMixin.overlap_and_add` to reconstruct the
76
+ original signal.
77
+
78
+ Parameters
79
+ ----------
80
+ window_duration : float
81
+ Duration of every window in seconds.
82
+ hop_duration : float
83
+ Hop between windows in seconds.
84
+ preprocess : bool, optional
85
+ Whether to preprocess the signal, so that the first sample is in
86
+ the middle of the first window, by default True
87
+
88
+ Returns
89
+ -------
90
+ AudioSignal
91
+ AudioSignal unfolded with shape ``(nb * nch * num_windows, 1, window_length)``
92
+ """
93
+ if preprocess:
94
+ window_length, hop_length = self._preprocess_signal_for_windowing(
95
+ window_duration, hop_duration
96
+ )
97
+
98
+ # self.audio_data: (nb, nch, nt).
99
+ unfolded = torch.nn.functional.unfold(
100
+ self.audio_data.reshape(-1, 1, 1, self.signal_length),
101
+ kernel_size=(1, window_length),
102
+ stride=(1, hop_length),
103
+ )
104
+ # unfolded: (nb * nch, window_length, num_windows).
105
+ # -> (nb * nch * num_windows, 1, window_length)
106
+ unfolded = unfolded.permute(0, 2, 1).reshape(-1, 1, window_length)
107
+ self.audio_data = unfolded
108
+ return self
109
+
110
+ def overlap_and_add(self, hop_duration: float):
111
+ """Function which takes a list of windows and overlap adds them into a
112
+ signal the same length as ``audio_signal``.
113
+
114
+ Parameters
115
+ ----------
116
+ hop_duration : float
117
+ How much to shift for each window
118
+ (overlap is window_duration - hop_duration) in seconds.
119
+
120
+ Returns
121
+ -------
122
+ AudioSignal
123
+ overlap-and-added signal.
124
+ """
125
+ hop_length = int(hop_duration * self.sample_rate)
126
+ window_length = self.signal_length
127
+
128
+ nb, nch = self._original_batch_size, self._original_num_channels
129
+
130
+ unfolded = self.audio_data.reshape(nb * nch, -1, window_length).permute(0, 2, 1)
131
+ folded = torch.nn.functional.fold(
132
+ unfolded,
133
+ output_size=(1, self._padded_signal_length),
134
+ kernel_size=(1, window_length),
135
+ stride=(1, hop_length),
136
+ )
137
+
138
+ norm = torch.ones_like(unfolded, device=unfolded.device)
139
+ norm = torch.nn.functional.fold(
140
+ norm,
141
+ output_size=(1, self._padded_signal_length),
142
+ kernel_size=(1, window_length),
143
+ stride=(1, hop_length),
144
+ )
145
+
146
+ folded = folded / norm
147
+
148
+ folded = folded.reshape(nb, nch, -1)
149
+ self.audio_data = folded
150
+ self.trim(hop_length, hop_length)
151
+ return self
152
+
153
+ def low_pass(
154
+ self, cutoffs: typing.Union[torch.Tensor, np.ndarray, float], zeros: int = 51
155
+ ):
156
+ """Low-passes the signal in-place. Each item in the batch
157
+ can have a different low-pass cutoff, if the input
158
+ to this signal is an array or tensor. If a float, all
159
+ items are given the same low-pass filter.
160
+
161
+ Parameters
162
+ ----------
163
+ cutoffs : typing.Union[torch.Tensor, np.ndarray, float]
164
+ Cutoff in Hz of low-pass filter.
165
+ zeros : int, optional
166
+ Number of taps to use in low-pass filter, by default 51
167
+
168
+ Returns
169
+ -------
170
+ AudioSignal
171
+ Low-passed AudioSignal.
172
+ """
173
+ cutoffs = util.ensure_tensor(cutoffs, 2, self.batch_size)
174
+ cutoffs = cutoffs / self.sample_rate
175
+ filtered = torch.empty_like(self.audio_data)
176
+
177
+ for i, cutoff in enumerate(cutoffs):
178
+ lp_filter = julius.LowPassFilter(cutoff.cpu(), zeros=zeros).to(self.device)
179
+ filtered[i] = lp_filter(self.audio_data[i])
180
+
181
+ self.audio_data = filtered
182
+ self.stft_data = None
183
+ return self
184
+
185
+ def high_pass(
186
+ self, cutoffs: typing.Union[torch.Tensor, np.ndarray, float], zeros: int = 51
187
+ ):
188
+ """High-passes the signal in-place. Each item in the batch
189
+ can have a different high-pass cutoff, if the input
190
+ to this signal is an array or tensor. If a float, all
191
+ items are given the same high-pass filter.
192
+
193
+ Parameters
194
+ ----------
195
+ cutoffs : typing.Union[torch.Tensor, np.ndarray, float]
196
+ Cutoff in Hz of high-pass filter.
197
+ zeros : int, optional
198
+ Number of taps to use in high-pass filter, by default 51
199
+
200
+ Returns
201
+ -------
202
+ AudioSignal
203
+ High-passed AudioSignal.
204
+ """
205
+ cutoffs = util.ensure_tensor(cutoffs, 2, self.batch_size)
206
+ cutoffs = cutoffs / self.sample_rate
207
+ filtered = torch.empty_like(self.audio_data)
208
+
209
+ for i, cutoff in enumerate(cutoffs):
210
+ hp_filter = julius.HighPassFilter(cutoff.cpu(), zeros=zeros).to(self.device)
211
+ filtered[i] = hp_filter(self.audio_data[i])
212
+
213
+ self.audio_data = filtered
214
+ self.stft_data = None
215
+ return self
216
+
217
+ def mask_frequencies(
218
+ self,
219
+ fmin_hz: typing.Union[torch.Tensor, np.ndarray, float],
220
+ fmax_hz: typing.Union[torch.Tensor, np.ndarray, float],
221
+ val: float = 0.0,
222
+ ):
223
+ """Masks frequencies between ``fmin_hz`` and ``fmax_hz``, and fills them
224
+ with the value specified by ``val``. Useful for implementing SpecAug.
225
+ The min and max can be different for every item in the batch.
226
+
227
+ Parameters
228
+ ----------
229
+ fmin_hz : typing.Union[torch.Tensor, np.ndarray, float]
230
+ Lower end of band to mask out.
231
+ fmax_hz : typing.Union[torch.Tensor, np.ndarray, float]
232
+ Upper end of band to mask out.
233
+ val : float, optional
234
+ Value to fill in, by default 0.0
235
+
236
+ Returns
237
+ -------
238
+ AudioSignal
239
+ Signal with ``stft_data`` manipulated. Apply ``.istft()`` to get the
240
+ masked audio data.
241
+ """
242
+ # SpecAug
243
+ mag, phase = self.magnitude, self.phase
244
+ fmin_hz = util.ensure_tensor(fmin_hz, ndim=mag.ndim)
245
+ fmax_hz = util.ensure_tensor(fmax_hz, ndim=mag.ndim)
246
+ assert torch.all(fmin_hz < fmax_hz)
247
+
248
+ # build mask
249
+ nbins = mag.shape[-2]
250
+ bins_hz = torch.linspace(0, self.sample_rate / 2, nbins, device=self.device)
251
+ bins_hz = bins_hz[None, None, :, None].repeat(
252
+ self.batch_size, 1, 1, mag.shape[-1]
253
+ )
254
+ mask = (fmin_hz <= bins_hz) & (bins_hz < fmax_hz)
255
+ mask = mask.to(self.device)
256
+
257
+ mag = mag.masked_fill(mask, val)
258
+ phase = phase.masked_fill(mask, val)
259
+ self.stft_data = mag * torch.exp(1j * phase)
260
+ return self
261
+
262
+ def mask_timesteps(
263
+ self,
264
+ tmin_s: typing.Union[torch.Tensor, np.ndarray, float],
265
+ tmax_s: typing.Union[torch.Tensor, np.ndarray, float],
266
+ val: float = 0.0,
267
+ ):
268
+ """Masks timesteps between ``tmin_s`` and ``tmax_s``, and fills them
269
+ with the value specified by ``val``. Useful for implementing SpecAug.
270
+ The min and max can be different for every item in the batch.
271
+
272
+ Parameters
273
+ ----------
274
+ tmin_s : typing.Union[torch.Tensor, np.ndarray, float]
275
+ Lower end of timesteps to mask out.
276
+ tmax_s : typing.Union[torch.Tensor, np.ndarray, float]
277
+ Upper end of timesteps to mask out.
278
+ val : float, optional
279
+ Value to fill in, by default 0.0
280
+
281
+ Returns
282
+ -------
283
+ AudioSignal
284
+ Signal with ``stft_data`` manipulated. Apply ``.istft()`` to get the
285
+ masked audio data.
286
+ """
287
+ # SpecAug
288
+ mag, phase = self.magnitude, self.phase
289
+ tmin_s = util.ensure_tensor(tmin_s, ndim=mag.ndim)
290
+ tmax_s = util.ensure_tensor(tmax_s, ndim=mag.ndim)
291
+
292
+ assert torch.all(tmin_s < tmax_s)
293
+
294
+ # build mask
295
+ nt = mag.shape[-1]
296
+ bins_t = torch.linspace(0, self.signal_duration, nt, device=self.device)
297
+ bins_t = bins_t[None, None, None, :].repeat(
298
+ self.batch_size, 1, mag.shape[-2], 1
299
+ )
300
+ mask = (tmin_s <= bins_t) & (bins_t < tmax_s)
301
+
302
+ mag = mag.masked_fill(mask, val)
303
+ phase = phase.masked_fill(mask, val)
304
+ self.stft_data = mag * torch.exp(1j * phase)
305
+ return self
306
+
307
+ def mask_low_magnitudes(
308
+ self, db_cutoff: typing.Union[torch.Tensor, np.ndarray, float], val: float = 0.0
309
+ ):
310
+ """Mask away magnitudes below a specified threshold, which
311
+ can be different for every item in the batch.
312
+
313
+ Parameters
314
+ ----------
315
+ db_cutoff : typing.Union[torch.Tensor, np.ndarray, float]
316
+ Decibel value for which things below it will be masked away.
317
+ val : float, optional
318
+ Value to fill in for masked portions, by default 0.0
319
+
320
+ Returns
321
+ -------
322
+ AudioSignal
323
+ Signal with ``stft_data`` manipulated. Apply ``.istft()`` to get the
324
+ masked audio data.
325
+ """
326
+ mag = self.magnitude
327
+ log_mag = self.log_magnitude()
328
+
329
+ db_cutoff = util.ensure_tensor(db_cutoff, ndim=mag.ndim)
330
+ mask = log_mag < db_cutoff
331
+ mag = mag.masked_fill(mask, val)
332
+
333
+ self.magnitude = mag
334
+ return self
335
+
336
+ def shift_phase(self, shift: typing.Union[torch.Tensor, np.ndarray, float]):
337
+ """Shifts the phase by a constant value.
338
+
339
+ Parameters
340
+ ----------
341
+ shift : typing.Union[torch.Tensor, np.ndarray, float]
342
+ What to shift the phase by.
343
+
344
+ Returns
345
+ -------
346
+ AudioSignal
347
+ Signal with ``stft_data`` manipulated. Apply ``.istft()`` to get the
348
+ masked audio data.
349
+ """
350
+ shift = util.ensure_tensor(shift, ndim=self.phase.ndim)
351
+ self.phase = self.phase + shift
352
+ return self
353
+
354
+ def corrupt_phase(self, scale: typing.Union[torch.Tensor, np.ndarray, float]):
355
+ """Corrupts the phase randomly by some scaled value.
356
+
357
+ Parameters
358
+ ----------
359
+ scale : typing.Union[torch.Tensor, np.ndarray, float]
360
+ Standard deviation of noise to add to the phase.
361
+
362
+ Returns
363
+ -------
364
+ AudioSignal
365
+ Signal with ``stft_data`` manipulated. Apply ``.istft()`` to get the
366
+ masked audio data.
367
+ """
368
+ scale = util.ensure_tensor(scale, ndim=self.phase.ndim)
369
+ self.phase = self.phase + scale * torch.randn_like(self.phase)
370
+ return self
371
+
372
+ def preemphasis(self, coef: float = 0.85):
373
+ """Applies pre-emphasis to audio signal.
374
+
375
+ Parameters
376
+ ----------
377
+ coef : float, optional
378
+ How much pre-emphasis to apply, lower values do less. 0 does nothing.
379
+ by default 0.85
380
+
381
+ Returns
382
+ -------
383
+ AudioSignal
384
+ Pre-emphasized signal.
385
+ """
386
+ kernel = torch.tensor([1, -coef, 0]).view(1, 1, -1).to(self.device)
387
+ x = self.audio_data.reshape(-1, 1, self.signal_length)
388
+ x = torch.nn.functional.conv1d(x, kernel, padding=1)
389
+ self.audio_data = x.reshape(*self.audio_data.shape)
390
+ return self
audiotools/core/effects.py ADDED
@@ -0,0 +1,647 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import typing
2
+
3
+ import julius
4
+ import numpy as np
5
+ import torch
6
+ import torchaudio
7
+
8
+ from . import util
9
+
10
+
11
+ class EffectMixin:
12
+ GAIN_FACTOR = np.log(10) / 20
13
+ """Gain factor for converting between amplitude and decibels."""
14
+ CODEC_PRESETS = {
15
+ "8-bit": {"format": "wav", "encoding": "ULAW", "bits_per_sample": 8},
16
+ "GSM-FR": {"format": "gsm"},
17
+ "MP3": {"format": "mp3", "compression": -9},
18
+ "Vorbis": {"format": "vorbis", "compression": -1},
19
+ "Ogg": {
20
+ "format": "ogg",
21
+ "compression": -1,
22
+ },
23
+ "Amr-nb": {"format": "amr-nb"},
24
+ }
25
+ """Presets for applying codecs via torchaudio."""
26
+
27
+ def mix(
28
+ self,
29
+ other,
30
+ snr: typing.Union[torch.Tensor, np.ndarray, float] = 10,
31
+ other_eq: typing.Union[torch.Tensor, np.ndarray] = None,
32
+ ):
33
+ """Mixes noise with signal at specified
34
+ signal-to-noise ratio. Optionally, the
35
+ other signal can be equalized in-place.
36
+
37
+
38
+ Parameters
39
+ ----------
40
+ other : AudioSignal
41
+ AudioSignal object to mix with.
42
+ snr : typing.Union[torch.Tensor, np.ndarray, float], optional
43
+ Signal to noise ratio, by default 10
44
+ other_eq : typing.Union[torch.Tensor, np.ndarray], optional
45
+ EQ curve to apply to other signal, if any, by default None
46
+
47
+ Returns
48
+ -------
49
+ AudioSignal
50
+ In-place modification of AudioSignal.
51
+ """
52
+ snr = util.ensure_tensor(snr).to(self.device)
53
+
54
+ pad_len = max(0, self.signal_length - other.signal_length)
55
+ other.zero_pad(0, pad_len)
56
+ other.truncate_samples(self.signal_length)
57
+ if other_eq is not None:
58
+ other = other.equalizer(other_eq)
59
+
60
+ tgt_loudness = self.loudness() - snr
61
+ other = other.normalize(tgt_loudness)
62
+
63
+ self.audio_data = self.audio_data + other.audio_data
64
+ return self
65
+
66
+ def convolve(self, other, start_at_max: bool = True):
67
+ """Convolves self with other.
68
+ This function uses FFTs to do the convolution.
69
+
70
+ Parameters
71
+ ----------
72
+ other : AudioSignal
73
+ Signal to convolve with.
74
+ start_at_max : bool, optional
75
+ Whether to start at the max value of other signal, to
76
+ avoid inducing delays, by default True
77
+
78
+ Returns
79
+ -------
80
+ AudioSignal
81
+ Convolved signal, in-place.
82
+ """
83
+ from . import AudioSignal
84
+
85
+ pad_len = self.signal_length - other.signal_length
86
+
87
+ if pad_len > 0:
88
+ other.zero_pad(0, pad_len)
89
+ else:
90
+ other.truncate_samples(self.signal_length)
91
+
92
+ if start_at_max:
93
+ # Use roll to rotate over the max for every item
94
+ # so that the impulse responses don't induce any
95
+ # delay.
96
+ idx = other.audio_data.abs().argmax(axis=-1)
97
+ irs = torch.zeros_like(other.audio_data)
98
+ for i in range(other.batch_size):
99
+ irs[i] = torch.roll(other.audio_data[i], -idx[i].item(), -1)
100
+ other = AudioSignal(irs, other.sample_rate)
101
+
102
+ delta = torch.zeros_like(other.audio_data)
103
+ delta[..., 0] = 1
104
+
105
+ length = self.signal_length
106
+ delta_fft = torch.fft.rfft(delta, length)
107
+ other_fft = torch.fft.rfft(other.audio_data, length)
108
+ self_fft = torch.fft.rfft(self.audio_data, length)
109
+
110
+ convolved_fft = other_fft * self_fft
111
+ convolved_audio = torch.fft.irfft(convolved_fft, length)
112
+
113
+ delta_convolved_fft = other_fft * delta_fft
114
+ delta_audio = torch.fft.irfft(delta_convolved_fft, length)
115
+
116
+ # Use the delta to rescale the audio exactly as needed.
117
+ delta_max = delta_audio.abs().max(dim=-1, keepdims=True)[0]
118
+ scale = 1 / delta_max.clamp(1e-5)
119
+ convolved_audio = convolved_audio * scale
120
+
121
+ self.audio_data = convolved_audio
122
+
123
+ return self
124
+
125
+ def apply_ir(
126
+ self,
127
+ ir,
128
+ drr: typing.Union[torch.Tensor, np.ndarray, float] = None,
129
+ ir_eq: typing.Union[torch.Tensor, np.ndarray] = None,
130
+ use_original_phase: bool = False,
131
+ ):
132
+ """Applies an impulse response to the signal. If ` is`ir_eq``
133
+ is specified, the impulse response is equalized before
134
+ it is applied, using the given curve.
135
+
136
+ Parameters
137
+ ----------
138
+ ir : AudioSignal
139
+ Impulse response to convolve with.
140
+ drr : typing.Union[torch.Tensor, np.ndarray, float], optional
141
+ Direct-to-reverberant ratio that impulse response will be
142
+ altered to, if specified, by default None
143
+ ir_eq : typing.Union[torch.Tensor, np.ndarray], optional
144
+ Equalization that will be applied to impulse response
145
+ if specified, by default None
146
+ use_original_phase : bool, optional
147
+ Whether to use the original phase, instead of the convolved
148
+ phase, by default False
149
+
150
+ Returns
151
+ -------
152
+ AudioSignal
153
+ Signal with impulse response applied to it
154
+ """
155
+ if ir_eq is not None:
156
+ ir = ir.equalizer(ir_eq)
157
+ if drr is not None:
158
+ ir = ir.alter_drr(drr)
159
+
160
+ # Save the peak before
161
+ max_spk = self.audio_data.abs().max(dim=-1, keepdims=True).values
162
+
163
+ # Augment the impulse response to simulate microphone effects
164
+ # and with varying direct-to-reverberant ratio.
165
+ phase = self.phase
166
+ self.convolve(ir)
167
+
168
+ # Use the input phase
169
+ if use_original_phase:
170
+ self.stft()
171
+ self.stft_data = self.magnitude * torch.exp(1j * phase)
172
+ self.istft()
173
+
174
+ # Rescale to the input's amplitude
175
+ max_transformed = self.audio_data.abs().max(dim=-1, keepdims=True).values
176
+ scale_factor = max_spk.clamp(1e-8) / max_transformed.clamp(1e-8)
177
+ self = self * scale_factor
178
+
179
+ return self
180
+
181
+ def ensure_max_of_audio(self, max: float = 1.0):
182
+ """Ensures that ``abs(audio_data) <= max``.
183
+
184
+ Parameters
185
+ ----------
186
+ max : float, optional
187
+ Max absolute value of signal, by default 1.0
188
+
189
+ Returns
190
+ -------
191
+ AudioSignal
192
+ Signal with values scaled between -max and max.
193
+ """
194
+ peak = self.audio_data.abs().max(dim=-1, keepdims=True)[0]
195
+ peak_gain = torch.ones_like(peak)
196
+ peak_gain[peak > max] = max / peak[peak > max]
197
+ self.audio_data = self.audio_data * peak_gain
198
+ return self
199
+
200
+ def normalize(self, db: typing.Union[torch.Tensor, np.ndarray, float] = -24.0):
201
+ """Normalizes the signal's volume to the specified db, in LUFS.
202
+ This is GPU-compatible, making for very fast loudness normalization.
203
+
204
+ Parameters
205
+ ----------
206
+ db : typing.Union[torch.Tensor, np.ndarray, float], optional
207
+ Loudness to normalize to, by default -24.0
208
+
209
+ Returns
210
+ -------
211
+ AudioSignal
212
+ Normalized audio signal.
213
+ """
214
+ db = util.ensure_tensor(db).to(self.device)
215
+ ref_db = self.loudness()
216
+ gain = db - ref_db
217
+ gain = torch.exp(gain * self.GAIN_FACTOR)
218
+
219
+ self.audio_data = self.audio_data * gain[:, None, None]
220
+ return self
221
+
222
+ def volume_change(self, db: typing.Union[torch.Tensor, np.ndarray, float]):
223
+ """Change volume of signal by some amount, in dB.
224
+
225
+ Parameters
226
+ ----------
227
+ db : typing.Union[torch.Tensor, np.ndarray, float]
228
+ Amount to change volume by.
229
+
230
+ Returns
231
+ -------
232
+ AudioSignal
233
+ Signal at new volume.
234
+ """
235
+ db = util.ensure_tensor(db, ndim=1).to(self.device)
236
+ gain = torch.exp(db * self.GAIN_FACTOR)
237
+ self.audio_data = self.audio_data * gain[:, None, None]
238
+ return self
239
+
240
+ def _to_2d(self):
241
+ waveform = self.audio_data.reshape(-1, self.signal_length)
242
+ return waveform
243
+
244
+ def _to_3d(self, waveform):
245
+ return waveform.reshape(self.batch_size, self.num_channels, -1)
246
+
247
+ def pitch_shift(self, n_semitones: int, quick: bool = True):
248
+ """Pitch shift the signal. All items in the batch
249
+ get the same pitch shift.
250
+
251
+ Parameters
252
+ ----------
253
+ n_semitones : int
254
+ How many semitones to shift the signal by.
255
+ quick : bool, optional
256
+ Using quick pitch shifting, by default True
257
+
258
+ Returns
259
+ -------
260
+ AudioSignal
261
+ Pitch shifted audio signal.
262
+ """
263
+ device = self.device
264
+ effects = [
265
+ ["pitch", str(n_semitones * 100)],
266
+ ["rate", str(self.sample_rate)],
267
+ ]
268
+ if quick:
269
+ effects[0].insert(1, "-q")
270
+
271
+ waveform = self._to_2d().cpu()
272
+ waveform, sample_rate = torchaudio.sox_effects.apply_effects_tensor(
273
+ waveform, self.sample_rate, effects, channels_first=True
274
+ )
275
+ self.sample_rate = sample_rate
276
+ self.audio_data = self._to_3d(waveform)
277
+ return self.to(device)
278
+
279
+ def time_stretch(self, factor: float, quick: bool = True):
280
+ """Time stretch the audio signal.
281
+
282
+ Parameters
283
+ ----------
284
+ factor : float
285
+ Factor by which to stretch the AudioSignal. Typically
286
+ between 0.8 and 1.2.
287
+ quick : bool, optional
288
+ Whether to use quick time stretching, by default True
289
+
290
+ Returns
291
+ -------
292
+ AudioSignal
293
+ Time-stretched AudioSignal.
294
+ """
295
+ device = self.device
296
+ effects = [
297
+ ["tempo", str(factor)],
298
+ ["rate", str(self.sample_rate)],
299
+ ]
300
+ if quick:
301
+ effects[0].insert(1, "-q")
302
+
303
+ waveform = self._to_2d().cpu()
304
+ waveform, sample_rate = torchaudio.sox_effects.apply_effects_tensor(
305
+ waveform, self.sample_rate, effects, channels_first=True
306
+ )
307
+ self.sample_rate = sample_rate
308
+ self.audio_data = self._to_3d(waveform)
309
+ return self.to(device)
310
+
311
+ def apply_codec(
312
+ self,
313
+ preset: str = None,
314
+ format: str = "wav",
315
+ encoding: str = None,
316
+ bits_per_sample: int = None,
317
+ compression: int = None,
318
+ ): # pragma: no cover
319
+ """Applies an audio codec to the signal.
320
+
321
+ Parameters
322
+ ----------
323
+ preset : str, optional
324
+ One of the keys in ``self.CODEC_PRESETS``, by default None
325
+ format : str, optional
326
+ Format for audio codec, by default "wav"
327
+ encoding : str, optional
328
+ Encoding to use, by default None
329
+ bits_per_sample : int, optional
330
+ How many bits per sample, by default None
331
+ compression : int, optional
332
+ Compression amount of codec, by default None
333
+
334
+ Returns
335
+ -------
336
+ AudioSignal
337
+ AudioSignal with codec applied.
338
+
339
+ Raises
340
+ ------
341
+ ValueError
342
+ If preset is not in ``self.CODEC_PRESETS``, an error
343
+ is thrown.
344
+ """
345
+ torchaudio_version_070 = "0.7" in torchaudio.__version__
346
+ if torchaudio_version_070:
347
+ return self
348
+
349
+ kwargs = {
350
+ "format": format,
351
+ "encoding": encoding,
352
+ "bits_per_sample": bits_per_sample,
353
+ "compression": compression,
354
+ }
355
+
356
+ if preset is not None:
357
+ if preset in self.CODEC_PRESETS:
358
+ kwargs = self.CODEC_PRESETS[preset]
359
+ else:
360
+ raise ValueError(
361
+ f"Unknown preset: {preset}. "
362
+ f"Known presets: {list(self.CODEC_PRESETS.keys())}"
363
+ )
364
+
365
+ waveform = self._to_2d()
366
+ if kwargs["format"] in ["vorbis", "mp3", "ogg", "amr-nb"]:
367
+ # Apply it in a for loop
368
+ augmented = torch.cat(
369
+ [
370
+ torchaudio.functional.apply_codec(
371
+ waveform[i][None, :], self.sample_rate, **kwargs
372
+ )
373
+ for i in range(waveform.shape[0])
374
+ ],
375
+ dim=0,
376
+ )
377
+ else:
378
+ augmented = torchaudio.functional.apply_codec(
379
+ waveform, self.sample_rate, **kwargs
380
+ )
381
+ augmented = self._to_3d(augmented)
382
+
383
+ self.audio_data = augmented
384
+ return self
385
+
386
+ def mel_filterbank(self, n_bands: int):
387
+ """Breaks signal into mel bands.
388
+
389
+ Parameters
390
+ ----------
391
+ n_bands : int
392
+ Number of mel bands to use.
393
+
394
+ Returns
395
+ -------
396
+ torch.Tensor
397
+ Mel-filtered bands, with last axis being the band index.
398
+ """
399
+ filterbank = (
400
+ julius.SplitBands(self.sample_rate, n_bands).float().to(self.device)
401
+ )
402
+ filtered = filterbank(self.audio_data)
403
+ return filtered.permute(1, 2, 3, 0)
404
+
405
+ def equalizer(self, db: typing.Union[torch.Tensor, np.ndarray]):
406
+ """Applies a mel-spaced equalizer to the audio signal.
407
+
408
+ Parameters
409
+ ----------
410
+ db : typing.Union[torch.Tensor, np.ndarray]
411
+ EQ curve to apply.
412
+
413
+ Returns
414
+ -------
415
+ AudioSignal
416
+ AudioSignal with equalization applied.
417
+ """
418
+ db = util.ensure_tensor(db)
419
+ n_bands = db.shape[-1]
420
+ fbank = self.mel_filterbank(n_bands)
421
+
422
+ # If there's a batch dimension, make sure it's the same.
423
+ if db.ndim == 2:
424
+ if db.shape[0] != 1:
425
+ assert db.shape[0] == fbank.shape[0]
426
+ else:
427
+ db = db.unsqueeze(0)
428
+
429
+ weights = (10**db).to(self.device).float()
430
+ fbank = fbank * weights[:, None, None, :]
431
+ eq_audio_data = fbank.sum(-1)
432
+ self.audio_data = eq_audio_data
433
+ return self
434
+
435
+ def clip_distortion(
436
+ self, clip_percentile: typing.Union[torch.Tensor, np.ndarray, float]
437
+ ):
438
+ """Clips the signal at a given percentile. The higher it is,
439
+ the lower the threshold for clipping.
440
+
441
+ Parameters
442
+ ----------
443
+ clip_percentile : typing.Union[torch.Tensor, np.ndarray, float]
444
+ Values are between 0.0 to 1.0. Typical values are 0.1 or below.
445
+
446
+ Returns
447
+ -------
448
+ AudioSignal
449
+ Audio signal with clipped audio data.
450
+ """
451
+ clip_percentile = util.ensure_tensor(clip_percentile, ndim=1)
452
+ min_thresh = torch.quantile(self.audio_data, clip_percentile / 2, dim=-1)
453
+ max_thresh = torch.quantile(self.audio_data, 1 - (clip_percentile / 2), dim=-1)
454
+
455
+ nc = self.audio_data.shape[1]
456
+ min_thresh = min_thresh[:, :nc, :]
457
+ max_thresh = max_thresh[:, :nc, :]
458
+
459
+ self.audio_data = self.audio_data.clamp(min_thresh, max_thresh)
460
+
461
+ return self
462
+
463
+ def quantization(
464
+ self, quantization_channels: typing.Union[torch.Tensor, np.ndarray, int]
465
+ ):
466
+ """Applies quantization to the input waveform.
467
+
468
+ Parameters
469
+ ----------
470
+ quantization_channels : typing.Union[torch.Tensor, np.ndarray, int]
471
+ Number of evenly spaced quantization channels to quantize
472
+ to.
473
+
474
+ Returns
475
+ -------
476
+ AudioSignal
477
+ Quantized AudioSignal.
478
+ """
479
+ quantization_channels = util.ensure_tensor(quantization_channels, ndim=3)
480
+
481
+ x = self.audio_data
482
+ x = (x + 1) / 2
483
+ x = x * quantization_channels
484
+ x = x.floor()
485
+ x = x / quantization_channels
486
+ x = 2 * x - 1
487
+
488
+ residual = (self.audio_data - x).detach()
489
+ self.audio_data = self.audio_data - residual
490
+ return self
491
+
492
+ def mulaw_quantization(
493
+ self, quantization_channels: typing.Union[torch.Tensor, np.ndarray, int]
494
+ ):
495
+ """Applies mu-law quantization to the input waveform.
496
+
497
+ Parameters
498
+ ----------
499
+ quantization_channels : typing.Union[torch.Tensor, np.ndarray, int]
500
+ Number of mu-law spaced quantization channels to quantize
501
+ to.
502
+
503
+ Returns
504
+ -------
505
+ AudioSignal
506
+ Quantized AudioSignal.
507
+ """
508
+ mu = quantization_channels - 1.0
509
+ mu = util.ensure_tensor(mu, ndim=3)
510
+
511
+ x = self.audio_data
512
+
513
+ # quantize
514
+ x = torch.sign(x) * torch.log1p(mu * torch.abs(x)) / torch.log1p(mu)
515
+ x = ((x + 1) / 2 * mu + 0.5).to(torch.int64)
516
+
517
+ # unquantize
518
+ x = (x / mu) * 2 - 1.0
519
+ x = torch.sign(x) * (torch.exp(torch.abs(x) * torch.log1p(mu)) - 1.0) / mu
520
+
521
+ residual = (self.audio_data - x).detach()
522
+ self.audio_data = self.audio_data - residual
523
+ return self
524
+
525
+ def __matmul__(self, other):
526
+ return self.convolve(other)
527
+
528
+
529
+ class ImpulseResponseMixin:
530
+ """These functions are generally only used with AudioSignals that are derived
531
+ from impulse responses, not other sources like music or speech. These methods
532
+ are used to replicate the data augmentation described in [1].
533
+
534
+ 1. Bryan, Nicholas J. "Impulse response data augmentation and deep
535
+ neural networks for blind room acoustic parameter estimation."
536
+ ICASSP 2020-2020 IEEE International Conference on Acoustics,
537
+ Speech and Signal Processing (ICASSP). IEEE, 2020.
538
+ """
539
+
540
+ def decompose_ir(self):
541
+ """Decomposes an impulse response into early and late
542
+ field responses.
543
+ """
544
+ # Equations 1 and 2
545
+ # -----------------
546
+ # Breaking up into early
547
+ # response + late field response.
548
+
549
+ td = torch.argmax(self.audio_data, dim=-1, keepdim=True)
550
+ t0 = int(self.sample_rate * 0.0025)
551
+
552
+ idx = torch.arange(self.audio_data.shape[-1], device=self.device)[None, None, :]
553
+ idx = idx.expand(self.batch_size, -1, -1)
554
+ early_idx = (idx >= td - t0) * (idx <= td + t0)
555
+
556
+ early_response = torch.zeros_like(self.audio_data, device=self.device)
557
+ early_response[early_idx] = self.audio_data[early_idx]
558
+
559
+ late_idx = ~early_idx
560
+ late_field = torch.zeros_like(self.audio_data, device=self.device)
561
+ late_field[late_idx] = self.audio_data[late_idx]
562
+
563
+ # Equation 4
564
+ # ----------
565
+ # Decompose early response into windowed
566
+ # direct path and windowed residual.
567
+
568
+ window = torch.zeros_like(self.audio_data, device=self.device)
569
+ for idx in range(self.batch_size):
570
+ window_idx = early_idx[idx, 0].nonzero()
571
+ window[idx, ..., window_idx] = self.get_window(
572
+ "hann", window_idx.shape[-1], self.device
573
+ )
574
+ return early_response, late_field, window
575
+
576
+ def measure_drr(self):
577
+ """Measures the direct-to-reverberant ratio of the impulse
578
+ response.
579
+
580
+ Returns
581
+ -------
582
+ float
583
+ Direct-to-reverberant ratio
584
+ """
585
+ early_response, late_field, _ = self.decompose_ir()
586
+ num = (early_response**2).sum(dim=-1)
587
+ den = (late_field**2).sum(dim=-1)
588
+ drr = 10 * torch.log10(num / den)
589
+ return drr
590
+
591
+ @staticmethod
592
+ def solve_alpha(early_response, late_field, wd, target_drr):
593
+ """Used to solve for the alpha value, which is used
594
+ to alter the drr.
595
+ """
596
+ # Equation 5
597
+ # ----------
598
+ # Apply the good ol' quadratic formula.
599
+
600
+ wd_sq = wd**2
601
+ wd_sq_1 = (1 - wd) ** 2
602
+ e_sq = early_response**2
603
+ l_sq = late_field**2
604
+ a = (wd_sq * e_sq).sum(dim=-1)
605
+ b = (2 * (1 - wd) * wd * e_sq).sum(dim=-1)
606
+ c = (wd_sq_1 * e_sq).sum(dim=-1) - torch.pow(10, target_drr / 10) * l_sq.sum(
607
+ dim=-1
608
+ )
609
+
610
+ expr = ((b**2) - 4 * a * c).sqrt()
611
+ alpha = torch.maximum(
612
+ (-b - expr) / (2 * a),
613
+ (-b + expr) / (2 * a),
614
+ )
615
+ return alpha
616
+
617
+ def alter_drr(self, drr: typing.Union[torch.Tensor, np.ndarray, float]):
618
+ """Alters the direct-to-reverberant ratio of the impulse response.
619
+
620
+ Parameters
621
+ ----------
622
+ drr : typing.Union[torch.Tensor, np.ndarray, float]
623
+ Direct-to-reverberant ratio that impulse response will be
624
+ altered to, if specified, by default None
625
+
626
+ Returns
627
+ -------
628
+ AudioSignal
629
+ Altered impulse response.
630
+ """
631
+ drr = util.ensure_tensor(drr, 2, self.batch_size).to(self.device)
632
+
633
+ early_response, late_field, window = self.decompose_ir()
634
+ alpha = self.solve_alpha(early_response, late_field, window, drr)
635
+ min_alpha = (
636
+ late_field.abs().max(dim=-1)[0] / early_response.abs().max(dim=-1)[0]
637
+ )
638
+ alpha = torch.maximum(alpha, min_alpha)[..., None]
639
+
640
+ aug_ir_data = (
641
+ alpha * window * early_response
642
+ + ((1 - window) * early_response)
643
+ + late_field
644
+ )
645
+ self.audio_data = aug_ir_data
646
+ self.ensure_max_of_audio()
647
+ return self
audiotools/core/ffmpeg.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import shlex
3
+ import subprocess
4
+ import tempfile
5
+ from pathlib import Path
6
+ from typing import Tuple
7
+
8
+ import ffmpy
9
+ import numpy as np
10
+ import torch
11
+
12
+
13
+ def r128stats(filepath: str, quiet: bool):
14
+ """Takes a path to an audio file, returns a dict with the loudness
15
+ stats computed by the ffmpeg ebur128 filter.
16
+
17
+ Parameters
18
+ ----------
19
+ filepath : str
20
+ Path to compute loudness stats on.
21
+ quiet : bool
22
+ Whether to show FFMPEG output during computation.
23
+
24
+ Returns
25
+ -------
26
+ dict
27
+ Dictionary containing loudness stats.
28
+ """
29
+ ffargs = [
30
+ "ffmpeg",
31
+ "-nostats",
32
+ "-i",
33
+ filepath,
34
+ "-filter_complex",
35
+ "ebur128",
36
+ "-f",
37
+ "null",
38
+ "-",
39
+ ]
40
+ if quiet:
41
+ ffargs += ["-hide_banner"]
42
+ proc = subprocess.Popen(ffargs, stderr=subprocess.PIPE, universal_newlines=True)
43
+ stats = proc.communicate()[1]
44
+ summary_index = stats.rfind("Summary:")
45
+
46
+ summary_list = stats[summary_index:].split()
47
+ i_lufs = float(summary_list[summary_list.index("I:") + 1])
48
+ i_thresh = float(summary_list[summary_list.index("I:") + 4])
49
+ lra = float(summary_list[summary_list.index("LRA:") + 1])
50
+ lra_thresh = float(summary_list[summary_list.index("LRA:") + 4])
51
+ lra_low = float(summary_list[summary_list.index("low:") + 1])
52
+ lra_high = float(summary_list[summary_list.index("high:") + 1])
53
+ stats_dict = {
54
+ "I": i_lufs,
55
+ "I Threshold": i_thresh,
56
+ "LRA": lra,
57
+ "LRA Threshold": lra_thresh,
58
+ "LRA Low": lra_low,
59
+ "LRA High": lra_high,
60
+ }
61
+
62
+ return stats_dict
63
+
64
+
65
+ def ffprobe_offset_and_codec(path: str) -> Tuple[float, str]:
66
+ """Given a path to a file, returns the start time offset and codec of
67
+ the first audio stream.
68
+ """
69
+ ff = ffmpy.FFprobe(
70
+ inputs={path: None},
71
+ global_options="-show_entries format=start_time:stream=duration,start_time,codec_type,codec_name,start_pts,time_base -of json -v quiet",
72
+ )
73
+ streams = json.loads(ff.run(stdout=subprocess.PIPE)[0])["streams"]
74
+ seconds_offset = 0.0
75
+ codec = None
76
+
77
+ # Get the offset and codec of the first audio stream we find
78
+ # and return its start time, if it has one.
79
+ for stream in streams:
80
+ if stream["codec_type"] == "audio":
81
+ seconds_offset = stream.get("start_time", 0.0)
82
+ codec = stream.get("codec_name")
83
+ break
84
+ return float(seconds_offset), codec
85
+
86
+
87
+ class FFMPEGMixin:
88
+ _loudness = None
89
+
90
+ def ffmpeg_loudness(self, quiet: bool = True):
91
+ """Computes loudness of audio file using FFMPEG.
92
+
93
+ Parameters
94
+ ----------
95
+ quiet : bool, optional
96
+ Whether to show FFMPEG output during computation,
97
+ by default True
98
+
99
+ Returns
100
+ -------
101
+ torch.Tensor
102
+ Loudness of every item in the batch, computed via
103
+ FFMPEG.
104
+ """
105
+ loudness = []
106
+
107
+ with tempfile.NamedTemporaryFile(suffix=".wav") as f:
108
+ for i in range(self.batch_size):
109
+ self[i].write(f.name)
110
+ loudness_stats = r128stats(f.name, quiet=quiet)
111
+ loudness.append(loudness_stats["I"])
112
+
113
+ self._loudness = torch.from_numpy(np.array(loudness)).float()
114
+ return self.loudness()
115
+
116
+ def ffmpeg_resample(self, sample_rate: int, quiet: bool = True):
117
+ """Resamples AudioSignal using FFMPEG. More memory-efficient
118
+ than using julius.resample for long audio files.
119
+
120
+ Parameters
121
+ ----------
122
+ sample_rate : int
123
+ Sample rate to resample to.
124
+ quiet : bool, optional
125
+ Whether to show FFMPEG output during computation,
126
+ by default True
127
+
128
+ Returns
129
+ -------
130
+ AudioSignal
131
+ Resampled AudioSignal.
132
+ """
133
+ from audiotools import AudioSignal
134
+
135
+ if sample_rate == self.sample_rate:
136
+ return self
137
+
138
+ with tempfile.NamedTemporaryFile(suffix=".wav") as f:
139
+ self.write(f.name)
140
+ f_out = f.name.replace("wav", "rs.wav")
141
+ command = f"ffmpeg -i {f.name} -ar {sample_rate} {f_out}"
142
+ if quiet:
143
+ command += " -hide_banner -loglevel error"
144
+ subprocess.check_call(shlex.split(command))
145
+ resampled = AudioSignal(f_out)
146
+ Path.unlink(Path(f_out))
147
+ return resampled
148
+
149
+ @classmethod
150
+ def load_from_file_with_ffmpeg(cls, audio_path: str, quiet: bool = True, **kwargs):
151
+ """Loads AudioSignal object after decoding it to a wav file using FFMPEG.
152
+ Useful for loading audio that isn't covered by librosa's loading mechanism. Also
153
+ useful for loading mp3 files, without any offset.
154
+
155
+ Parameters
156
+ ----------
157
+ audio_path : str
158
+ Path to load AudioSignal from.
159
+ quiet : bool, optional
160
+ Whether to show FFMPEG output during computation,
161
+ by default True
162
+
163
+ Returns
164
+ -------
165
+ AudioSignal
166
+ AudioSignal loaded from file with FFMPEG.
167
+ """
168
+ audio_path = str(audio_path)
169
+ with tempfile.TemporaryDirectory() as d:
170
+ wav_file = str(Path(d) / "extracted.wav")
171
+ padded_wav = str(Path(d) / "padded.wav")
172
+
173
+ global_options = "-y"
174
+ if quiet:
175
+ global_options += " -loglevel error"
176
+
177
+ ff = ffmpy.FFmpeg(
178
+ inputs={audio_path: None},
179
+ outputs={wav_file: None},
180
+ global_options=global_options,
181
+ )
182
+ ff.run()
183
+
184
+ # We pad the file using the start time offset in case it's an audio
185
+ # stream starting at some offset in a video container.
186
+ pad, codec = ffprobe_offset_and_codec(audio_path)
187
+
188
+ # For mp3s, don't pad files with discrepancies less than 0.027s -
189
+ # it's likely due to codec latency. The amount of latency introduced
190
+ # by mp3 is 1152, which is 0.0261 44khz. So we set the threshold
191
+ # here slightly above that.
192
+ # Source: https://lame.sourceforge.io/tech-FAQ.txt.
193
+ if codec == "mp3" and pad < 0.027:
194
+ pad = 0.0
195
+ ff = ffmpy.FFmpeg(
196
+ inputs={wav_file: None},
197
+ outputs={padded_wav: f"-af 'adelay={pad*1000}:all=true'"},
198
+ global_options=global_options,
199
+ )
200
+ ff.run()
201
+
202
+ signal = cls(padded_wav, **kwargs)
203
+
204
+ return signal
audiotools/core/loudness.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+
3
+ import julius
4
+ import numpy as np
5
+ import scipy
6
+ import torch
7
+ import torch.nn.functional as F
8
+ import torchaudio
9
+
10
+
11
+ class Meter(torch.nn.Module):
12
+ """Tensorized version of pyloudnorm.Meter. Works with batched audio tensors.
13
+
14
+ Parameters
15
+ ----------
16
+ rate : int
17
+ Sample rate of audio.
18
+ filter_class : str, optional
19
+ Class of weighting filter used.
20
+ K-weighting' (default), 'Fenton/Lee 1'
21
+ 'Fenton/Lee 2', 'Dash et al.'
22
+ by default "K-weighting"
23
+ block_size : float, optional
24
+ Gating block size in seconds, by default 0.400
25
+ zeros : int, optional
26
+ Number of zeros to use in FIR approximation of
27
+ IIR filters, by default 512
28
+ use_fir : bool, optional
29
+ Whether to use FIR approximation or exact IIR formulation.
30
+ If computing on GPU, ``use_fir=True`` will be used, as its
31
+ much faster, by default False
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ rate: int,
37
+ filter_class: str = "K-weighting",
38
+ block_size: float = 0.400,
39
+ zeros: int = 512,
40
+ use_fir: bool = False,
41
+ ):
42
+ super().__init__()
43
+
44
+ self.rate = rate
45
+ self.filter_class = filter_class
46
+ self.block_size = block_size
47
+ self.use_fir = use_fir
48
+
49
+ G = torch.from_numpy(np.array([1.0, 1.0, 1.0, 1.41, 1.41]))
50
+ self.register_buffer("G", G)
51
+
52
+ # Compute impulse responses so that filtering is fast via
53
+ # a convolution at runtime, on GPU, unlike lfilter.
54
+ impulse = np.zeros((zeros,))
55
+ impulse[..., 0] = 1.0
56
+
57
+ firs = np.zeros((len(self._filters), 1, zeros))
58
+ passband_gain = torch.zeros(len(self._filters))
59
+
60
+ for i, (_, filter_stage) in enumerate(self._filters.items()):
61
+ firs[i] = scipy.signal.lfilter(filter_stage.b, filter_stage.a, impulse)
62
+ passband_gain[i] = filter_stage.passband_gain
63
+
64
+ firs = torch.from_numpy(firs[..., ::-1].copy()).float()
65
+
66
+ self.register_buffer("firs", firs)
67
+ self.register_buffer("passband_gain", passband_gain)
68
+
69
+ def apply_filter_gpu(self, data: torch.Tensor):
70
+ """Performs FIR approximation of loudness computation.
71
+
72
+ Parameters
73
+ ----------
74
+ data : torch.Tensor
75
+ Audio data of shape (nb, nch, nt).
76
+
77
+ Returns
78
+ -------
79
+ torch.Tensor
80
+ Filtered audio data.
81
+ """
82
+ # Data is of shape (nb, nch, nt)
83
+ # Reshape to (nb*nch, 1, nt)
84
+ nb, nt, nch = data.shape
85
+ data = data.permute(0, 2, 1)
86
+ data = data.reshape(nb * nch, 1, nt)
87
+
88
+ # Apply padding
89
+ pad_length = self.firs.shape[-1]
90
+
91
+ # Apply filtering in sequence
92
+ for i in range(self.firs.shape[0]):
93
+ data = F.pad(data, (pad_length, pad_length))
94
+ data = julius.fftconv.fft_conv1d(data, self.firs[i, None, ...])
95
+ data = self.passband_gain[i] * data
96
+ data = data[..., 1 : nt + 1]
97
+
98
+ data = data.permute(0, 2, 1)
99
+ data = data[:, :nt, :]
100
+ return data
101
+
102
+ def apply_filter_cpu(self, data: torch.Tensor):
103
+ """Performs IIR formulation of loudness computation.
104
+
105
+ Parameters
106
+ ----------
107
+ data : torch.Tensor
108
+ Audio data of shape (nb, nch, nt).
109
+
110
+ Returns
111
+ -------
112
+ torch.Tensor
113
+ Filtered audio data.
114
+ """
115
+ for _, filter_stage in self._filters.items():
116
+ passband_gain = filter_stage.passband_gain
117
+
118
+ a_coeffs = torch.from_numpy(filter_stage.a).float().to(data.device)
119
+ b_coeffs = torch.from_numpy(filter_stage.b).float().to(data.device)
120
+
121
+ _data = data.permute(0, 2, 1)
122
+ filtered = torchaudio.functional.lfilter(
123
+ _data, a_coeffs, b_coeffs, clamp=False
124
+ )
125
+ data = passband_gain * filtered.permute(0, 2, 1)
126
+ return data
127
+
128
+ def apply_filter(self, data: torch.Tensor):
129
+ """Applies filter on either CPU or GPU, depending
130
+ on if the audio is on GPU or is on CPU, or if
131
+ ``self.use_fir`` is True.
132
+
133
+ Parameters
134
+ ----------
135
+ data : torch.Tensor
136
+ Audio data of shape (nb, nch, nt).
137
+
138
+ Returns
139
+ -------
140
+ torch.Tensor
141
+ Filtered audio data.
142
+ """
143
+ if data.is_cuda or self.use_fir:
144
+ data = self.apply_filter_gpu(data)
145
+ else:
146
+ data = self.apply_filter_cpu(data)
147
+ return data
148
+
149
+ def forward(self, data: torch.Tensor):
150
+ """Computes integrated loudness of data.
151
+
152
+ Parameters
153
+ ----------
154
+ data : torch.Tensor
155
+ Audio data of shape (nb, nch, nt).
156
+
157
+ Returns
158
+ -------
159
+ torch.Tensor
160
+ Filtered audio data.
161
+ """
162
+ return self.integrated_loudness(data)
163
+
164
+ def _unfold(self, input_data):
165
+ T_g = self.block_size
166
+ overlap = 0.75 # overlap of 75% of the block duration
167
+ step = 1.0 - overlap # step size by percentage
168
+
169
+ kernel_size = int(T_g * self.rate)
170
+ stride = int(T_g * self.rate * step)
171
+ unfolded = julius.core.unfold(input_data.permute(0, 2, 1), kernel_size, stride)
172
+ unfolded = unfolded.transpose(-1, -2)
173
+
174
+ return unfolded
175
+
176
+ def integrated_loudness(self, data: torch.Tensor):
177
+ """Computes integrated loudness of data.
178
+
179
+ Parameters
180
+ ----------
181
+ data : torch.Tensor
182
+ Audio data of shape (nb, nch, nt).
183
+
184
+ Returns
185
+ -------
186
+ torch.Tensor
187
+ Filtered audio data.
188
+ """
189
+ if not torch.is_tensor(data):
190
+ data = torch.from_numpy(data).float()
191
+ else:
192
+ data = data.float()
193
+
194
+ input_data = copy.copy(data)
195
+ # Data always has a batch and channel dimension.
196
+ # Is of shape (nb, nt, nch)
197
+ if input_data.ndim < 2:
198
+ input_data = input_data.unsqueeze(-1)
199
+ if input_data.ndim < 3:
200
+ input_data = input_data.unsqueeze(0)
201
+
202
+ nb, nt, nch = input_data.shape
203
+
204
+ # Apply frequency weighting filters - account
205
+ # for the acoustic respose of the head and auditory system
206
+ input_data = self.apply_filter(input_data)
207
+
208
+ G = self.G # channel gains
209
+ T_g = self.block_size # 400 ms gating block standard
210
+ Gamma_a = -70.0 # -70 LKFS = absolute loudness threshold
211
+
212
+ unfolded = self._unfold(input_data)
213
+
214
+ z = (1.0 / (T_g * self.rate)) * unfolded.square().sum(2)
215
+ l = -0.691 + 10.0 * torch.log10((G[None, :nch, None] * z).sum(1, keepdim=True))
216
+ l = l.expand_as(z)
217
+
218
+ # find gating block indices above absolute threshold
219
+ z_avg_gated = z
220
+ z_avg_gated[l <= Gamma_a] = 0
221
+ masked = l > Gamma_a
222
+ z_avg_gated = z_avg_gated.sum(2) / masked.sum(2)
223
+
224
+ # calculate the relative threshold value (see eq. 6)
225
+ Gamma_r = (
226
+ -0.691 + 10.0 * torch.log10((z_avg_gated * G[None, :nch]).sum(-1)) - 10.0
227
+ )
228
+ Gamma_r = Gamma_r[:, None, None]
229
+ Gamma_r = Gamma_r.expand(nb, nch, l.shape[-1])
230
+
231
+ # find gating block indices above relative and absolute thresholds (end of eq. 7)
232
+ z_avg_gated = z
233
+ z_avg_gated[l <= Gamma_a] = 0
234
+ z_avg_gated[l <= Gamma_r] = 0
235
+ masked = (l > Gamma_a) * (l > Gamma_r)
236
+ z_avg_gated = z_avg_gated.sum(2) / masked.sum(2)
237
+
238
+ # # Cannot use nan_to_num (pytorch 1.8 does not come with GCP-supported cuda version)
239
+ # z_avg_gated = torch.nan_to_num(z_avg_gated)
240
+ z_avg_gated = torch.where(
241
+ z_avg_gated.isnan(), torch.zeros_like(z_avg_gated), z_avg_gated
242
+ )
243
+ z_avg_gated[z_avg_gated == float("inf")] = float(np.finfo(np.float32).max)
244
+ z_avg_gated[z_avg_gated == -float("inf")] = float(np.finfo(np.float32).min)
245
+
246
+ LUFS = -0.691 + 10.0 * torch.log10((G[None, :nch] * z_avg_gated).sum(1))
247
+ return LUFS.float()
248
+
249
+ @property
250
+ def filter_class(self):
251
+ return self._filter_class
252
+
253
+ @filter_class.setter
254
+ def filter_class(self, value):
255
+ from pyloudnorm import Meter
256
+
257
+ meter = Meter(self.rate)
258
+ meter.filter_class = value
259
+ self._filter_class = value
260
+ self._filters = meter._filters
261
+
262
+
263
+ class LoudnessMixin:
264
+ _loudness = None
265
+ MIN_LOUDNESS = -70
266
+ """Minimum loudness possible."""
267
+
268
+ def loudness(
269
+ self, filter_class: str = "K-weighting", block_size: float = 0.400, **kwargs
270
+ ):
271
+ """Calculates loudness using an implementation of ITU-R BS.1770-4.
272
+ Allows control over gating block size and frequency weighting filters for
273
+ additional control. Measure the integrated gated loudness of a signal.
274
+
275
+ API is derived from PyLoudnorm, but this implementation is ported to PyTorch
276
+ and is tensorized across batches. When on GPU, an FIR approximation of the IIR
277
+ filters is used to compute loudness for speed.
278
+
279
+ Uses the weighting filters and block size defined by the meter
280
+ the integrated loudness is measured based upon the gating algorithm
281
+ defined in the ITU-R BS.1770-4 specification.
282
+
283
+ Parameters
284
+ ----------
285
+ filter_class : str, optional
286
+ Class of weighting filter used.
287
+ K-weighting' (default), 'Fenton/Lee 1'
288
+ 'Fenton/Lee 2', 'Dash et al.'
289
+ by default "K-weighting"
290
+ block_size : float, optional
291
+ Gating block size in seconds, by default 0.400
292
+ kwargs : dict, optional
293
+ Keyword arguments to :py:func:`audiotools.core.loudness.Meter`.
294
+
295
+ Returns
296
+ -------
297
+ torch.Tensor
298
+ Loudness of audio data.
299
+ """
300
+ if self._loudness is not None:
301
+ return self._loudness.to(self.device)
302
+ original_length = self.signal_length
303
+ if self.signal_duration < 0.5:
304
+ pad_len = int((0.5 - self.signal_duration) * self.sample_rate)
305
+ self.zero_pad(0, pad_len)
306
+
307
+ # create BS.1770 meter
308
+ meter = Meter(
309
+ self.sample_rate, filter_class=filter_class, block_size=block_size, **kwargs
310
+ )
311
+ meter = meter.to(self.device)
312
+ # measure loudness
313
+ loudness = meter.integrated_loudness(self.audio_data.permute(0, 2, 1))
314
+ self.truncate_samples(original_length)
315
+ min_loudness = (
316
+ torch.ones_like(loudness, device=loudness.device) * self.MIN_LOUDNESS
317
+ )
318
+ self._loudness = torch.maximum(loudness, min_loudness)
319
+
320
+ return self._loudness.to(self.device)
audiotools/core/playback.py ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ These are utilities that allow one to embed an AudioSignal
3
+ as a playable object in a Jupyter notebook, or to play audio from
4
+ the terminal, etc.
5
+ """ # fmt: skip
6
+ import base64
7
+ import io
8
+ import random
9
+ import string
10
+ import subprocess
11
+ from tempfile import NamedTemporaryFile
12
+
13
+ import importlib_resources as pkg_resources
14
+
15
+ from . import templates
16
+ from .util import _close_temp_files
17
+ from .util import format_figure
18
+
19
+ headers = pkg_resources.files(templates).joinpath("headers.html").read_text()
20
+ widget = pkg_resources.files(templates).joinpath("widget.html").read_text()
21
+
22
+ DEFAULT_EXTENSION = ".wav"
23
+
24
+
25
+ def _check_imports(): # pragma: no cover
26
+ try:
27
+ import ffmpy
28
+ except:
29
+ ffmpy = False
30
+
31
+ try:
32
+ import IPython
33
+ except:
34
+ raise ImportError("IPython must be installed in order to use this function!")
35
+ return ffmpy, IPython
36
+
37
+
38
+ class PlayMixin:
39
+ def embed(self, ext: str = None, display: bool = True, return_html: bool = False):
40
+ """Embeds audio as a playable audio embed in a notebook, or HTML
41
+ document, etc.
42
+
43
+ Parameters
44
+ ----------
45
+ ext : str, optional
46
+ Extension to use when saving the audio, by default ".wav"
47
+ display : bool, optional
48
+ This controls whether or not to display the audio when called. This
49
+ is used when the embed is the last line in a Jupyter cell, to prevent
50
+ the audio from being embedded twice, by default True
51
+ return_html : bool, optional
52
+ Whether to return the data wrapped in an HTML audio element, by default False
53
+
54
+ Returns
55
+ -------
56
+ str
57
+ Either the element for display, or the HTML string of it.
58
+ """
59
+ if ext is None:
60
+ ext = DEFAULT_EXTENSION
61
+ ext = f".{ext}" if not ext.startswith(".") else ext
62
+ ffmpy, IPython = _check_imports()
63
+ sr = self.sample_rate
64
+ tmpfiles = []
65
+
66
+ with _close_temp_files(tmpfiles):
67
+ tmp_wav = NamedTemporaryFile(mode="w+", suffix=".wav", delete=False)
68
+ tmpfiles.append(tmp_wav)
69
+ self.write(tmp_wav.name)
70
+ if ext != ".wav" and ffmpy:
71
+ tmp_converted = NamedTemporaryFile(mode="w+", suffix=ext, delete=False)
72
+ tmpfiles.append(tmp_wav)
73
+ ff = ffmpy.FFmpeg(
74
+ inputs={tmp_wav.name: None},
75
+ outputs={
76
+ tmp_converted.name: "-write_xing 0 -codec:a libmp3lame -b:a 128k -y -hide_banner -loglevel error"
77
+ },
78
+ )
79
+ ff.run()
80
+ else:
81
+ tmp_converted = tmp_wav
82
+
83
+ audio_element = IPython.display.Audio(data=tmp_converted.name, rate=sr)
84
+ if display:
85
+ IPython.display.display(audio_element)
86
+
87
+ if return_html:
88
+ audio_element = (
89
+ f"<audio "
90
+ f" controls "
91
+ f" src='{audio_element.src_attr()}'> "
92
+ f"</audio> "
93
+ )
94
+ return audio_element
95
+
96
+ def widget(
97
+ self,
98
+ title: str = None,
99
+ ext: str = ".wav",
100
+ add_headers: bool = True,
101
+ player_width: str = "100%",
102
+ margin: str = "10px",
103
+ plot_fn: str = "specshow",
104
+ return_html: bool = False,
105
+ **kwargs,
106
+ ):
107
+ """Creates a playable widget with spectrogram. Inspired (heavily) by
108
+ https://sjvasquez.github.io/blog/melnet/.
109
+
110
+ Parameters
111
+ ----------
112
+ title : str, optional
113
+ Title of plot, placed in upper right of top-most axis.
114
+ ext : str, optional
115
+ Extension for embedding, by default ".mp3"
116
+ add_headers : bool, optional
117
+ Whether or not to add headers (use for first embed, False for later embeds), by default True
118
+ player_width : str, optional
119
+ Width of the player, as a string in a CSS rule, by default "100%"
120
+ margin : str, optional
121
+ Margin on all sides of player, by default "10px"
122
+ plot_fn : function, optional
123
+ Plotting function to use (by default self.specshow).
124
+ return_html : bool, optional
125
+ Whether to return the data wrapped in an HTML audio element, by default False
126
+ kwargs : dict, optional
127
+ Keyword arguments to plot_fn (by default self.specshow).
128
+
129
+ Returns
130
+ -------
131
+ HTML
132
+ HTML object.
133
+ """
134
+ import matplotlib.pyplot as plt
135
+
136
+ def _save_fig_to_tag():
137
+ buffer = io.BytesIO()
138
+
139
+ plt.savefig(buffer, bbox_inches="tight", pad_inches=0)
140
+ plt.close()
141
+
142
+ buffer.seek(0)
143
+ data_uri = base64.b64encode(buffer.read()).decode("ascii")
144
+ tag = "data:image/png;base64,{0}".format(data_uri)
145
+
146
+ return tag
147
+
148
+ _, IPython = _check_imports()
149
+
150
+ header_html = ""
151
+
152
+ if add_headers:
153
+ header_html = headers.replace("PLAYER_WIDTH", str(player_width))
154
+ header_html = header_html.replace("MARGIN", str(margin))
155
+ IPython.display.display(IPython.display.HTML(header_html))
156
+
157
+ widget_html = widget
158
+ if isinstance(plot_fn, str):
159
+ plot_fn = getattr(self, plot_fn)
160
+ kwargs["title"] = title
161
+ plot_fn(**kwargs)
162
+
163
+ fig = plt.gcf()
164
+ pixels = fig.get_size_inches() * fig.dpi
165
+
166
+ tag = _save_fig_to_tag()
167
+
168
+ # Make the source image for the levels
169
+ self.specshow()
170
+ format_figure((12, 1.5))
171
+ levels_tag = _save_fig_to_tag()
172
+
173
+ player_id = "".join(random.choice(string.ascii_uppercase) for _ in range(10))
174
+
175
+ audio_elem = self.embed(ext=ext, display=False)
176
+ widget_html = widget_html.replace("AUDIO_SRC", audio_elem.src_attr())
177
+ widget_html = widget_html.replace("IMAGE_SRC", tag)
178
+ widget_html = widget_html.replace("LEVELS_SRC", levels_tag)
179
+ widget_html = widget_html.replace("PLAYER_ID", player_id)
180
+
181
+ # Calculate width/height of figure based on figure size.
182
+ widget_html = widget_html.replace("PADDING_AMOUNT", f"{int(pixels[1])}px")
183
+ widget_html = widget_html.replace("MAX_WIDTH", f"{int(pixels[0])}px")
184
+
185
+ IPython.display.display(IPython.display.HTML(widget_html))
186
+
187
+ if return_html:
188
+ html = header_html if add_headers else ""
189
+ html += widget_html
190
+ return html
191
+
192
+ def play(self):
193
+ """
194
+ Plays an audio signal if ffplay from the ffmpeg suite of tools is installed.
195
+ Otherwise, will fail. The audio signal is written to a temporary file
196
+ and then played with ffplay.
197
+ """
198
+ tmpfiles = []
199
+ with _close_temp_files(tmpfiles):
200
+ tmp_wav = NamedTemporaryFile(suffix=".wav", delete=False)
201
+ tmpfiles.append(tmp_wav)
202
+ self.write(tmp_wav.name)
203
+ print(self)
204
+ subprocess.call(
205
+ [
206
+ "ffplay",
207
+ "-nodisp",
208
+ "-autoexit",
209
+ "-hide_banner",
210
+ "-loglevel",
211
+ "error",
212
+ tmp_wav.name,
213
+ ]
214
+ )
215
+ return self
216
+
217
+
218
+ if __name__ == "__main__": # pragma: no cover
219
+ from audiotools import AudioSignal
220
+
221
+ signal = AudioSignal(
222
+ "tests/audio/spk/f10_script4_produced.mp3", offset=5, duration=5
223
+ )
224
+
225
+ wave_html = signal.widget(
226
+ "Waveform",
227
+ plot_fn="waveplot",
228
+ return_html=True,
229
+ )
230
+
231
+ spec_html = signal.widget("Spectrogram", return_html=True, add_headers=False)
232
+
233
+ combined_html = signal.widget(
234
+ "Waveform + spectrogram",
235
+ plot_fn="wavespec",
236
+ return_html=True,
237
+ add_headers=False,
238
+ )
239
+
240
+ signal.low_pass(8000)
241
+ lowpass_html = signal.widget(
242
+ "Lowpassed audio",
243
+ plot_fn="wavespec",
244
+ return_html=True,
245
+ add_headers=False,
246
+ )
247
+
248
+ with open("/tmp/index.html", "w") as f:
249
+ f.write(wave_html)
250
+ f.write(spec_html)
251
+ f.write(combined_html)
252
+ f.write(lowpass_html)
audiotools/core/templates/__init__.py ADDED
File without changes
audiotools/core/templates/headers.html ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <style>
2
+ .player {
3
+ width: 100%;
4
+ /*border: 1px solid black;*/
5
+ margin: 10px;
6
+ }
7
+
8
+ .underlay img {
9
+ width: 100%;
10
+ height: 100%;
11
+ }
12
+
13
+ .spectrogram {
14
+ height: 0;
15
+ width: 100%;
16
+ position: relative;
17
+ }
18
+
19
+ .audio-controls {
20
+ width: 100%;
21
+ height: 54px;
22
+ display: flex;
23
+ /*border-top: 1px solid black;*/
24
+ /*background-color: rgb(241, 243, 244);*/
25
+ background-color: rgb(248, 248, 248);
26
+ background-color: rgb(253, 253, 254);
27
+ border: 1px solid rgb(205, 208, 211);
28
+ margin-top: 20px;
29
+ /*border: 1px solid black;*/
30
+ border-radius: 30px;
31
+
32
+ }
33
+
34
+ .play-img {
35
+ margin: auto;
36
+ height: 45%;
37
+ width: 45%;
38
+ display: block;
39
+ }
40
+
41
+ .download-img {
42
+ margin: auto;
43
+ height: 100%;
44
+ width: 100%;
45
+ display: block;
46
+ }
47
+
48
+ .pause-img {
49
+ margin: auto;
50
+ height: 45%;
51
+ width: 45%;
52
+ display: none
53
+ }
54
+
55
+ .playpause {
56
+ margin:11px 11px 11px 11px;
57
+ width: 32px;
58
+ min-width: 32px;
59
+ height: 32px;
60
+ /*background-color: rgb(241, 243, 244);*/
61
+ background-color: rgba(0, 0, 0, 0.0);
62
+ /*border-right: 1px solid black;*/
63
+ /*border: 1px solid red;*/
64
+ border-radius: 16px;
65
+ color: black;
66
+ transition: 0.25s;
67
+ box-sizing: border-box !important;
68
+ }
69
+
70
+ .download {
71
+ margin:11px 11px 11px 11px;
72
+ width: 32px;
73
+ min-width: 32px;
74
+ height: 32px;
75
+ /*background-color: rgb(241, 243, 244);*/
76
+ background-color: rgba(0, 0, 0, 0.0);
77
+ /*border-right: 1px solid black;*/
78
+ /*border: 1px solid red;*/
79
+ border-radius: 16px;
80
+ color: black;
81
+ transition: 0.25s;
82
+ box-sizing: border-box !important;
83
+ }
84
+
85
+ /*.playpause:disabled {
86
+ background-color: red;
87
+ }*/
88
+
89
+ .playpause:hover {
90
+ background-color: rgba(10, 20, 30, 0.03);
91
+ }
92
+
93
+ .playpause:focus {
94
+ outline:none;
95
+ }
96
+
97
+ .response {
98
+ padding:0px 20px 0px 0px;
99
+ width: calc(100% - 132px);
100
+ height: 100%;
101
+
102
+ /*border: 1px solid red;*/
103
+ /*border-bottom: 1px solid rgb(89, 89, 89);*/
104
+ }
105
+
106
+ .response-canvas {
107
+ height: 100%;
108
+ width: 100%;
109
+ }
110
+
111
+
112
+ .underlay {
113
+ height: 100%;
114
+ width: 100%;
115
+ position: absolute;
116
+ top: 0;
117
+ left: 0;
118
+ }
119
+
120
+ .overlay{
121
+ width: 0%;
122
+ height:100%;
123
+ top: 0;
124
+ right: 0px;
125
+
126
+ background:rgba(255, 255, 255, 0.15);
127
+ overflow:hidden;
128
+ position: absolute;
129
+ z-index: 10;
130
+ border-left: solid 1px rgba(0, 0, 0, 0.664);
131
+
132
+ position: absolute;
133
+ pointer-events: none;
134
+ }
135
+ </style>
136
+
137
+ <script>
138
+ !function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).pako=t()}}(function(){return function(){return function t(e,a,i){function n(s,o){if(!a[s]){if(!e[s]){var l="function"==typeof require&&require;if(!o&&l)return l(s,!0);if(r)return r(s,!0);var h=new Error("Cannot find module '"+s+"'");throw h.code="MODULE_NOT_FOUND",h}var d=a[s]={exports:{}};e[s][0].call(d.exports,function(t){return n(e[s][1][t]||t)},d,d.exports,t,e,a,i)}return a[s].exports}for(var r="function"==typeof require&&require,s=0;s<i.length;s++)n(i[s]);return n}}()({1:[function(t,e,a){"use strict";var i=t("./zlib/deflate"),n=t("./utils/common"),r=t("./utils/strings"),s=t("./zlib/messages"),o=t("./zlib/zstream"),l=Object.prototype.toString,h=0,d=-1,f=0,_=8;function u(t){if(!(this instanceof u))return new u(t);this.options=n.assign({level:d,method:_,chunkSize:16384,windowBits:15,memLevel:8,strategy:f,to:""},t||{});var e=this.options;e.raw&&e.windowBits>0?e.windowBits=-e.windowBits:e.gzip&&e.windowBits>0&&e.windowBits<16&&(e.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new o,this.strm.avail_out=0;var a=i.deflateInit2(this.strm,e.level,e.method,e.windowBits,e.memLevel,e.strategy);if(a!==h)throw new Error(s[a]);if(e.header&&i.deflateSetHeader(this.strm,e.header),e.dictionary){var c;if(c="string"==typeof e.dictionary?r.string2buf(e.dictionary):"[object ArrayBuffer]"===l.call(e.dictionary)?new Uint8Array(e.dictionary):e.dictionary,(a=i.deflateSetDictionary(this.strm,c))!==h)throw new Error(s[a]);this._dict_set=!0}}function c(t,e){var a=new u(e);if(a.push(t,!0),a.err)throw a.msg||s[a.err];return a.result}u.prototype.push=function(t,e){var a,s,o=this.strm,d=this.options.chunkSize;if(this.ended)return!1;s=e===~~e?e:!0===e?4:0,"string"==typeof t?o.input=r.string2buf(t):"[object ArrayBuffer]"===l.call(t)?o.input=new Uint8Array(t):o.input=t,o.next_in=0,o.avail_in=o.input.length;do{if(0===o.avail_out&&(o.output=new n.Buf8(d),o.next_out=0,o.avail_out=d),1!==(a=i.deflate(o,s))&&a!==h)return this.onEnd(a),this.ended=!0,!1;0!==o.avail_out&&(0!==o.avail_in||4!==s&&2!==s)||("string"===this.options.to?this.onData(r.buf2binstring(n.shrinkBuf(o.output,o.next_out))):this.onData(n.shrinkBuf(o.output,o.next_out)))}while((o.avail_in>0||0===o.avail_out)&&1!==a);return 4===s?(a=i.deflateEnd(this.strm),this.onEnd(a),this.ended=!0,a===h):2!==s||(this.onEnd(h),o.avail_out=0,!0)},u.prototype.onData=function(t){this.chunks.push(t)},u.prototype.onEnd=function(t){t===h&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=n.flattenChunks(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg},a.Deflate=u,a.deflate=c,a.deflateRaw=function(t,e){return(e=e||{}).raw=!0,c(t,e)},a.gzip=function(t,e){return(e=e||{}).gzip=!0,c(t,e)}},{"./utils/common":3,"./utils/strings":4,"./zlib/deflate":8,"./zlib/messages":13,"./zlib/zstream":15}],2:[function(t,e,a){"use strict";var i=t("./zlib/inflate"),n=t("./utils/common"),r=t("./utils/strings"),s=t("./zlib/constants"),o=t("./zlib/messages"),l=t("./zlib/zstream"),h=t("./zlib/gzheader"),d=Object.prototype.toString;function f(t){if(!(this instanceof f))return new f(t);this.options=n.assign({chunkSize:16384,windowBits:0,to:""},t||{});var e=this.options;e.raw&&e.windowBits>=0&&e.windowBits<16&&(e.windowBits=-e.windowBits,0===e.windowBits&&(e.windowBits=-15)),!(e.windowBits>=0&&e.windowBits<16)||t&&t.windowBits||(e.windowBits+=32),e.windowBits>15&&e.windowBits<48&&0==(15&e.windowBits)&&(e.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new l,this.strm.avail_out=0;var a=i.inflateInit2(this.strm,e.windowBits);if(a!==s.Z_OK)throw new Error(o[a]);if(this.header=new h,i.inflateGetHeader(this.strm,this.header),e.dictionary&&("string"==typeof e.dictionary?e.dictionary=r.string2buf(e.dictionary):"[object ArrayBuffer]"===d.call(e.dictionary)&&(e.dictionary=new Uint8Array(e.dictionary)),e.raw&&(a=i.inflateSetDictionary(this.strm,e.dictionary))!==s.Z_OK))throw new Error(o[a])}function _(t,e){var a=new f(e);if(a.push(t,!0),a.err)throw a.msg||o[a.err];return a.result}f.prototype.push=function(t,e){var a,o,l,h,f,_=this.strm,u=this.options.chunkSize,c=this.options.dictionary,b=!1;if(this.ended)return!1;o=e===~~e?e:!0===e?s.Z_FINISH:s.Z_NO_FLUSH,"string"==typeof t?_.input=r.binstring2buf(t):"[object ArrayBuffer]"===d.call(t)?_.input=new Uint8Array(t):_.input=t,_.next_in=0,_.avail_in=_.input.length;do{if(0===_.avail_out&&(_.output=new n.Buf8(u),_.next_out=0,_.avail_out=u),(a=i.inflate(_,s.Z_NO_FLUSH))===s.Z_NEED_DICT&&c&&(a=i.inflateSetDictionary(this.strm,c)),a===s.Z_BUF_ERROR&&!0===b&&(a=s.Z_OK,b=!1),a!==s.Z_STREAM_END&&a!==s.Z_OK)return this.onEnd(a),this.ended=!0,!1;_.next_out&&(0!==_.avail_out&&a!==s.Z_STREAM_END&&(0!==_.avail_in||o!==s.Z_FINISH&&o!==s.Z_SYNC_FLUSH)||("string"===this.options.to?(l=r.utf8border(_.output,_.next_out),h=_.next_out-l,f=r.buf2string(_.output,l),_.next_out=h,_.avail_out=u-h,h&&n.arraySet(_.output,_.output,l,h,0),this.onData(f)):this.onData(n.shrinkBuf(_.output,_.next_out)))),0===_.avail_in&&0===_.avail_out&&(b=!0)}while((_.avail_in>0||0===_.avail_out)&&a!==s.Z_STREAM_END);return a===s.Z_STREAM_END&&(o=s.Z_FINISH),o===s.Z_FINISH?(a=i.inflateEnd(this.strm),this.onEnd(a),this.ended=!0,a===s.Z_OK):o!==s.Z_SYNC_FLUSH||(this.onEnd(s.Z_OK),_.avail_out=0,!0)},f.prototype.onData=function(t){this.chunks.push(t)},f.prototype.onEnd=function(t){t===s.Z_OK&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=n.flattenChunks(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg},a.Inflate=f,a.inflate=_,a.inflateRaw=function(t,e){return(e=e||{}).raw=!0,_(t,e)},a.ungzip=_},{"./utils/common":3,"./utils/strings":4,"./zlib/constants":6,"./zlib/gzheader":9,"./zlib/inflate":11,"./zlib/messages":13,"./zlib/zstream":15}],3:[function(t,e,a){"use strict";var i="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Int32Array;function n(t,e){return Object.prototype.hasOwnProperty.call(t,e)}a.assign=function(t){for(var e=Array.prototype.slice.call(arguments,1);e.length;){var a=e.shift();if(a){if("object"!=typeof a)throw new TypeError(a+"must be non-object");for(var i in a)n(a,i)&&(t[i]=a[i])}}return t},a.shrinkBuf=function(t,e){return t.length===e?t:t.subarray?t.subarray(0,e):(t.length=e,t)};var r={arraySet:function(t,e,a,i,n){if(e.subarray&&t.subarray)t.set(e.subarray(a,a+i),n);else for(var r=0;r<i;r++)t[n+r]=e[a+r]},flattenChunks:function(t){var e,a,i,n,r,s;for(i=0,e=0,a=t.length;e<a;e++)i+=t[e].length;for(s=new Uint8Array(i),n=0,e=0,a=t.length;e<a;e++)r=t[e],s.set(r,n),n+=r.length;return s}},s={arraySet:function(t,e,a,i,n){for(var r=0;r<i;r++)t[n+r]=e[a+r]},flattenChunks:function(t){return[].concat.apply([],t)}};a.setTyped=function(t){t?(a.Buf8=Uint8Array,a.Buf16=Uint16Array,a.Buf32=Int32Array,a.assign(a,r)):(a.Buf8=Array,a.Buf16=Array,a.Buf32=Array,a.assign(a,s))},a.setTyped(i)},{}],4:[function(t,e,a){"use strict";var i=t("./common"),n=!0,r=!0;try{String.fromCharCode.apply(null,[0])}catch(t){n=!1}try{String.fromCharCode.apply(null,new Uint8Array(1))}catch(t){r=!1}for(var s=new i.Buf8(256),o=0;o<256;o++)s[o]=o>=252?6:o>=248?5:o>=240?4:o>=224?3:o>=192?2:1;function l(t,e){if(e<65534&&(t.subarray&&r||!t.subarray&&n))return String.fromCharCode.apply(null,i.shrinkBuf(t,e));for(var a="",s=0;s<e;s++)a+=String.fromCharCode(t[s]);return a}s[254]=s[254]=1,a.string2buf=function(t){var e,a,n,r,s,o=t.length,l=0;for(r=0;r<o;r++)55296==(64512&(a=t.charCodeAt(r)))&&r+1<o&&56320==(64512&(n=t.charCodeAt(r+1)))&&(a=65536+(a-55296<<10)+(n-56320),r++),l+=a<128?1:a<2048?2:a<65536?3:4;for(e=new i.Buf8(l),s=0,r=0;s<l;r++)55296==(64512&(a=t.charCodeAt(r)))&&r+1<o&&56320==(64512&(n=t.charCodeAt(r+1)))&&(a=65536+(a-55296<<10)+(n-56320),r++),a<128?e[s++]=a:a<2048?(e[s++]=192|a>>>6,e[s++]=128|63&a):a<65536?(e[s++]=224|a>>>12,e[s++]=128|a>>>6&63,e[s++]=128|63&a):(e[s++]=240|a>>>18,e[s++]=128|a>>>12&63,e[s++]=128|a>>>6&63,e[s++]=128|63&a);return e},a.buf2binstring=function(t){return l(t,t.length)},a.binstring2buf=function(t){for(var e=new i.Buf8(t.length),a=0,n=e.length;a<n;a++)e[a]=t.charCodeAt(a);return e},a.buf2string=function(t,e){var a,i,n,r,o=e||t.length,h=new Array(2*o);for(i=0,a=0;a<o;)if((n=t[a++])<128)h[i++]=n;else if((r=s[n])>4)h[i++]=65533,a+=r-1;else{for(n&=2===r?31:3===r?15:7;r>1&&a<o;)n=n<<6|63&t[a++],r--;r>1?h[i++]=65533:n<65536?h[i++]=n:(n-=65536,h[i++]=55296|n>>10&1023,h[i++]=56320|1023&n)}return l(h,i)},a.utf8border=function(t,e){var a;for((e=e||t.length)>t.length&&(e=t.length),a=e-1;a>=0&&128==(192&t[a]);)a--;return a<0?e:0===a?e:a+s[t[a]]>e?a:e}},{"./common":3}],5:[function(t,e,a){"use strict";e.exports=function(t,e,a,i){for(var n=65535&t|0,r=t>>>16&65535|0,s=0;0!==a;){a-=s=a>2e3?2e3:a;do{r=r+(n=n+e[i++]|0)|0}while(--s);n%=65521,r%=65521}return n|r<<16|0}},{}],6:[function(t,e,a){"use strict";e.exports={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8}},{}],7:[function(t,e,a){"use strict";var i=function(){for(var t,e=[],a=0;a<256;a++){t=a;for(var i=0;i<8;i++)t=1&t?3988292384^t>>>1:t>>>1;e[a]=t}return e}();e.exports=function(t,e,a,n){var r=i,s=n+a;t^=-1;for(var o=n;o<s;o++)t=t>>>8^r[255&(t^e[o])];return-1^t}},{}],8:[function(t,e,a){"use strict";var i,n=t("../utils/common"),r=t("./trees"),s=t("./adler32"),o=t("./crc32"),l=t("./messages"),h=0,d=1,f=3,_=4,u=5,c=0,b=1,g=-2,m=-3,w=-5,p=-1,v=1,k=2,y=3,x=4,z=0,B=2,S=8,E=9,A=15,Z=8,R=286,C=30,N=19,O=2*R+1,D=15,I=3,U=258,T=U+I+1,F=32,L=42,H=69,j=73,K=91,M=103,P=113,Y=666,q=1,G=2,X=3,W=4,J=3;function Q(t,e){return t.msg=l[e],e}function V(t){return(t<<1)-(t>4?9:0)}function $(t){for(var e=t.length;--e>=0;)t[e]=0}function tt(t){var e=t.state,a=e.pending;a>t.avail_out&&(a=t.avail_out),0!==a&&(n.arraySet(t.output,e.pending_buf,e.pending_out,a,t.next_out),t.next_out+=a,e.pending_out+=a,t.total_out+=a,t.avail_out-=a,e.pending-=a,0===e.pending&&(e.pending_out=0))}function et(t,e){r._tr_flush_block(t,t.block_start>=0?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,tt(t.strm)}function at(t,e){t.pending_buf[t.pending++]=e}function it(t,e){t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e}function nt(t,e){var a,i,n=t.max_chain_length,r=t.strstart,s=t.prev_length,o=t.nice_match,l=t.strstart>t.w_size-T?t.strstart-(t.w_size-T):0,h=t.window,d=t.w_mask,f=t.prev,_=t.strstart+U,u=h[r+s-1],c=h[r+s];t.prev_length>=t.good_match&&(n>>=2),o>t.lookahead&&(o=t.lookahead);do{if(h[(a=e)+s]===c&&h[a+s-1]===u&&h[a]===h[r]&&h[++a]===h[r+1]){r+=2,a++;do{}while(h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&r<_);if(i=U-(_-r),r=_-U,i>s){if(t.match_start=e,s=i,i>=o)break;u=h[r+s-1],c=h[r+s]}}}while((e=f[e&d])>l&&0!=--n);return s<=t.lookahead?s:t.lookahead}function rt(t){var e,a,i,r,l,h,d,f,_,u,c=t.w_size;do{if(r=t.window_size-t.lookahead-t.strstart,t.strstart>=c+(c-T)){n.arraySet(t.window,t.window,c,c,0),t.match_start-=c,t.strstart-=c,t.block_start-=c,e=a=t.hash_size;do{i=t.head[--e],t.head[e]=i>=c?i-c:0}while(--a);e=a=c;do{i=t.prev[--e],t.prev[e]=i>=c?i-c:0}while(--a);r+=c}if(0===t.strm.avail_in)break;if(h=t.strm,d=t.window,f=t.strstart+t.lookahead,_=r,u=void 0,(u=h.avail_in)>_&&(u=_),a=0===u?0:(h.avail_in-=u,n.arraySet(d,h.input,h.next_in,u,f),1===h.state.wrap?h.adler=s(h.adler,d,u,f):2===h.state.wrap&&(h.adler=o(h.adler,d,u,f)),h.next_in+=u,h.total_in+=u,u),t.lookahead+=a,t.lookahead+t.insert>=I)for(l=t.strstart-t.insert,t.ins_h=t.window[l],t.ins_h=(t.ins_h<<t.hash_shift^t.window[l+1])&t.hash_mask;t.insert&&(t.ins_h=(t.ins_h<<t.hash_shift^t.window[l+I-1])&t.hash_mask,t.prev[l&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=l,l++,t.insert--,!(t.lookahead+t.insert<I)););}while(t.lookahead<T&&0!==t.strm.avail_in)}function st(t,e){for(var a,i;;){if(t.lookahead<T){if(rt(t),t.lookahead<T&&e===h)return q;if(0===t.lookahead)break}if(a=0,t.lookahead>=I&&(t.ins_h=(t.ins_h<<t.hash_shift^t.window[t.strstart+I-1])&t.hash_mask,a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),0!==a&&t.strstart-a<=t.w_size-T&&(t.match_length=nt(t,a)),t.match_length>=I)if(i=r._tr_tally(t,t.strstart-t.match_start,t.match_length-I),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=I){t.match_length--;do{t.strstart++,t.ins_h=(t.ins_h<<t.hash_shift^t.window[t.strstart+I-1])&t.hash_mask,a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart}while(0!=--t.match_length);t.strstart++}else t.strstart+=t.match_length,t.match_length=0,t.ins_h=t.window[t.strstart],t.ins_h=(t.ins_h<<t.hash_shift^t.window[t.strstart+1])&t.hash_mask;else i=r._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++;if(i&&(et(t,!1),0===t.strm.avail_out))return q}return t.insert=t.strstart<I-1?t.strstart:I-1,e===_?(et(t,!0),0===t.strm.avail_out?X:W):t.last_lit&&(et(t,!1),0===t.strm.avail_out)?q:G}function ot(t,e){for(var a,i,n;;){if(t.lookahead<T){if(rt(t),t.lookahead<T&&e===h)return q;if(0===t.lookahead)break}if(a=0,t.lookahead>=I&&(t.ins_h=(t.ins_h<<t.hash_shift^t.window[t.strstart+I-1])&t.hash_mask,a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),t.prev_length=t.match_length,t.prev_match=t.match_start,t.match_length=I-1,0!==a&&t.prev_length<t.max_lazy_match&&t.strstart-a<=t.w_size-T&&(t.match_length=nt(t,a),t.match_length<=5&&(t.strategy===v||t.match_length===I&&t.strstart-t.match_start>4096)&&(t.match_length=I-1)),t.prev_length>=I&&t.match_length<=t.prev_length){n=t.strstart+t.lookahead-I,i=r._tr_tally(t,t.strstart-1-t.prev_match,t.prev_length-I),t.lookahead-=t.prev_length-1,t.prev_length-=2;do{++t.strstart<=n&&(t.ins_h=(t.ins_h<<t.hash_shift^t.window[t.strstart+I-1])&t.hash_mask,a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart)}while(0!=--t.prev_length);if(t.match_available=0,t.match_length=I-1,t.strstart++,i&&(et(t,!1),0===t.strm.avail_out))return q}else if(t.match_available){if((i=r._tr_tally(t,0,t.window[t.strstart-1]))&&et(t,!1),t.strstart++,t.lookahead--,0===t.strm.avail_out)return q}else t.match_available=1,t.strstart++,t.lookahead--}return t.match_available&&(i=r._tr_tally(t,0,t.window[t.strstart-1]),t.match_available=0),t.insert=t.strstart<I-1?t.strstart:I-1,e===_?(et(t,!0),0===t.strm.avail_out?X:W):t.last_lit&&(et(t,!1),0===t.strm.avail_out)?q:G}function lt(t,e,a,i,n){this.good_length=t,this.max_lazy=e,this.nice_length=a,this.max_chain=i,this.func=n}function ht(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=S,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new n.Buf16(2*O),this.dyn_dtree=new n.Buf16(2*(2*C+1)),this.bl_tree=new n.Buf16(2*(2*N+1)),$(this.dyn_ltree),$(this.dyn_dtree),$(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new n.Buf16(D+1),this.heap=new n.Buf16(2*R+1),$(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new n.Buf16(2*R+1),$(this.depth),this.l_buf=0,this.lit_bufsize=0,this.last_lit=0,this.d_buf=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}function dt(t){var e;return t&&t.state?(t.total_in=t.total_out=0,t.data_type=B,(e=t.state).pending=0,e.pending_out=0,e.wrap<0&&(e.wrap=-e.wrap),e.status=e.wrap?L:P,t.adler=2===e.wrap?0:1,e.last_flush=h,r._tr_init(e),c):Q(t,g)}function ft(t){var e,a=dt(t);return a===c&&((e=t.state).window_size=2*e.w_size,$(e.head),e.max_lazy_match=i[e.level].max_lazy,e.good_match=i[e.level].good_length,e.nice_match=i[e.level].nice_length,e.max_chain_length=i[e.level].max_chain,e.strstart=0,e.block_start=0,e.lookahead=0,e.insert=0,e.match_length=e.prev_length=I-1,e.match_available=0,e.ins_h=0),a}function _t(t,e,a,i,r,s){if(!t)return g;var o=1;if(e===p&&(e=6),i<0?(o=0,i=-i):i>15&&(o=2,i-=16),r<1||r>E||a!==S||i<8||i>15||e<0||e>9||s<0||s>x)return Q(t,g);8===i&&(i=9);var l=new ht;return t.state=l,l.strm=t,l.wrap=o,l.gzhead=null,l.w_bits=i,l.w_size=1<<l.w_bits,l.w_mask=l.w_size-1,l.hash_bits=r+7,l.hash_size=1<<l.hash_bits,l.hash_mask=l.hash_size-1,l.hash_shift=~~((l.hash_bits+I-1)/I),l.window=new n.Buf8(2*l.w_size),l.head=new n.Buf16(l.hash_size),l.prev=new n.Buf16(l.w_size),l.lit_bufsize=1<<r+6,l.pending_buf_size=4*l.lit_bufsize,l.pending_buf=new n.Buf8(l.pending_buf_size),l.d_buf=1*l.lit_bufsize,l.l_buf=3*l.lit_bufsize,l.level=e,l.strategy=s,l.method=a,ft(t)}i=[new lt(0,0,0,0,function(t,e){var a=65535;for(a>t.pending_buf_size-5&&(a=t.pending_buf_size-5);;){if(t.lookahead<=1){if(rt(t),0===t.lookahead&&e===h)return q;if(0===t.lookahead)break}t.strstart+=t.lookahead,t.lookahead=0;var i=t.block_start+a;if((0===t.strstart||t.strstart>=i)&&(t.lookahead=t.strstart-i,t.strstart=i,et(t,!1),0===t.strm.avail_out))return q;if(t.strstart-t.block_start>=t.w_size-T&&(et(t,!1),0===t.strm.avail_out))return q}return t.insert=0,e===_?(et(t,!0),0===t.strm.avail_out?X:W):(t.strstart>t.block_start&&(et(t,!1),t.strm.avail_out),q)}),new lt(4,4,8,4,st),new lt(4,5,16,8,st),new lt(4,6,32,32,st),new lt(4,4,16,16,ot),new lt(8,16,32,32,ot),new lt(8,16,128,128,ot),new lt(8,32,128,256,ot),new lt(32,128,258,1024,ot),new lt(32,258,258,4096,ot)],a.deflateInit=function(t,e){return _t(t,e,S,A,Z,z)},a.deflateInit2=_t,a.deflateReset=ft,a.deflateResetKeep=dt,a.deflateSetHeader=function(t,e){return t&&t.state?2!==t.state.wrap?g:(t.state.gzhead=e,c):g},a.deflate=function(t,e){var a,n,s,l;if(!t||!t.state||e>u||e<0)return t?Q(t,g):g;if(n=t.state,!t.output||!t.input&&0!==t.avail_in||n.status===Y&&e!==_)return Q(t,0===t.avail_out?w:g);if(n.strm=t,a=n.last_flush,n.last_flush=e,n.status===L)if(2===n.wrap)t.adler=0,at(n,31),at(n,139),at(n,8),n.gzhead?(at(n,(n.gzhead.text?1:0)+(n.gzhead.hcrc?2:0)+(n.gzhead.extra?4:0)+(n.gzhead.name?8:0)+(n.gzhead.comment?16:0)),at(n,255&n.gzhead.time),at(n,n.gzhead.time>>8&255),at(n,n.gzhead.time>>16&255),at(n,n.gzhead.time>>24&255),at(n,9===n.level?2:n.strategy>=k||n.level<2?4:0),at(n,255&n.gzhead.os),n.gzhead.extra&&n.gzhead.extra.length&&(at(n,255&n.gzhead.extra.length),at(n,n.gzhead.extra.length>>8&255)),n.gzhead.hcrc&&(t.adler=o(t.adler,n.pending_buf,n.pending,0)),n.gzindex=0,n.status=H):(at(n,0),at(n,0),at(n,0),at(n,0),at(n,0),at(n,9===n.level?2:n.strategy>=k||n.level<2?4:0),at(n,J),n.status=P);else{var m=S+(n.w_bits-8<<4)<<8;m|=(n.strategy>=k||n.level<2?0:n.level<6?1:6===n.level?2:3)<<6,0!==n.strstart&&(m|=F),m+=31-m%31,n.status=P,it(n,m),0!==n.strstart&&(it(n,t.adler>>>16),it(n,65535&t.adler)),t.adler=1}if(n.status===H)if(n.gzhead.extra){for(s=n.pending;n.gzindex<(65535&n.gzhead.extra.length)&&(n.pending!==n.pending_buf_size||(n.gzhead.hcrc&&n.pending>s&&(t.adler=o(t.adler,n.pending_buf,n.pending-s,s)),tt(t),s=n.pending,n.pending!==n.pending_buf_size));)at(n,255&n.gzhead.extra[n.gzindex]),n.gzindex++;n.gzhead.hcrc&&n.pending>s&&(t.adler=o(t.adler,n.pending_buf,n.pending-s,s)),n.gzindex===n.gzhead.extra.length&&(n.gzindex=0,n.status=j)}else n.status=j;if(n.status===j)if(n.gzhead.name){s=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>s&&(t.adler=o(t.adler,n.pending_buf,n.pending-s,s)),tt(t),s=n.pending,n.pending===n.pending_buf_size)){l=1;break}l=n.gzindex<n.gzhead.name.length?255&n.gzhead.name.charCodeAt(n.gzindex++):0,at(n,l)}while(0!==l);n.gzhead.hcrc&&n.pending>s&&(t.adler=o(t.adler,n.pending_buf,n.pending-s,s)),0===l&&(n.gzindex=0,n.status=K)}else n.status=K;if(n.status===K)if(n.gzhead.comment){s=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>s&&(t.adler=o(t.adler,n.pending_buf,n.pending-s,s)),tt(t),s=n.pending,n.pending===n.pending_buf_size)){l=1;break}l=n.gzindex<n.gzhead.comment.length?255&n.gzhead.comment.charCodeAt(n.gzindex++):0,at(n,l)}while(0!==l);n.gzhead.hcrc&&n.pending>s&&(t.adler=o(t.adler,n.pending_buf,n.pending-s,s)),0===l&&(n.status=M)}else n.status=M;if(n.status===M&&(n.gzhead.hcrc?(n.pending+2>n.pending_buf_size&&tt(t),n.pending+2<=n.pending_buf_size&&(at(n,255&t.adler),at(n,t.adler>>8&255),t.adler=0,n.status=P)):n.status=P),0!==n.pending){if(tt(t),0===t.avail_out)return n.last_flush=-1,c}else if(0===t.avail_in&&V(e)<=V(a)&&e!==_)return Q(t,w);if(n.status===Y&&0!==t.avail_in)return Q(t,w);if(0!==t.avail_in||0!==n.lookahead||e!==h&&n.status!==Y){var p=n.strategy===k?function(t,e){for(var a;;){if(0===t.lookahead&&(rt(t),0===t.lookahead)){if(e===h)return q;break}if(t.match_length=0,a=r._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,a&&(et(t,!1),0===t.strm.avail_out))return q}return t.insert=0,e===_?(et(t,!0),0===t.strm.avail_out?X:W):t.last_lit&&(et(t,!1),0===t.strm.avail_out)?q:G}(n,e):n.strategy===y?function(t,e){for(var a,i,n,s,o=t.window;;){if(t.lookahead<=U){if(rt(t),t.lookahead<=U&&e===h)return q;if(0===t.lookahead)break}if(t.match_length=0,t.lookahead>=I&&t.strstart>0&&(i=o[n=t.strstart-1])===o[++n]&&i===o[++n]&&i===o[++n]){s=t.strstart+U;do{}while(i===o[++n]&&i===o[++n]&&i===o[++n]&&i===o[++n]&&i===o[++n]&&i===o[++n]&&i===o[++n]&&i===o[++n]&&n<s);t.match_length=U-(s-n),t.match_length>t.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=I?(a=r._tr_tally(t,1,t.match_length-I),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(a=r._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),a&&(et(t,!1),0===t.strm.avail_out))return q}return t.insert=0,e===_?(et(t,!0),0===t.strm.avail_out?X:W):t.last_lit&&(et(t,!1),0===t.strm.avail_out)?q:G}(n,e):i[n.level].func(n,e);if(p!==X&&p!==W||(n.status=Y),p===q||p===X)return 0===t.avail_out&&(n.last_flush=-1),c;if(p===G&&(e===d?r._tr_align(n):e!==u&&(r._tr_stored_block(n,0,0,!1),e===f&&($(n.head),0===n.lookahead&&(n.strstart=0,n.block_start=0,n.insert=0))),tt(t),0===t.avail_out))return n.last_flush=-1,c}return e!==_?c:n.wrap<=0?b:(2===n.wrap?(at(n,255&t.adler),at(n,t.adler>>8&255),at(n,t.adler>>16&255),at(n,t.adler>>24&255),at(n,255&t.total_in),at(n,t.total_in>>8&255),at(n,t.total_in>>16&255),at(n,t.total_in>>24&255)):(it(n,t.adler>>>16),it(n,65535&t.adler)),tt(t),n.wrap>0&&(n.wrap=-n.wrap),0!==n.pending?c:b)},a.deflateEnd=function(t){var e;return t&&t.state?(e=t.state.status)!==L&&e!==H&&e!==j&&e!==K&&e!==M&&e!==P&&e!==Y?Q(t,g):(t.state=null,e===P?Q(t,m):c):g},a.deflateSetDictionary=function(t,e){var a,i,r,o,l,h,d,f,_=e.length;if(!t||!t.state)return g;if(2===(o=(a=t.state).wrap)||1===o&&a.status!==L||a.lookahead)return g;for(1===o&&(t.adler=s(t.adler,e,_,0)),a.wrap=0,_>=a.w_size&&(0===o&&($(a.head),a.strstart=0,a.block_start=0,a.insert=0),f=new n.Buf8(a.w_size),n.arraySet(f,e,_-a.w_size,a.w_size,0),e=f,_=a.w_size),l=t.avail_in,h=t.next_in,d=t.input,t.avail_in=_,t.next_in=0,t.input=e,rt(a);a.lookahead>=I;){i=a.strstart,r=a.lookahead-(I-1);do{a.ins_h=(a.ins_h<<a.hash_shift^a.window[i+I-1])&a.hash_mask,a.prev[i&a.w_mask]=a.head[a.ins_h],a.head[a.ins_h]=i,i++}while(--r);a.strstart=i,a.lookahead=I-1,rt(a)}return a.strstart+=a.lookahead,a.block_start=a.strstart,a.insert=a.lookahead,a.lookahead=0,a.match_length=a.prev_length=I-1,a.match_available=0,t.next_in=h,t.input=d,t.avail_in=l,a.wrap=o,c},a.deflateInfo="pako deflate (from Nodeca project)"},{"../utils/common":3,"./adler32":5,"./crc32":7,"./messages":13,"./trees":14}],9:[function(t,e,a){"use strict";e.exports=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1}},{}],10:[function(t,e,a){"use strict";e.exports=function(t,e){var a,i,n,r,s,o,l,h,d,f,_,u,c,b,g,m,w,p,v,k,y,x,z,B,S;a=t.state,i=t.next_in,B=t.input,n=i+(t.avail_in-5),r=t.next_out,S=t.output,s=r-(e-t.avail_out),o=r+(t.avail_out-257),l=a.dmax,h=a.wsize,d=a.whave,f=a.wnext,_=a.window,u=a.hold,c=a.bits,b=a.lencode,g=a.distcode,m=(1<<a.lenbits)-1,w=(1<<a.distbits)-1;t:do{c<15&&(u+=B[i++]<<c,c+=8,u+=B[i++]<<c,c+=8),p=b[u&m];e:for(;;){if(u>>>=v=p>>>24,c-=v,0===(v=p>>>16&255))S[r++]=65535&p;else{if(!(16&v)){if(0==(64&v)){p=b[(65535&p)+(u&(1<<v)-1)];continue e}if(32&v){a.mode=12;break t}t.msg="invalid literal/length code",a.mode=30;break t}k=65535&p,(v&=15)&&(c<v&&(u+=B[i++]<<c,c+=8),k+=u&(1<<v)-1,u>>>=v,c-=v),c<15&&(u+=B[i++]<<c,c+=8,u+=B[i++]<<c,c+=8),p=g[u&w];a:for(;;){if(u>>>=v=p>>>24,c-=v,!(16&(v=p>>>16&255))){if(0==(64&v)){p=g[(65535&p)+(u&(1<<v)-1)];continue a}t.msg="invalid distance code",a.mode=30;break t}if(y=65535&p,c<(v&=15)&&(u+=B[i++]<<c,(c+=8)<v&&(u+=B[i++]<<c,c+=8)),(y+=u&(1<<v)-1)>l){t.msg="invalid distance too far back",a.mode=30;break t}if(u>>>=v,c-=v,y>(v=r-s)){if((v=y-v)>d&&a.sane){t.msg="invalid distance too far back",a.mode=30;break t}if(x=0,z=_,0===f){if(x+=h-v,v<k){k-=v;do{S[r++]=_[x++]}while(--v);x=r-y,z=S}}else if(f<v){if(x+=h+f-v,(v-=f)<k){k-=v;do{S[r++]=_[x++]}while(--v);if(x=0,f<k){k-=v=f;do{S[r++]=_[x++]}while(--v);x=r-y,z=S}}}else if(x+=f-v,v<k){k-=v;do{S[r++]=_[x++]}while(--v);x=r-y,z=S}for(;k>2;)S[r++]=z[x++],S[r++]=z[x++],S[r++]=z[x++],k-=3;k&&(S[r++]=z[x++],k>1&&(S[r++]=z[x++]))}else{x=r-y;do{S[r++]=S[x++],S[r++]=S[x++],S[r++]=S[x++],k-=3}while(k>2);k&&(S[r++]=S[x++],k>1&&(S[r++]=S[x++]))}break}}break}}while(i<n&&r<o);i-=k=c>>3,u&=(1<<(c-=k<<3))-1,t.next_in=i,t.next_out=r,t.avail_in=i<n?n-i+5:5-(i-n),t.avail_out=r<o?o-r+257:257-(r-o),a.hold=u,a.bits=c}},{}],11:[function(t,e,a){"use strict";var i=t("../utils/common"),n=t("./adler32"),r=t("./crc32"),s=t("./inffast"),o=t("./inftrees"),l=0,h=1,d=2,f=4,_=5,u=6,c=0,b=1,g=2,m=-2,w=-3,p=-4,v=-5,k=8,y=1,x=2,z=3,B=4,S=5,E=6,A=7,Z=8,R=9,C=10,N=11,O=12,D=13,I=14,U=15,T=16,F=17,L=18,H=19,j=20,K=21,M=22,P=23,Y=24,q=25,G=26,X=27,W=28,J=29,Q=30,V=31,$=32,tt=852,et=592,at=15;function it(t){return(t>>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24)}function nt(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new i.Buf16(320),this.work=new i.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function rt(t){var e;return t&&t.state?(e=t.state,t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=y,e.last=0,e.havedict=0,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new i.Buf32(tt),e.distcode=e.distdyn=new i.Buf32(et),e.sane=1,e.back=-1,c):m}function st(t){var e;return t&&t.state?((e=t.state).wsize=0,e.whave=0,e.wnext=0,rt(t)):m}function ot(t,e){var a,i;return t&&t.state?(i=t.state,e<0?(a=0,e=-e):(a=1+(e>>4),e<48&&(e&=15)),e&&(e<8||e>15)?m:(null!==i.window&&i.wbits!==e&&(i.window=null),i.wrap=a,i.wbits=e,st(t))):m}function lt(t,e){var a,i;return t?(i=new nt,t.state=i,i.window=null,(a=ot(t,e))!==c&&(t.state=null),a):m}var ht,dt,ft=!0;function _t(t){if(ft){var e;for(ht=new i.Buf32(512),dt=new i.Buf32(32),e=0;e<144;)t.lens[e++]=8;for(;e<256;)t.lens[e++]=9;for(;e<280;)t.lens[e++]=7;for(;e<288;)t.lens[e++]=8;for(o(h,t.lens,0,288,ht,0,t.work,{bits:9}),e=0;e<32;)t.lens[e++]=5;o(d,t.lens,0,32,dt,0,t.work,{bits:5}),ft=!1}t.lencode=ht,t.lenbits=9,t.distcode=dt,t.distbits=5}function ut(t,e,a,n){var r,s=t.state;return null===s.window&&(s.wsize=1<<s.wbits,s.wnext=0,s.whave=0,s.window=new i.Buf8(s.wsize)),n>=s.wsize?(i.arraySet(s.window,e,a-s.wsize,s.wsize,0),s.wnext=0,s.whave=s.wsize):((r=s.wsize-s.wnext)>n&&(r=n),i.arraySet(s.window,e,a-n,r,s.wnext),(n-=r)?(i.arraySet(s.window,e,a-n,n,0),s.wnext=n,s.whave=s.wsize):(s.wnext+=r,s.wnext===s.wsize&&(s.wnext=0),s.whave<s.wsize&&(s.whave+=r))),0}a.inflateReset=st,a.inflateReset2=ot,a.inflateResetKeep=rt,a.inflateInit=function(t){return lt(t,at)},a.inflateInit2=lt,a.inflate=function(t,e){var a,tt,et,at,nt,rt,st,ot,lt,ht,dt,ft,ct,bt,gt,mt,wt,pt,vt,kt,yt,xt,zt,Bt,St=0,Et=new i.Buf8(4),At=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15];if(!t||!t.state||!t.output||!t.input&&0!==t.avail_in)return m;(a=t.state).mode===O&&(a.mode=D),nt=t.next_out,et=t.output,st=t.avail_out,at=t.next_in,tt=t.input,rt=t.avail_in,ot=a.hold,lt=a.bits,ht=rt,dt=st,xt=c;t:for(;;)switch(a.mode){case y:if(0===a.wrap){a.mode=D;break}for(;lt<16;){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}if(2&a.wrap&&35615===ot){a.check=0,Et[0]=255&ot,Et[1]=ot>>>8&255,a.check=r(a.check,Et,2,0),ot=0,lt=0,a.mode=x;break}if(a.flags=0,a.head&&(a.head.done=!1),!(1&a.wrap)||(((255&ot)<<8)+(ot>>8))%31){t.msg="incorrect header check",a.mode=Q;break}if((15&ot)!==k){t.msg="unknown compression method",a.mode=Q;break}if(lt-=4,yt=8+(15&(ot>>>=4)),0===a.wbits)a.wbits=yt;else if(yt>a.wbits){t.msg="invalid window size",a.mode=Q;break}a.dmax=1<<yt,t.adler=a.check=1,a.mode=512&ot?C:O,ot=0,lt=0;break;case x:for(;lt<16;){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}if(a.flags=ot,(255&a.flags)!==k){t.msg="unknown compression method",a.mode=Q;break}if(57344&a.flags){t.msg="unknown header flags set",a.mode=Q;break}a.head&&(a.head.text=ot>>8&1),512&a.flags&&(Et[0]=255&ot,Et[1]=ot>>>8&255,a.check=r(a.check,Et,2,0)),ot=0,lt=0,a.mode=z;case z:for(;lt<32;){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}a.head&&(a.head.time=ot),512&a.flags&&(Et[0]=255&ot,Et[1]=ot>>>8&255,Et[2]=ot>>>16&255,Et[3]=ot>>>24&255,a.check=r(a.check,Et,4,0)),ot=0,lt=0,a.mode=B;case B:for(;lt<16;){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}a.head&&(a.head.xflags=255&ot,a.head.os=ot>>8),512&a.flags&&(Et[0]=255&ot,Et[1]=ot>>>8&255,a.check=r(a.check,Et,2,0)),ot=0,lt=0,a.mode=S;case S:if(1024&a.flags){for(;lt<16;){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}a.length=ot,a.head&&(a.head.extra_len=ot),512&a.flags&&(Et[0]=255&ot,Et[1]=ot>>>8&255,a.check=r(a.check,Et,2,0)),ot=0,lt=0}else a.head&&(a.head.extra=null);a.mode=E;case E:if(1024&a.flags&&((ft=a.length)>rt&&(ft=rt),ft&&(a.head&&(yt=a.head.extra_len-a.length,a.head.extra||(a.head.extra=new Array(a.head.extra_len)),i.arraySet(a.head.extra,tt,at,ft,yt)),512&a.flags&&(a.check=r(a.check,tt,ft,at)),rt-=ft,at+=ft,a.length-=ft),a.length))break t;a.length=0,a.mode=A;case A:if(2048&a.flags){if(0===rt)break t;ft=0;do{yt=tt[at+ft++],a.head&&yt&&a.length<65536&&(a.head.name+=String.fromCharCode(yt))}while(yt&&ft<rt);if(512&a.flags&&(a.check=r(a.check,tt,ft,at)),rt-=ft,at+=ft,yt)break t}else a.head&&(a.head.name=null);a.length=0,a.mode=Z;case Z:if(4096&a.flags){if(0===rt)break t;ft=0;do{yt=tt[at+ft++],a.head&&yt&&a.length<65536&&(a.head.comment+=String.fromCharCode(yt))}while(yt&&ft<rt);if(512&a.flags&&(a.check=r(a.check,tt,ft,at)),rt-=ft,at+=ft,yt)break t}else a.head&&(a.head.comment=null);a.mode=R;case R:if(512&a.flags){for(;lt<16;){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}if(ot!==(65535&a.check)){t.msg="header crc mismatch",a.mode=Q;break}ot=0,lt=0}a.head&&(a.head.hcrc=a.flags>>9&1,a.head.done=!0),t.adler=a.check=0,a.mode=O;break;case C:for(;lt<32;){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}t.adler=a.check=it(ot),ot=0,lt=0,a.mode=N;case N:if(0===a.havedict)return t.next_out=nt,t.avail_out=st,t.next_in=at,t.avail_in=rt,a.hold=ot,a.bits=lt,g;t.adler=a.check=1,a.mode=O;case O:if(e===_||e===u)break t;case D:if(a.last){ot>>>=7&lt,lt-=7&lt,a.mode=X;break}for(;lt<3;){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}switch(a.last=1&ot,lt-=1,3&(ot>>>=1)){case 0:a.mode=I;break;case 1:if(_t(a),a.mode=j,e===u){ot>>>=2,lt-=2;break t}break;case 2:a.mode=F;break;case 3:t.msg="invalid block type",a.mode=Q}ot>>>=2,lt-=2;break;case I:for(ot>>>=7&lt,lt-=7&lt;lt<32;){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}if((65535&ot)!=(ot>>>16^65535)){t.msg="invalid stored block lengths",a.mode=Q;break}if(a.length=65535&ot,ot=0,lt=0,a.mode=U,e===u)break t;case U:a.mode=T;case T:if(ft=a.length){if(ft>rt&&(ft=rt),ft>st&&(ft=st),0===ft)break t;i.arraySet(et,tt,at,ft,nt),rt-=ft,at+=ft,st-=ft,nt+=ft,a.length-=ft;break}a.mode=O;break;case F:for(;lt<14;){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}if(a.nlen=257+(31&ot),ot>>>=5,lt-=5,a.ndist=1+(31&ot),ot>>>=5,lt-=5,a.ncode=4+(15&ot),ot>>>=4,lt-=4,a.nlen>286||a.ndist>30){t.msg="too many length or distance symbols",a.mode=Q;break}a.have=0,a.mode=L;case L:for(;a.have<a.ncode;){for(;lt<3;){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}a.lens[At[a.have++]]=7&ot,ot>>>=3,lt-=3}for(;a.have<19;)a.lens[At[a.have++]]=0;if(a.lencode=a.lendyn,a.lenbits=7,zt={bits:a.lenbits},xt=o(l,a.lens,0,19,a.lencode,0,a.work,zt),a.lenbits=zt.bits,xt){t.msg="invalid code lengths set",a.mode=Q;break}a.have=0,a.mode=H;case H:for(;a.have<a.nlen+a.ndist;){for(;mt=(St=a.lencode[ot&(1<<a.lenbits)-1])>>>16&255,wt=65535&St,!((gt=St>>>24)<=lt);){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}if(wt<16)ot>>>=gt,lt-=gt,a.lens[a.have++]=wt;else{if(16===wt){for(Bt=gt+2;lt<Bt;){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}if(ot>>>=gt,lt-=gt,0===a.have){t.msg="invalid bit length repeat",a.mode=Q;break}yt=a.lens[a.have-1],ft=3+(3&ot),ot>>>=2,lt-=2}else if(17===wt){for(Bt=gt+3;lt<Bt;){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}lt-=gt,yt=0,ft=3+(7&(ot>>>=gt)),ot>>>=3,lt-=3}else{for(Bt=gt+7;lt<Bt;){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}lt-=gt,yt=0,ft=11+(127&(ot>>>=gt)),ot>>>=7,lt-=7}if(a.have+ft>a.nlen+a.ndist){t.msg="invalid bit length repeat",a.mode=Q;break}for(;ft--;)a.lens[a.have++]=yt}}if(a.mode===Q)break;if(0===a.lens[256]){t.msg="invalid code -- missing end-of-block",a.mode=Q;break}if(a.lenbits=9,zt={bits:a.lenbits},xt=o(h,a.lens,0,a.nlen,a.lencode,0,a.work,zt),a.lenbits=zt.bits,xt){t.msg="invalid literal/lengths set",a.mode=Q;break}if(a.distbits=6,a.distcode=a.distdyn,zt={bits:a.distbits},xt=o(d,a.lens,a.nlen,a.ndist,a.distcode,0,a.work,zt),a.distbits=zt.bits,xt){t.msg="invalid distances set",a.mode=Q;break}if(a.mode=j,e===u)break t;case j:a.mode=K;case K:if(rt>=6&&st>=258){t.next_out=nt,t.avail_out=st,t.next_in=at,t.avail_in=rt,a.hold=ot,a.bits=lt,s(t,dt),nt=t.next_out,et=t.output,st=t.avail_out,at=t.next_in,tt=t.input,rt=t.avail_in,ot=a.hold,lt=a.bits,a.mode===O&&(a.back=-1);break}for(a.back=0;mt=(St=a.lencode[ot&(1<<a.lenbits)-1])>>>16&255,wt=65535&St,!((gt=St>>>24)<=lt);){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}if(mt&&0==(240&mt)){for(pt=gt,vt=mt,kt=wt;mt=(St=a.lencode[kt+((ot&(1<<pt+vt)-1)>>pt)])>>>16&255,wt=65535&St,!(pt+(gt=St>>>24)<=lt);){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}ot>>>=pt,lt-=pt,a.back+=pt}if(ot>>>=gt,lt-=gt,a.back+=gt,a.length=wt,0===mt){a.mode=G;break}if(32&mt){a.back=-1,a.mode=O;break}if(64&mt){t.msg="invalid literal/length code",a.mode=Q;break}a.extra=15&mt,a.mode=M;case M:if(a.extra){for(Bt=a.extra;lt<Bt;){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}a.length+=ot&(1<<a.extra)-1,ot>>>=a.extra,lt-=a.extra,a.back+=a.extra}a.was=a.length,a.mode=P;case P:for(;mt=(St=a.distcode[ot&(1<<a.distbits)-1])>>>16&255,wt=65535&St,!((gt=St>>>24)<=lt);){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}if(0==(240&mt)){for(pt=gt,vt=mt,kt=wt;mt=(St=a.distcode[kt+((ot&(1<<pt+vt)-1)>>pt)])>>>16&255,wt=65535&St,!(pt+(gt=St>>>24)<=lt);){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}ot>>>=pt,lt-=pt,a.back+=pt}if(ot>>>=gt,lt-=gt,a.back+=gt,64&mt){t.msg="invalid distance code",a.mode=Q;break}a.offset=wt,a.extra=15&mt,a.mode=Y;case Y:if(a.extra){for(Bt=a.extra;lt<Bt;){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}a.offset+=ot&(1<<a.extra)-1,ot>>>=a.extra,lt-=a.extra,a.back+=a.extra}if(a.offset>a.dmax){t.msg="invalid distance too far back",a.mode=Q;break}a.mode=q;case q:if(0===st)break t;if(ft=dt-st,a.offset>ft){if((ft=a.offset-ft)>a.whave&&a.sane){t.msg="invalid distance too far back",a.mode=Q;break}ft>a.wnext?(ft-=a.wnext,ct=a.wsize-ft):ct=a.wnext-ft,ft>a.length&&(ft=a.length),bt=a.window}else bt=et,ct=nt-a.offset,ft=a.length;ft>st&&(ft=st),st-=ft,a.length-=ft;do{et[nt++]=bt[ct++]}while(--ft);0===a.length&&(a.mode=K);break;case G:if(0===st)break t;et[nt++]=a.length,st--,a.mode=K;break;case X:if(a.wrap){for(;lt<32;){if(0===rt)break t;rt--,ot|=tt[at++]<<lt,lt+=8}if(dt-=st,t.total_out+=dt,a.total+=dt,dt&&(t.adler=a.check=a.flags?r(a.check,et,dt,nt-dt):n(a.check,et,dt,nt-dt)),dt=st,(a.flags?ot:it(ot))!==a.check){t.msg="incorrect data check",a.mode=Q;break}ot=0,lt=0}a.mode=W;case W:if(a.wrap&&a.flags){for(;lt<32;){if(0===rt)break t;rt--,ot+=tt[at++]<<lt,lt+=8}if(ot!==(4294967295&a.total)){t.msg="incorrect length check",a.mode=Q;break}ot=0,lt=0}a.mode=J;case J:xt=b;break t;case Q:xt=w;break t;case V:return p;case $:default:return m}return t.next_out=nt,t.avail_out=st,t.next_in=at,t.avail_in=rt,a.hold=ot,a.bits=lt,(a.wsize||dt!==t.avail_out&&a.mode<Q&&(a.mode<X||e!==f))&&ut(t,t.output,t.next_out,dt-t.avail_out)?(a.mode=V,p):(ht-=t.avail_in,dt-=t.avail_out,t.total_in+=ht,t.total_out+=dt,a.total+=dt,a.wrap&&dt&&(t.adler=a.check=a.flags?r(a.check,et,dt,t.next_out-dt):n(a.check,et,dt,t.next_out-dt)),t.data_type=a.bits+(a.last?64:0)+(a.mode===O?128:0)+(a.mode===j||a.mode===U?256:0),(0===ht&&0===dt||e===f)&&xt===c&&(xt=v),xt)},a.inflateEnd=function(t){if(!t||!t.state)return m;var e=t.state;return e.window&&(e.window=null),t.state=null,c},a.inflateGetHeader=function(t,e){var a;return t&&t.state?0==(2&(a=t.state).wrap)?m:(a.head=e,e.done=!1,c):m},a.inflateSetDictionary=function(t,e){var a,i=e.length;return t&&t.state?0!==(a=t.state).wrap&&a.mode!==N?m:a.mode===N&&n(1,e,i,0)!==a.check?w:ut(t,e,i,i)?(a.mode=V,p):(a.havedict=1,c):m},a.inflateInfo="pako inflate (from Nodeca project)"},{"../utils/common":3,"./adler32":5,"./crc32":7,"./inffast":10,"./inftrees":12}],12:[function(t,e,a){"use strict";var i=t("../utils/common"),n=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0],r=[16,16,16,16,16,16,16,16,17,17,17,17,18,18,18,18,19,19,19,19,20,20,20,20,21,21,21,21,16,72,78],s=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0],o=[16,16,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,64,64];e.exports=function(t,e,a,l,h,d,f,_){var u,c,b,g,m,w,p,v,k,y=_.bits,x=0,z=0,B=0,S=0,E=0,A=0,Z=0,R=0,C=0,N=0,O=null,D=0,I=new i.Buf16(16),U=new i.Buf16(16),T=null,F=0;for(x=0;x<=15;x++)I[x]=0;for(z=0;z<l;z++)I[e[a+z]]++;for(E=y,S=15;S>=1&&0===I[S];S--);if(E>S&&(E=S),0===S)return h[d++]=20971520,h[d++]=20971520,_.bits=1,0;for(B=1;B<S&&0===I[B];B++);for(E<B&&(E=B),R=1,x=1;x<=15;x++)if(R<<=1,(R-=I[x])<0)return-1;if(R>0&&(0===t||1!==S))return-1;for(U[1]=0,x=1;x<15;x++)U[x+1]=U[x]+I[x];for(z=0;z<l;z++)0!==e[a+z]&&(f[U[e[a+z]]++]=z);if(0===t?(O=T=f,w=19):1===t?(O=n,D-=257,T=r,F-=257,w=256):(O=s,T=o,w=-1),N=0,z=0,x=B,m=d,A=E,Z=0,b=-1,g=(C=1<<E)-1,1===t&&C>852||2===t&&C>592)return 1;for(;;){p=x-Z,f[z]<w?(v=0,k=f[z]):f[z]>w?(v=T[F+f[z]],k=O[D+f[z]]):(v=96,k=0),u=1<<x-Z,B=c=1<<A;do{h[m+(N>>Z)+(c-=u)]=p<<24|v<<16|k|0}while(0!==c);for(u=1<<x-1;N&u;)u>>=1;if(0!==u?(N&=u-1,N+=u):N=0,z++,0==--I[x]){if(x===S)break;x=e[a+f[z]]}if(x>E&&(N&g)!==b){for(0===Z&&(Z=E),m+=B,R=1<<(A=x-Z);A+Z<S&&!((R-=I[A+Z])<=0);)A++,R<<=1;if(C+=1<<A,1===t&&C>852||2===t&&C>592)return 1;h[b=N&g]=E<<24|A<<16|m-d|0}}return 0!==N&&(h[m+N]=x-Z<<24|64<<16|0),_.bits=E,0}},{"../utils/common":3}],13:[function(t,e,a){"use strict";e.exports={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"}},{}],14:[function(t,e,a){"use strict";var i=t("../utils/common"),n=4,r=0,s=1,o=2;function l(t){for(var e=t.length;--e>=0;)t[e]=0}var h=0,d=1,f=2,_=29,u=256,c=u+1+_,b=30,g=19,m=2*c+1,w=15,p=16,v=7,k=256,y=16,x=17,z=18,B=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],S=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],E=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],A=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],Z=new Array(2*(c+2));l(Z);var R=new Array(2*b);l(R);var C=new Array(512);l(C);var N=new Array(256);l(N);var O=new Array(_);l(O);var D,I,U,T=new Array(b);function F(t,e,a,i,n){this.static_tree=t,this.extra_bits=e,this.extra_base=a,this.elems=i,this.max_length=n,this.has_stree=t&&t.length}function L(t,e){this.dyn_tree=t,this.max_code=0,this.stat_desc=e}function H(t){return t<256?C[t]:C[256+(t>>>7)]}function j(t,e){t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255}function K(t,e,a){t.bi_valid>p-a?(t.bi_buf|=e<<t.bi_valid&65535,j(t,t.bi_buf),t.bi_buf=e>>p-t.bi_valid,t.bi_valid+=a-p):(t.bi_buf|=e<<t.bi_valid&65535,t.bi_valid+=a)}function M(t,e,a){K(t,a[2*e],a[2*e+1])}function P(t,e){var a=0;do{a|=1&t,t>>>=1,a<<=1}while(--e>0);return a>>>1}function Y(t,e,a){var i,n,r=new Array(w+1),s=0;for(i=1;i<=w;i++)r[i]=s=s+a[i-1]<<1;for(n=0;n<=e;n++){var o=t[2*n+1];0!==o&&(t[2*n]=P(r[o]++,o))}}function q(t){var e;for(e=0;e<c;e++)t.dyn_ltree[2*e]=0;for(e=0;e<b;e++)t.dyn_dtree[2*e]=0;for(e=0;e<g;e++)t.bl_tree[2*e]=0;t.dyn_ltree[2*k]=1,t.opt_len=t.static_len=0,t.last_lit=t.matches=0}function G(t){t.bi_valid>8?j(t,t.bi_buf):t.bi_valid>0&&(t.pending_buf[t.pending++]=t.bi_buf),t.bi_buf=0,t.bi_valid=0}function X(t,e,a,i){var n=2*e,r=2*a;return t[n]<t[r]||t[n]===t[r]&&i[e]<=i[a]}function W(t,e,a){for(var i=t.heap[a],n=a<<1;n<=t.heap_len&&(n<t.heap_len&&X(e,t.heap[n+1],t.heap[n],t.depth)&&n++,!X(e,i,t.heap[n],t.depth));)t.heap[a]=t.heap[n],a=n,n<<=1;t.heap[a]=i}function J(t,e,a){var i,n,r,s,o=0;if(0!==t.last_lit)do{i=t.pending_buf[t.d_buf+2*o]<<8|t.pending_buf[t.d_buf+2*o+1],n=t.pending_buf[t.l_buf+o],o++,0===i?M(t,n,e):(M(t,(r=N[n])+u+1,e),0!==(s=B[r])&&K(t,n-=O[r],s),M(t,r=H(--i),a),0!==(s=S[r])&&K(t,i-=T[r],s))}while(o<t.last_lit);M(t,k,e)}function Q(t,e){var a,i,n,r=e.dyn_tree,s=e.stat_desc.static_tree,o=e.stat_desc.has_stree,l=e.stat_desc.elems,h=-1;for(t.heap_len=0,t.heap_max=m,a=0;a<l;a++)0!==r[2*a]?(t.heap[++t.heap_len]=h=a,t.depth[a]=0):r[2*a+1]=0;for(;t.heap_len<2;)r[2*(n=t.heap[++t.heap_len]=h<2?++h:0)]=1,t.depth[n]=0,t.opt_len--,o&&(t.static_len-=s[2*n+1]);for(e.max_code=h,a=t.heap_len>>1;a>=1;a--)W(t,r,a);n=l;do{a=t.heap[1],t.heap[1]=t.heap[t.heap_len--],W(t,r,1),i=t.heap[1],t.heap[--t.heap_max]=a,t.heap[--t.heap_max]=i,r[2*n]=r[2*a]+r[2*i],t.depth[n]=(t.depth[a]>=t.depth[i]?t.depth[a]:t.depth[i])+1,r[2*a+1]=r[2*i+1]=n,t.heap[1]=n++,W(t,r,1)}while(t.heap_len>=2);t.heap[--t.heap_max]=t.heap[1],function(t,e){var a,i,n,r,s,o,l=e.dyn_tree,h=e.max_code,d=e.stat_desc.static_tree,f=e.stat_desc.has_stree,_=e.stat_desc.extra_bits,u=e.stat_desc.extra_base,c=e.stat_desc.max_length,b=0;for(r=0;r<=w;r++)t.bl_count[r]=0;for(l[2*t.heap[t.heap_max]+1]=0,a=t.heap_max+1;a<m;a++)(r=l[2*l[2*(i=t.heap[a])+1]+1]+1)>c&&(r=c,b++),l[2*i+1]=r,i>h||(t.bl_count[r]++,s=0,i>=u&&(s=_[i-u]),o=l[2*i],t.opt_len+=o*(r+s),f&&(t.static_len+=o*(d[2*i+1]+s)));if(0!==b){do{for(r=c-1;0===t.bl_count[r];)r--;t.bl_count[r]--,t.bl_count[r+1]+=2,t.bl_count[c]--,b-=2}while(b>0);for(r=c;0!==r;r--)for(i=t.bl_count[r];0!==i;)(n=t.heap[--a])>h||(l[2*n+1]!==r&&(t.opt_len+=(r-l[2*n+1])*l[2*n],l[2*n+1]=r),i--)}}(t,e),Y(r,h,t.bl_count)}function V(t,e,a){var i,n,r=-1,s=e[1],o=0,l=7,h=4;for(0===s&&(l=138,h=3),e[2*(a+1)+1]=65535,i=0;i<=a;i++)n=s,s=e[2*(i+1)+1],++o<l&&n===s||(o<h?t.bl_tree[2*n]+=o:0!==n?(n!==r&&t.bl_tree[2*n]++,t.bl_tree[2*y]++):o<=10?t.bl_tree[2*x]++:t.bl_tree[2*z]++,o=0,r=n,0===s?(l=138,h=3):n===s?(l=6,h=3):(l=7,h=4))}function $(t,e,a){var i,n,r=-1,s=e[1],o=0,l=7,h=4;for(0===s&&(l=138,h=3),i=0;i<=a;i++)if(n=s,s=e[2*(i+1)+1],!(++o<l&&n===s)){if(o<h)do{M(t,n,t.bl_tree)}while(0!=--o);else 0!==n?(n!==r&&(M(t,n,t.bl_tree),o--),M(t,y,t.bl_tree),K(t,o-3,2)):o<=10?(M(t,x,t.bl_tree),K(t,o-3,3)):(M(t,z,t.bl_tree),K(t,o-11,7));o=0,r=n,0===s?(l=138,h=3):n===s?(l=6,h=3):(l=7,h=4)}}l(T);var tt=!1;function et(t,e,a,n){K(t,(h<<1)+(n?1:0),3),function(t,e,a,n){G(t),n&&(j(t,a),j(t,~a)),i.arraySet(t.pending_buf,t.window,e,a,t.pending),t.pending+=a}(t,e,a,!0)}a._tr_init=function(t){tt||(function(){var t,e,a,i,n,r=new Array(w+1);for(a=0,i=0;i<_-1;i++)for(O[i]=a,t=0;t<1<<B[i];t++)N[a++]=i;for(N[a-1]=i,n=0,i=0;i<16;i++)for(T[i]=n,t=0;t<1<<S[i];t++)C[n++]=i;for(n>>=7;i<b;i++)for(T[i]=n<<7,t=0;t<1<<S[i]-7;t++)C[256+n++]=i;for(e=0;e<=w;e++)r[e]=0;for(t=0;t<=143;)Z[2*t+1]=8,t++,r[8]++;for(;t<=255;)Z[2*t+1]=9,t++,r[9]++;for(;t<=279;)Z[2*t+1]=7,t++,r[7]++;for(;t<=287;)Z[2*t+1]=8,t++,r[8]++;for(Y(Z,c+1,r),t=0;t<b;t++)R[2*t+1]=5,R[2*t]=P(t,5);D=new F(Z,B,u+1,c,w),I=new F(R,S,0,b,w),U=new F(new Array(0),E,0,g,v)}(),tt=!0),t.l_desc=new L(t.dyn_ltree,D),t.d_desc=new L(t.dyn_dtree,I),t.bl_desc=new L(t.bl_tree,U),t.bi_buf=0,t.bi_valid=0,q(t)},a._tr_stored_block=et,a._tr_flush_block=function(t,e,a,i){var l,h,_=0;t.level>0?(t.strm.data_type===o&&(t.strm.data_type=function(t){var e,a=4093624447;for(e=0;e<=31;e++,a>>>=1)if(1&a&&0!==t.dyn_ltree[2*e])return r;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return s;for(e=32;e<u;e++)if(0!==t.dyn_ltree[2*e])return s;return r}(t)),Q(t,t.l_desc),Q(t,t.d_desc),_=function(t){var e;for(V(t,t.dyn_ltree,t.l_desc.max_code),V(t,t.dyn_dtree,t.d_desc.max_code),Q(t,t.bl_desc),e=g-1;e>=3&&0===t.bl_tree[2*A[e]+1];e--);return t.opt_len+=3*(e+1)+5+5+4,e}(t),l=t.opt_len+3+7>>>3,(h=t.static_len+3+7>>>3)<=l&&(l=h)):l=h=a+5,a+4<=l&&-1!==e?et(t,e,a,i):t.strategy===n||h===l?(K(t,(d<<1)+(i?1:0),3),J(t,Z,R)):(K(t,(f<<1)+(i?1:0),3),function(t,e,a,i){var n;for(K(t,e-257,5),K(t,a-1,5),K(t,i-4,4),n=0;n<i;n++)K(t,t.bl_tree[2*A[n]+1],3);$(t,t.dyn_ltree,e-1),$(t,t.dyn_dtree,a-1)}(t,t.l_desc.max_code+1,t.d_desc.max_code+1,_+1),J(t,t.dyn_ltree,t.dyn_dtree)),q(t),i&&G(t)},a._tr_tally=function(t,e,a){return t.pending_buf[t.d_buf+2*t.last_lit]=e>>>8&255,t.pending_buf[t.d_buf+2*t.last_lit+1]=255&e,t.pending_buf[t.l_buf+t.last_lit]=255&a,t.last_lit++,0===e?t.dyn_ltree[2*a]++:(t.matches++,e--,t.dyn_ltree[2*(N[a]+u+1)]++,t.dyn_dtree[2*H(e)]++),t.last_lit===t.lit_bufsize-1},a._tr_align=function(t){K(t,d<<1,3),M(t,k,Z),function(t){16===t.bi_valid?(j(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):t.bi_valid>=8&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8)}(t)}},{"../utils/common":3}],15:[function(t,e,a){"use strict";e.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],"/":[function(t,e,a){"use strict";var i={};(0,t("./lib/utils/common").assign)(i,t("./lib/deflate"),t("./lib/inflate"),t("./lib/zlib/constants")),e.exports=i},{"./lib/deflate":1,"./lib/inflate":2,"./lib/utils/common":3,"./lib/zlib/constants":6}]},{},[])("/")});
139
+ </script>
140
+ <script>
141
+ !function(){var e={};"object"==typeof module?module.exports=e:window.UPNG=e,function(e,r){e.toRGBA8=function(r){var t=r.width,n=r.height;if(null==r.tabs.acTL)return[e.toRGBA8.decodeImage(r.data,t,n,r).buffer];var i=[];null==r.frames[0].data&&(r.frames[0].data=r.data);for(var a,f=new Uint8Array(t*n*4),o=0;o<r.frames.length;o++){var s=r.frames[o],l=s.rect.x,c=s.rect.y,u=s.rect.width,d=s.rect.height,h=e.toRGBA8.decodeImage(s.data,u,d,r);if(0==o?a=h:0==s.blend?e._copyTile(h,u,d,a,t,n,l,c,0):1==s.blend&&e._copyTile(h,u,d,a,t,n,l,c,1),i.push(a.buffer),a=a.slice(0),0==s.dispose);else if(1==s.dispose)e._copyTile(f,u,d,a,t,n,l,c,0);else if(2==s.dispose){for(var v=o-1;2==r.frames[v].dispose;)v--;a=new Uint8Array(i[v]).slice(0)}}return i},e.toRGBA8.decodeImage=function(r,t,n,i){var a=t*n,f=e.decode._getBPP(i),o=Math.ceil(t*f/8),s=new Uint8Array(4*a),l=new Uint32Array(s.buffer),c=i.ctype,u=i.depth,d=e._bin.readUshort;if(6==c){var h=a<<2;if(8==u)for(var v=0;v<h;v++)s[v]=r[v];if(16==u)for(v=0;v<h;v++)s[v]=r[v<<1]}else if(2==c){var p=i.tabs.tRNS,b=-1,g=-1,m=-1;if(p&&(b=p[0],g=p[1],m=p[2]),8==u)for(v=0;v<a;v++){var y=3*v;s[M=v<<2]=r[y],s[M+1]=r[y+1],s[M+2]=r[y+2],s[M+3]=255,-1!=b&&r[y]==b&&r[y+1]==g&&r[y+2]==m&&(s[M+3]=0)}if(16==u)for(v=0;v<a;v++){y=6*v;s[M=v<<2]=r[y],s[M+1]=r[y+2],s[M+2]=r[y+4],s[M+3]=255,-1!=b&&d(r,y)==b&&d(r,y+2)==g&&d(r,y+4)==m&&(s[M+3]=0)}}else if(3==c){var w=i.tabs.PLTE,A=i.tabs.tRNS,U=A?A.length:0;if(1==u)for(var _=0;_<n;_++){var q=_*o,I=_*t;for(v=0;v<t;v++){var M=I+v<<2,T=3*(z=r[q+(v>>3)]>>7-((7&v)<<0)&1);s[M]=w[T],s[M+1]=w[T+1],s[M+2]=w[T+2],s[M+3]=z<U?A[z]:255}}if(2==u)for(_=0;_<n;_++)for(q=_*o,I=_*t,v=0;v<t;v++){M=I+v<<2,T=3*(z=r[q+(v>>2)]>>6-((3&v)<<1)&3);s[M]=w[T],s[M+1]=w[T+1],s[M+2]=w[T+2],s[M+3]=z<U?A[z]:255}if(4==u)for(_=0;_<n;_++)for(q=_*o,I=_*t,v=0;v<t;v++){M=I+v<<2,T=3*(z=r[q+(v>>1)]>>4-((1&v)<<2)&15);s[M]=w[T],s[M+1]=w[T+1],s[M+2]=w[T+2],s[M+3]=z<U?A[z]:255}if(8==u)for(v=0;v<a;v++){var z;M=v<<2,T=3*(z=r[v]);s[M]=w[T],s[M+1]=w[T+1],s[M+2]=w[T+2],s[M+3]=z<U?A[z]:255}}else if(4==c){if(8==u)for(v=0;v<a;v++){M=v<<2;var R=r[N=v<<1];s[M]=R,s[M+1]=R,s[M+2]=R,s[M+3]=r[N+1]}if(16==u)for(v=0;v<a;v++){var N;M=v<<2,R=r[N=v<<2];s[M]=R,s[M+1]=R,s[M+2]=R,s[M+3]=r[N+2]}}else if(0==c){b=i.tabs.tRNS?i.tabs.tRNS:-1;if(1==u)for(v=0;v<a;v++){var L=(R=255*(r[v>>3]>>7-(7&v)&1))==255*b?0:255;l[v]=L<<24|R<<16|R<<8|R}if(2==u)for(v=0;v<a;v++){L=(R=85*(r[v>>2]>>6-((3&v)<<1)&3))==85*b?0:255;l[v]=L<<24|R<<16|R<<8|R}if(4==u)for(v=0;v<a;v++){L=(R=17*(r[v>>1]>>4-((1&v)<<2)&15))==17*b?0:255;l[v]=L<<24|R<<16|R<<8|R}if(8==u)for(v=0;v<a;v++){L=(R=r[v])==b?0:255;l[v]=L<<24|R<<16|R<<8|R}if(16==u)for(v=0;v<a;v++){R=r[v<<1],L=d(r,v<<1)==b?0:255;l[v]=L<<24|R<<16|R<<8|R}}return s},e.decode=function(r){for(var t,n=new Uint8Array(r),i=8,a=e._bin,f=a.readUshort,o=a.readUint,s={tabs:{},frames:[]},l=new Uint8Array(n.length),c=0,u=0,d=[137,80,78,71,13,10,26,10],h=0;h<8;h++)if(n[h]!=d[h])throw"The input is not a PNG file!";for(;i<n.length;){var v=a.readUint(n,i);i+=4;var p=a.readASCII(n,i,4);if(i+=4,"IHDR"==p)e.decode._IHDR(n,i,s);else if("IDAT"==p){for(h=0;h<v;h++)l[c+h]=n[i+h];c+=v}else if("acTL"==p)s.tabs[p]={num_frames:o(n,i),num_plays:o(n,i+4)},t=new Uint8Array(n.length);else if("fcTL"==p){var b;if(0!=u)(b=s.frames[s.frames.length-1]).data=e.decode._decompress(s,t.slice(0,u),b.rect.width,b.rect.height),u=0;var g={x:o(n,i+12),y:o(n,i+16),width:o(n,i+4),height:o(n,i+8)},m=f(n,i+22);m=f(n,i+20)/(0==m?100:m);var y={rect:g,delay:Math.round(1e3*m),dispose:n[i+24],blend:n[i+25]};s.frames.push(y)}else if("fdAT"==p){for(h=0;h<v-4;h++)t[u+h]=n[i+h+4];u+=v-4}else if("pHYs"==p)s.tabs[p]=[a.readUint(n,i),a.readUint(n,i+4),n[i+8]];else if("cHRM"==p){s.tabs[p]=[];for(h=0;h<8;h++)s.tabs[p].push(a.readUint(n,i+4*h))}else if("tEXt"==p){null==s.tabs[p]&&(s.tabs[p]={});var w=a.nextZero(n,i),A=a.readASCII(n,i,w-i),U=a.readASCII(n,w+1,i+v-w-1);s.tabs[p][A]=U}else if("iTXt"==p){null==s.tabs[p]&&(s.tabs[p]={});w=0;var _=i;w=a.nextZero(n,_);A=a.readASCII(n,_,w-_),n[_=w+1],n[_+1];_+=2,w=a.nextZero(n,_);a.readASCII(n,_,w-_);_=w+1,w=a.nextZero(n,_);a.readUTF8(n,_,w-_);_=w+1;U=a.readUTF8(n,_,v-(_-i));s.tabs[p][A]=U}else if("PLTE"==p)s.tabs[p]=a.readBytes(n,i,v);else if("hIST"==p){var q=s.tabs.PLTE.length/3;s.tabs[p]=[];for(h=0;h<q;h++)s.tabs[p].push(f(n,i+2*h))}else if("tRNS"==p)3==s.ctype?s.tabs[p]=a.readBytes(n,i,v):0==s.ctype?s.tabs[p]=f(n,i):2==s.ctype&&(s.tabs[p]=[f(n,i),f(n,i+2),f(n,i+4)]);else if("gAMA"==p)s.tabs[p]=a.readUint(n,i)/1e5;else if("sRGB"==p)s.tabs[p]=n[i];else if("bKGD"==p)0==s.ctype||4==s.ctype?s.tabs[p]=[f(n,i)]:2==s.ctype||6==s.ctype?s.tabs[p]=[f(n,i),f(n,i+2),f(n,i+4)]:3==s.ctype&&(s.tabs[p]=n[i]);else if("IEND"==p)break;i+=v;a.readUint(n,i);i+=4}0!=u&&((b=s.frames[s.frames.length-1]).data=e.decode._decompress(s,t.slice(0,u),b.rect.width,b.rect.height),u=0);return s.data=e.decode._decompress(s,l,s.width,s.height),delete s.compress,delete s.interlace,delete s.filter,s},e.decode._decompress=function(r,t,n,i){return 0==r.compress&&(t=e.decode._inflate(t)),0==r.interlace?t=e.decode._filterZero(t,r,0,n,i):1==r.interlace&&(t=e.decode._readInterlace(t,r)),t},e.decode._inflate=function(e){return r.inflate(e)},e.decode._readInterlace=function(r,t){for(var n=t.width,i=t.height,a=e.decode._getBPP(t),f=a>>3,o=Math.ceil(n*a/8),s=new Uint8Array(i*o),l=0,c=[0,0,4,0,2,0,1],u=[0,4,0,2,0,1,0],d=[8,8,8,4,4,2,2],h=[8,8,4,4,2,2,1],v=0;v<7;){for(var p=d[v],b=h[v],g=0,m=0,y=c[v];y<i;)y+=p,m++;for(var w=u[v];w<n;)w+=b,g++;var A=Math.ceil(g*a/8);e.decode._filterZero(r,t,l,g,m);for(var U=0,_=c[v];_<i;){for(var q=u[v],I=l+U*A<<3;q<n;){var M;if(1==a)M=(M=r[I>>3])>>7-(7&I)&1,s[_*o+(q>>3)]|=M<<7-((3&q)<<0);if(2==a)M=(M=r[I>>3])>>6-(7&I)&3,s[_*o+(q>>2)]|=M<<6-((3&q)<<1);if(4==a)M=(M=r[I>>3])>>4-(7&I)&15,s[_*o+(q>>1)]|=M<<4-((1&q)<<2);if(a>=8)for(var T=_*o+q*f,z=0;z<f;z++)s[T+z]=r[(I>>3)+z];I+=a,q+=b}U++,_+=p}g*m!=0&&(l+=m*(1+A)),v+=1}return s},e.decode._getBPP=function(e){return[1,null,3,1,2,null,4][e.ctype]*e.depth},e.decode._filterZero=function(r,t,n,i,a){var f=e.decode._getBPP(t),o=Math.ceil(i*f/8),s=e.decode._paeth;f=Math.ceil(f/8);for(var l=0;l<a;l++){var c=n+l*o,u=c+l+1,d=r[u-1];if(0==d)for(var h=0;h<o;h++)r[c+h]=r[u+h];else if(1==d){for(h=0;h<f;h++)r[c+h]=r[u+h];for(h=f;h<o;h++)r[c+h]=r[u+h]+r[c+h-f]&255}else if(0==l){for(h=0;h<f;h++)r[c+h]=r[u+h];if(2==d)for(h=f;h<o;h++)r[c+h]=255&r[u+h];if(3==d)for(h=f;h<o;h++)r[c+h]=r[u+h]+(r[c+h-f]>>1)&255;if(4==d)for(h=f;h<o;h++)r[c+h]=r[u+h]+s(r[c+h-f],0,0)&255}else{if(2==d)for(h=0;h<o;h++)r[c+h]=r[u+h]+r[c+h-o]&255;if(3==d){for(h=0;h<f;h++)r[c+h]=r[u+h]+(r[c+h-o]>>1)&255;for(h=f;h<o;h++)r[c+h]=r[u+h]+(r[c+h-o]+r[c+h-f]>>1)&255}if(4==d){for(h=0;h<f;h++)r[c+h]=r[u+h]+s(0,r[c+h-o],0)&255;for(h=f;h<o;h++)r[c+h]=r[u+h]+s(r[c+h-f],r[c+h-o],r[c+h-f-o])&255}}}return r},e.decode._paeth=function(e,r,t){var n=e+r-t,i=Math.abs(n-e),a=Math.abs(n-r),f=Math.abs(n-t);return i<=a&&i<=f?e:a<=f?r:t},e.decode._IHDR=function(r,t,n){var i=e._bin;n.width=i.readUint(r,t),t+=4,n.height=i.readUint(r,t),t+=4,n.depth=r[t],t++,n.ctype=r[t],t++,n.compress=r[t],t++,n.filter=r[t],t++,n.interlace=r[t],t++},e._bin={nextZero:function(e,r){for(;0!=e[r];)r++;return r},readUshort:function(e,r){return e[r]<<8|e[r+1]},writeUshort:function(e,r,t){e[r]=t>>8&255,e[r+1]=255&t},readUint:function(e,r){return 16777216*e[r]+(e[r+1]<<16|e[r+2]<<8|e[r+3])},writeUint:function(e,r,t){e[r]=t>>24&255,e[r+1]=t>>16&255,e[r+2]=t>>8&255,e[r+3]=255&t},readASCII:function(e,r,t){for(var n="",i=0;i<t;i++)n+=String.fromCharCode(e[r+i]);return n},writeASCII:function(e,r,t){for(var n=0;n<t.length;n++)e[r+n]=t.charCodeAt(n)},readBytes:function(e,r,t){for(var n=[],i=0;i<t;i++)n.push(e[r+i]);return n},pad:function(e){return e.length<2?"0"+e:e},readUTF8:function(r,t,n){for(var i,a="",f=0;f<n;f++)a+="%"+e._bin.pad(r[t+f].toString(16));try{i=decodeURIComponent(a)}catch(i){return e._bin.readASCII(r,t,n)}return i}},e._copyTile=function(e,r,t,n,i,a,f,o,s){for(var l=Math.min(r,i),c=Math.min(t,a),u=0,d=0,h=0;h<c;h++)for(var v=0;v<l;v++)if(f>=0&&o>=0?(u=h*r+v<<2,d=(o+h)*i+f+v<<2):(u=(-o+h)*r-f+v<<2,d=h*i+v<<2),0==s)n[d]=e[u],n[d+1]=e[u+1],n[d+2]=e[u+2],n[d+3]=e[u+3];else if(1==s){var p=e[u+3]*(1/255),b=e[u]*p,g=e[u+1]*p,m=e[u+2]*p,y=n[d+3]*(1/255),w=n[d]*y,A=n[d+1]*y,U=n[d+2]*y,_=1-p,q=p+y*_,I=0==q?0:1/q;n[d+3]=255*q,n[d+0]=(b+w*_)*I,n[d+1]=(g+A*_)*I,n[d+2]=(m+U*_)*I}else if(2==s){p=e[u+3],b=e[u],g=e[u+1],m=e[u+2],y=n[d+3],w=n[d],A=n[d+1],U=n[d+2];p==y&&b==w&&g==A&&m==U?(n[d]=0,n[d+1]=0,n[d+2]=0,n[d+3]=0):(n[d]=b,n[d+1]=g,n[d+2]=m,n[d+3]=p)}else if(3==s){p=e[u+3],b=e[u],g=e[u+1],m=e[u+2],y=n[d+3],w=n[d],A=n[d+1],U=n[d+2];if(p==y&&b==w&&g==A&&m==U)continue;if(p<220&&y>20)return!1}return!0},e.encode=function(r,t,n,i,a,f){null==i&&(i=0),null==f&&(f=!1);var o=e.encode.compress(r,t,n,i,!1,f);return e.encode.compressPNG(o,-1),e.encode._main(o,t,n,a)},e.encodeLL=function(r,t,n,i,a,f,o){for(var s={ctype:0+(1==i?0:2)+(0==a?0:4),depth:f,frames:[]},l=(i+a)*f,c=l*t,u=0;u<r.length;u++)s.frames.push({rect:{x:0,y:0,width:t,height:n},img:new Uint8Array(r[u]),blend:0,dispose:1,bpp:Math.ceil(l/8),bpl:Math.ceil(c/8)});return e.encode.compressPNG(s,4),e.encode._main(s,t,n,o)},e.encode._main=function(r,t,n,i){var a=e.crc.crc,f=e._bin.writeUint,o=e._bin.writeUshort,s=e._bin.writeASCII,l=8,c=r.frames.length>1,u=!1,d=46+(c?20:0);if(3==r.ctype){for(var h=r.plte.length,v=0;v<h;v++)r.plte[v]>>>24!=255&&(u=!0);d+=8+3*h+4+(u?8+1*h+4:0)}for(var p=0;p<r.frames.length;p++){c&&(d+=38),d+=(q=r.frames[p]).cimg.length+12,0!=p&&(d+=4)}d+=12;var b=new Uint8Array(d),g=[137,80,78,71,13,10,26,10];for(v=0;v<8;v++)b[v]=g[v];if(f(b,l,13),s(b,l+=4,"IHDR"),f(b,l+=4,t),f(b,l+=4,n),b[l+=4]=r.depth,b[++l]=r.ctype,b[++l]=0,b[++l]=0,b[++l]=0,f(b,++l,a(b,l-17,17)),f(b,l+=4,1),s(b,l+=4,"sRGB"),b[l+=4]=1,f(b,++l,a(b,l-5,5)),l+=4,c&&(f(b,l,8),s(b,l+=4,"acTL"),f(b,l+=4,r.frames.length),f(b,l+=4,0),f(b,l+=4,a(b,l-12,12)),l+=4),3==r.ctype){f(b,l,3*(h=r.plte.length)),s(b,l+=4,"PLTE"),l+=4;for(v=0;v<h;v++){var m=3*v,y=r.plte[v],w=255&y,A=y>>>8&255,U=y>>>16&255;b[l+m+0]=w,b[l+m+1]=A,b[l+m+2]=U}if(f(b,l+=3*h,a(b,l-3*h-4,3*h+4)),l+=4,u){f(b,l,h),s(b,l+=4,"tRNS"),l+=4;for(v=0;v<h;v++)b[l+v]=r.plte[v]>>>24&255;f(b,l+=h,a(b,l-h-4,h+4)),l+=4}}var _=0;for(p=0;p<r.frames.length;p++){var q=r.frames[p];c&&(f(b,l,26),s(b,l+=4,"fcTL"),f(b,l+=4,_++),f(b,l+=4,q.rect.width),f(b,l+=4,q.rect.height),f(b,l+=4,q.rect.x),f(b,l+=4,q.rect.y),o(b,l+=4,i[p]),o(b,l+=2,1e3),b[l+=2]=q.dispose,b[++l]=q.blend,f(b,++l,a(b,l-30,30)),l+=4);var I=q.cimg;f(b,l,(h=I.length)+(0==p?0:4));var M=l+=4;s(b,l,0==p?"IDAT":"fdAT"),l+=4,0!=p&&(f(b,l,_++),l+=4);for(v=0;v<h;v++)b[l+v]=I[v];f(b,l+=h,a(b,M,l-M)),l+=4}return f(b,l,0),s(b,l+=4,"IEND"),f(b,l+=4,a(b,l-4,4)),l+=4,b.buffer},e.encode.compressPNG=function(r,t){for(var n=0;n<r.frames.length;n++){var i=r.frames[n],a=(i.rect.width,i.rect.height),f=new Uint8Array(a*i.bpl+a);i.cimg=e.encode._filterZero(i.img,a,i.bpp,i.bpl,f,t)}},e.encode.compress=function(r,t,n,i,a,f){null==f&&(f=!1);for(var o=6,s=8,l=255,c=0;c<r.length;c++)for(var u=new Uint8Array(r[c]),d=u.length,h=0;h<d;h+=4)l&=u[h+3];var v=255!=l,p=v&&a,b=e.encode.framize(r,t,n,a,p),g={},m=[],y=[];if(0!=i){var w=[];for(h=0;h<b.length;h++)w.push(b[h].img.buffer);var A=e.encode.concatRGBA(w,a),U=e.quantize(A,i),_=0,q=new Uint8Array(U.abuf);for(h=0;h<b.length;h++){var I=(F=b[h].img).length;y.push(new Uint8Array(U.inds.buffer,_>>2,I>>2));for(c=0;c<I;c+=4)F[c]=q[_+c],F[c+1]=q[_+c+1],F[c+2]=q[_+c+2],F[c+3]=q[_+c+3];_+=I}for(h=0;h<U.plte.length;h++)m.push(U.plte[h].est.rgba)}else for(c=0;c<b.length;c++){var M=b[c],T=new Uint32Array(M.img.buffer),z=M.rect.width,R=(d=T.length,new Uint8Array(d));y.push(R);for(h=0;h<d;h++){var N=T[h];if(0!=h&&N==T[h-1])R[h]=R[h-1];else if(h>z&&N==T[h-z])R[h]=R[h-z];else{var L=g[N];if(null==L&&(g[N]=L=m.length,m.push(N),m.length>=300))break;R[h]=L}}}var P=m.length;P<=256&&0==f&&(s=P<=2?1:P<=4?2:P<=16?4:8,a&&(s=8));for(c=0;c<b.length;c++){(M=b[c]).rect.x,M.rect.y,z=M.rect.width;var S=M.rect.height,D=M.img,B=(new Uint32Array(D.buffer),4*z),x=4;if(P<=256&&0==f){B=Math.ceil(s*z/8);for(var C=new Uint8Array(B*S),G=y[c],Z=0;Z<S;Z++){h=Z*B;var k=Z*z;if(8==s)for(var E=0;E<z;E++)C[h+E]=G[k+E];else if(4==s)for(E=0;E<z;E++)C[h+(E>>1)]|=G[k+E]<<4-4*(1&E);else if(2==s)for(E=0;E<z;E++)C[h+(E>>2)]|=G[k+E]<<6-2*(3&E);else if(1==s)for(E=0;E<z;E++)C[h+(E>>3)]|=G[k+E]<<7-1*(7&E)}D=C,o=3,x=1}else if(0==v&&1==b.length){C=new Uint8Array(z*S*3);var H=z*S;for(h=0;h<H;h++){var F,K=4*h;C[F=3*h]=D[K],C[F+1]=D[K+1],C[F+2]=D[K+2]}D=C,o=2,x=3,B=3*z}M.img=D,M.bpl=B,M.bpp=x}return{ctype:o,depth:s,plte:m,frames:b}},e.encode.framize=function(r,t,n,i,a){for(var f=[],o=0;o<r.length;o++){var s=new Uint8Array(r[o]),l=new Uint32Array(s.buffer),c=0,u=0,d=t,h=n,v=0;if(0==o||a)s=s.slice(0);else{for(var p=i||1==o||2==f[f.length-2].dispose?1:2,b=0,g=1e9,m=0;m<p;m++){for(var y=new Uint8Array(r[o-1-m]),w=new Uint32Array(r[o-1-m]),A=t,U=n,_=-1,q=-1,I=0;I<n;I++)for(var M=0;M<t;M++){var T=I*t+M;l[T]!=w[T]&&(M<A&&(A=M),M>_&&(_=M),I<U&&(U=I),I>q&&(q=I))}var z=-1==_?1:(_-A+1)*(q-U+1);z<g&&(g=z,b=m,-1==_?(c=u=0,d=h=1):(c=A,u=U,d=_-A+1,h=q-U+1))}y=new Uint8Array(r[o-1-b]);1==b&&(f[f.length-1].dispose=2);var R=new Uint8Array(d*h*4);new Uint32Array(R.buffer);e._copyTile(y,t,n,R,d,h,-c,-u,0),e._copyTile(s,t,n,R,d,h,-c,-u,3)?(e._copyTile(s,t,n,R,d,h,-c,-u,2),v=1):(e._copyTile(s,t,n,R,d,h,-c,-u,0),v=0),s=R}f.push({rect:{x:c,y:u,width:d,height:h},img:s,blend:v,dispose:a?1:0})}return f},e.encode._filterZero=function(t,n,i,a,f,o){if(-1!=o){for(var s=0;s<n;s++)e.encode._filterLine(f,t,s,a,i,o);return r.deflate(f)}for(var l=[],c=0;c<5;c++)if(!(n*a>5e5)||2!=c&&3!=c&&4!=c){for(s=0;s<n;s++)e.encode._filterLine(f,t,s,a,i,c);if(l.push(r.deflate(f)),1==i)break}for(var u,d=1e9,h=0;h<l.length;h++)l[h].length<d&&(u=h,d=l[h].length);return l[u]},e.encode._filterLine=function(r,t,n,i,a,f){var o=n*i,s=o+n,l=e.decode._paeth;if(r[s]=f,s++,0==f)for(var c=0;c<i;c++)r[s+c]=t[o+c];else if(1==f){for(c=0;c<a;c++)r[s+c]=t[o+c];for(c=a;c<i;c++)r[s+c]=t[o+c]-t[o+c-a]+256&255}else if(0==n){for(c=0;c<a;c++)r[s+c]=t[o+c];if(2==f)for(c=a;c<i;c++)r[s+c]=t[o+c];if(3==f)for(c=a;c<i;c++)r[s+c]=t[o+c]-(t[o+c-a]>>1)+256&255;if(4==f)for(c=a;c<i;c++)r[s+c]=t[o+c]-l(t[o+c-a],0,0)+256&255}else{if(2==f)for(c=0;c<i;c++)r[s+c]=t[o+c]+256-t[o+c-i]&255;if(3==f){for(c=0;c<a;c++)r[s+c]=t[o+c]+256-(t[o+c-i]>>1)&255;for(c=a;c<i;c++)r[s+c]=t[o+c]+256-(t[o+c-i]+t[o+c-a]>>1)&255}if(4==f){for(c=0;c<a;c++)r[s+c]=t[o+c]+256-l(0,t[o+c-i],0)&255;for(c=a;c<i;c++)r[s+c]=t[o+c]+256-l(t[o+c-a],t[o+c-i],t[o+c-a-i])&255}}},e.crc={table:function(){for(var e=new Uint32Array(256),r=0;r<256;r++){for(var t=r,n=0;n<8;n++)1&t?t=3988292384^t>>>1:t>>>=1;e[r]=t}return e}(),update:function(r,t,n,i){for(var a=0;a<i;a++)r=e.crc.table[255&(r^t[n+a])]^r>>>8;return r},crc:function(r,t,n){return 4294967295^e.crc.update(4294967295,r,t,n)}},e.quantize=function(r,t){for(var n=new Uint8Array(r),i=n.slice(0),a=new Uint32Array(i.buffer),f=e.quantize.getKDtree(i,t),o=f[0],s=f[1],l=(e.quantize.planeDst,n),c=a,u=l.length,d=new Uint8Array(n.length>>2),h=0;h<u;h+=4){var v=l[h]*(1/255),p=l[h+1]*(1/255),b=l[h+2]*(1/255),g=l[h+3]*(1/255),m=e.quantize.getNearest(o,v,p,b,g);d[h>>2]=m.ind,c[h>>2]=m.est.rgba}return{abuf:i.buffer,inds:d,plte:s}},e.quantize.getKDtree=function(r,t,n){null==n&&(n=1e-4);var i=new Uint32Array(r.buffer),a={i0:0,i1:r.length,bst:null,est:null,tdst:0,left:null,right:null};a.bst=e.quantize.stats(r,a.i0,a.i1),a.est=e.quantize.estats(a.bst);for(var f=[a];f.length<t;){for(var o=0,s=0,l=0;l<f.length;l++)f[l].est.L>o&&(o=f[l].est.L,s=l);if(o<n)break;var c=f[s],u=e.quantize.splitPixels(r,i,c.i0,c.i1,c.est.e,c.est.eMq255);if(c.i0>=u||c.i1<=u)c.est.L=0;else{var d={i0:c.i0,i1:u,bst:null,est:null,tdst:0,left:null,right:null};d.bst=e.quantize.stats(r,d.i0,d.i1),d.est=e.quantize.estats(d.bst);var h={i0:u,i1:c.i1,bst:null,est:null,tdst:0,left:null,right:null};h.bst={R:[],m:[],N:c.bst.N-d.bst.N};for(l=0;l<16;l++)h.bst.R[l]=c.bst.R[l]-d.bst.R[l];for(l=0;l<4;l++)h.bst.m[l]=c.bst.m[l]-d.bst.m[l];h.est=e.quantize.estats(h.bst),c.left=d,c.right=h,f[s]=d,f.push(h)}}f.sort(function(e,r){return r.bst.N-e.bst.N});for(l=0;l<f.length;l++)f[l].ind=l;return[a,f]},e.quantize.getNearest=function(r,t,n,i,a){if(null==r.left)return r.tdst=e.quantize.dist(r.est.q,t,n,i,a),r;var f=e.quantize.planeDst(r.est,t,n,i,a),o=r.left,s=r.right;f>0&&(o=r.right,s=r.left);var l=e.quantize.getNearest(o,t,n,i,a);if(l.tdst<=f*f)return l;var c=e.quantize.getNearest(s,t,n,i,a);return c.tdst<l.tdst?c:l},e.quantize.planeDst=function(e,r,t,n,i){var a=e.e;return a[0]*r+a[1]*t+a[2]*n+a[3]*i-e.eMq},e.quantize.dist=function(e,r,t,n,i){var a=r-e[0],f=t-e[1],o=n-e[2],s=i-e[3];return a*a+f*f+o*o+s*s},e.quantize.splitPixels=function(r,t,n,i,a,f){var o=e.quantize.vecDot;i-=4;for(;n<i;){for(;o(r,n,a)<=f;)n+=4;for(;o(r,i,a)>f;)i-=4;if(n>=i)break;var s=t[n>>2];t[n>>2]=t[i>>2],t[i>>2]=s,n+=4,i-=4}for(;o(r,n,a)>f;)n-=4;return n+4},e.quantize.vecDot=function(e,r,t){return e[r]*t[0]+e[r+1]*t[1]+e[r+2]*t[2]+e[r+3]*t[3]},e.quantize.stats=function(e,r,t){for(var n=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],i=[0,0,0,0],a=t-r>>2,f=r;f<t;f+=4){var o=e[f]*(1/255),s=e[f+1]*(1/255),l=e[f+2]*(1/255),c=e[f+3]*(1/255);i[0]+=o,i[1]+=s,i[2]+=l,i[3]+=c,n[0]+=o*o,n[1]+=o*s,n[2]+=o*l,n[3]+=o*c,n[5]+=s*s,n[6]+=s*l,n[7]+=s*c,n[10]+=l*l,n[11]+=l*c,n[15]+=c*c}return n[4]=n[1],n[8]=n[2],n[9]=n[6],n[12]=n[3],n[13]=n[7],n[14]=n[11],{R:n,m:i,N:a}},e.quantize.estats=function(r){var t=r.R,n=r.m,i=r.N,a=n[0],f=n[1],o=n[2],s=n[3],l=0==i?0:1/i,c=[t[0]-a*a*l,t[1]-a*f*l,t[2]-a*o*l,t[3]-a*s*l,t[4]-f*a*l,t[5]-f*f*l,t[6]-f*o*l,t[7]-f*s*l,t[8]-o*a*l,t[9]-o*f*l,t[10]-o*o*l,t[11]-o*s*l,t[12]-s*a*l,t[13]-s*f*l,t[14]-s*o*l,t[15]-s*s*l],u=c,d=e.M4,h=[.5,.5,.5,.5],v=0,p=0;if(0!=i)for(var b=0;b<10&&(h=d.multVec(u,h),p=Math.sqrt(d.dot(h,h)),h=d.sml(1/p,h),!(Math.abs(p-v)<1e-9));b++)v=p;var g=[a*l,f*l,o*l,s*l];return{Cov:c,q:g,e:h,L:v,eMq255:d.dot(d.sml(255,g),h),eMq:d.dot(h,g),rgba:(Math.round(255*g[3])<<24|Math.round(255*g[2])<<16|Math.round(255*g[1])<<8|Math.round(255*g[0])<<0)>>>0}},e.M4={multVec:function(e,r){return[e[0]*r[0]+e[1]*r[1]+e[2]*r[2]+e[3]*r[3],e[4]*r[0]+e[5]*r[1]+e[6]*r[2]+e[7]*r[3],e[8]*r[0]+e[9]*r[1]+e[10]*r[2]+e[11]*r[3],e[12]*r[0]+e[13]*r[1]+e[14]*r[2]+e[15]*r[3]]},dot:function(e,r){return e[0]*r[0]+e[1]*r[1]+e[2]*r[2]+e[3]*r[3]},sml:function(e,r){return[e*r[0],e*r[1],e*r[2],e*r[3]]}},e.encode.concatRGBA=function(e,r){for(var t=0,n=0;n<e.length;n++)t+=e[n].byteLength;var i=new Uint8Array(t),a=0;for(n=0;n<e.length;n++){for(var f=new Uint8Array(e[n]),o=f.length,s=0;s<o;s+=4){var l=f[s],c=f[s+1],u=f[s+2],d=f[s+3];r&&(d=0==(128&d)?0:255),0==d&&(l=c=u=0),i[a+s]=l,i[a+s+1]=c,i[a+s+2]=u,i[a+s+3]=d}a+=o}return i.buffer}}(e,"function"==typeof require?require("pako"):window.pako)}();
142
+ </script>
143
+
144
+ <script>
145
+ class Player {
146
+
147
+ constructor(container) {
148
+ this.container = container
149
+ this.global_frac = 0.0
150
+ this.container = document.getElementById(container)
151
+ this.progress = null;
152
+ this.mat = [[]]
153
+
154
+ this.player = this.container.querySelector('audio')
155
+ this.demo_img = this.container.querySelector('.underlay > img')
156
+ this.overlay = this.container.querySelector('.overlay')
157
+ this.playpause = this.container.querySelector(".playpause");
158
+ this.download = this.container.querySelector(".download");
159
+ this.play_img = this.container.querySelector('.play-img')
160
+ this.pause_img = this.container.querySelector('.pause-img')
161
+ this.canvas = this.container.querySelector('.response-canvas')
162
+ this.response_container = this.container.querySelector('.response')
163
+ this.context = this.canvas.getContext('2d');
164
+
165
+ // console.log(this.player.duration)
166
+ var togglePlayPause = () => {
167
+ if (this.player.networkState !== 1) {
168
+ return
169
+ }
170
+ if (this.player.paused || this.player.ended) {
171
+ this.play()
172
+ } else {
173
+ this.pause()
174
+ }
175
+ }
176
+
177
+ this.update = () => {
178
+ this.global_frac = this.player.currentTime / this.player.duration
179
+ // this.global_frac = frac
180
+ // console.log(this.player.currentTime, this.player.duration, this.global_frac)
181
+ this.overlay.style.width = (100*(1.0 - this.global_frac)).toString() + '%'
182
+ this.redraw()
183
+ }
184
+
185
+ // var start = null;
186
+ this.updateLoop = (timestamp) => {
187
+ // if (!start) start = timestamp;
188
+ // var progress = timestamp - start;
189
+ this.update()
190
+ // this.progress = setTimeout(this.updateLoop, 10)
191
+ this.progress = window.requestAnimationFrame(this.updateLoop)
192
+ }
193
+
194
+ this.seek = (e) => {
195
+ this.global_frac = e.offsetX / this.demo_img.width
196
+ this.player.currentTime = this.global_frac * this.player.duration
197
+ // console.log(this.global_frac)
198
+ this.overlay.style.width = (100*(1.0 - this.global_frac)).toString() + '%'
199
+ this.redraw()
200
+ }
201
+
202
+ var download_audio = () => {
203
+ var url = this.player.querySelector('#src').src
204
+ const a = document.createElement('a')
205
+ a.href = url
206
+ a.download = "download"
207
+ document.body.appendChild(a)
208
+ a.click()
209
+ document.body.removeChild(a)
210
+ }
211
+
212
+ this.demo_img.onclick = this.seek;
213
+ this.playpause.disabled = true
214
+ this.player.onplay = this.updateLoop
215
+ this.player.onpause = () => {
216
+ window.cancelAnimationFrame(this.progress)
217
+ this.update();
218
+ }
219
+ this.player.onended = () => {this.pause()}
220
+ this.playpause.onclick = togglePlayPause;
221
+ this.download.onclick = download_audio;
222
+ }
223
+
224
+ load(audio_fname, img_fname, levels_fname) {
225
+ this.pause()
226
+ window.cancelAnimationFrame(this.progress)
227
+ this.playpause.disabled = true
228
+
229
+ this.player.querySelector('#src').setAttribute("src", audio_fname)
230
+ this.player.load()
231
+ this.demo_img.setAttribute("src", img_fname)
232
+ this.overlay.style.width = '0%'
233
+
234
+ fetch(levels_fname)
235
+ .then(response => response.arrayBuffer())
236
+ .then(text => {
237
+ this.mat = this.parse(text);
238
+ this.playpause.disabled = false;
239
+ this.redraw();
240
+ })
241
+ }
242
+
243
+ parse(buffer) {
244
+ var img = UPNG.decode(buffer)
245
+ var dat = UPNG.toRGBA8(img)[0]
246
+ var view = new DataView(dat)
247
+ var data = new Array(img.width).fill(0).map(() => new Array(img.height).fill(0));
248
+
249
+ var min =100
250
+ var max = -100
251
+ var idx = 0
252
+ for (let i=0; i < img.height*img.width*4; i+=4) {
253
+ var rgba = [view.getUint8(i, 1) / 255, view.getUint8(i + 1, 1) / 255, view.getUint8(i + 2, 1) / 255, view.getUint8(i + 3, 1) / 255]
254
+ var norm = Math.pow(Math.pow(rgba[0], 2) + Math.pow(rgba[1], 2) + Math.pow(rgba[2], 2), 0.5)
255
+ data[idx % img.width][img.height - Math.floor(idx / img.width) - 1] = norm
256
+
257
+ idx += 1
258
+ min = Math.min(min, norm)
259
+ max = Math.max(max, norm)
260
+ }
261
+ for (let i = 0; i < data.length; i++) {
262
+ for (let j = 0; j < data[i].length; j++) {
263
+ data[i][j] = Math.pow((data[i][j] - min) / (max - min), 1.5)
264
+ }
265
+ }
266
+ var data3 = new Array(img.width).fill(0).map(() => new Array(img.height).fill(0));
267
+ for (let i = 0; i < data.length; i++) {
268
+ for (let j = 0; j < data[i].length; j++) {
269
+ if (i == 0 || i == (data.length - 1)) {
270
+ data3[i][j] = data[i][j]
271
+ } else{
272
+ data3[i][j] = 0.33*(data[i - 1][j]) + 0.33*(data[i][j]) + 0.33*(data[i + 1][j])
273
+ // data3[i][j] = 0.00*(data[i - 1][j]) + 1.00*(data[i][j]) + 0.00*(data[i + 1][j])
274
+ }
275
+ }
276
+ }
277
+
278
+ var scale = 5
279
+ var data2 = new Array(scale*img.width).fill(0).map(() => new Array(img.height).fill(0));
280
+ for (let j = 0; j < data[0].length; j++) {
281
+ for (let i = 0; i < data.length - 1; i++) {
282
+ for (let k = 0; k < scale; k++) {
283
+ data2[scale*i + k][j] = (1.0 - (k/scale))*data3[i][j] + (k / scale)*data3[i + 1][j]
284
+ }
285
+ }
286
+ }
287
+ return data2
288
+ }
289
+
290
+ play() {
291
+ this.player.play();
292
+ this.play_img.style.display = 'none'
293
+ this.pause_img.style.display = 'block'
294
+ }
295
+
296
+ pause() {
297
+ this.player.pause();
298
+ this.pause_img.style.display = 'none'
299
+ this.play_img.style.display = 'block'
300
+ }
301
+
302
+ redraw() {
303
+ this.canvas.width = window.devicePixelRatio*this.response_container.offsetWidth;
304
+ this.canvas.height = window.devicePixelRatio*this.response_container.offsetHeight;
305
+
306
+ this.context.clearRect(0, 0, this.canvas.width, this.canvas.height)
307
+ this.canvas.style.width = (this.canvas.width / window.devicePixelRatio).toString() + "px";
308
+ this.canvas.style.height = (this.canvas.height / window.devicePixelRatio).toString() + "px";
309
+
310
+ var f = this.global_frac*this.mat.length
311
+ var tstep = Math.min(Math.floor(f), this.mat.length - 2)
312
+ var heights = this.mat[tstep]
313
+ var bar_width = (this.canvas.width / heights.length) - 1
314
+
315
+ for (let k = 0; k < heights.length - 1; k++) {
316
+ var height = Math.max(Math.round((heights[k])*this.canvas.height), 3)
317
+ this.context.fillStyle = '#696f7b';
318
+ this.context.fillRect(k*(bar_width + 1), (this.canvas.height - height) / 2, bar_width, height);
319
+ }
320
+ }
321
+ }
322
+ </script>
audiotools/core/templates/pandoc.css ADDED
@@ -0,0 +1,407 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Copyright (c) 2017 Chris Patuzzo
3
+ https://twitter.com/chrispatuzzo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+ */
23
+
24
+ body {
25
+ font-family: Helvetica, arial, sans-serif;
26
+ font-size: 14px;
27
+ line-height: 1.6;
28
+ padding-top: 10px;
29
+ padding-bottom: 10px;
30
+ background-color: white;
31
+ padding: 30px;
32
+ color: #333;
33
+ }
34
+
35
+ body > *:first-child {
36
+ margin-top: 0 !important;
37
+ }
38
+
39
+ body > *:last-child {
40
+ margin-bottom: 0 !important;
41
+ }
42
+
43
+ a {
44
+ color: #4183C4;
45
+ text-decoration: none;
46
+ }
47
+
48
+ a.absent {
49
+ color: #cc0000;
50
+ }
51
+
52
+ a.anchor {
53
+ display: block;
54
+ padding-left: 30px;
55
+ margin-left: -30px;
56
+ cursor: pointer;
57
+ position: absolute;
58
+ top: 0;
59
+ left: 0;
60
+ bottom: 0;
61
+ }
62
+
63
+ h1, h2, h3, h4, h5, h6 {
64
+ margin: 20px 0 10px;
65
+ padding: 0;
66
+ font-weight: bold;
67
+ -webkit-font-smoothing: antialiased;
68
+ cursor: text;
69
+ position: relative;
70
+ }
71
+
72
+ h2:first-child, h1:first-child, h1:first-child + h2, h3:first-child, h4:first-child, h5:first-child, h6:first-child {
73
+ margin-top: 0;
74
+ padding-top: 0;
75
+ }
76
+
77
+ h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, h5:hover a.anchor, h6:hover a.anchor {
78
+ text-decoration: none;
79
+ }
80
+
81
+ h1 tt, h1 code {
82
+ font-size: inherit;
83
+ }
84
+
85
+ h2 tt, h2 code {
86
+ font-size: inherit;
87
+ }
88
+
89
+ h3 tt, h3 code {
90
+ font-size: inherit;
91
+ }
92
+
93
+ h4 tt, h4 code {
94
+ font-size: inherit;
95
+ }
96
+
97
+ h5 tt, h5 code {
98
+ font-size: inherit;
99
+ }
100
+
101
+ h6 tt, h6 code {
102
+ font-size: inherit;
103
+ }
104
+
105
+ h1 {
106
+ font-size: 28px;
107
+ color: black;
108
+ }
109
+
110
+ h2 {
111
+ font-size: 24px;
112
+ border-bottom: 1px solid #cccccc;
113
+ color: black;
114
+ }
115
+
116
+ h3 {
117
+ font-size: 18px;
118
+ }
119
+
120
+ h4 {
121
+ font-size: 16px;
122
+ }
123
+
124
+ h5 {
125
+ font-size: 14px;
126
+ }
127
+
128
+ h6 {
129
+ color: #777777;
130
+ font-size: 14px;
131
+ }
132
+
133
+ p, blockquote, ul, ol, dl, li, table, pre {
134
+ margin: 15px 0;
135
+ }
136
+
137
+ hr {
138
+ border: 0 none;
139
+ color: #cccccc;
140
+ height: 4px;
141
+ padding: 0;
142
+ }
143
+
144
+ body > h2:first-child {
145
+ margin-top: 0;
146
+ padding-top: 0;
147
+ }
148
+
149
+ body > h1:first-child {
150
+ margin-top: 0;
151
+ padding-top: 0;
152
+ }
153
+
154
+ body > h1:first-child + h2 {
155
+ margin-top: 0;
156
+ padding-top: 0;
157
+ }
158
+
159
+ body > h3:first-child, body > h4:first-child, body > h5:first-child, body > h6:first-child {
160
+ margin-top: 0;
161
+ padding-top: 0;
162
+ }
163
+
164
+ a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 {
165
+ margin-top: 0;
166
+ padding-top: 0;
167
+ }
168
+
169
+ h1 p, h2 p, h3 p, h4 p, h5 p, h6 p {
170
+ margin-top: 0;
171
+ }
172
+
173
+ li p.first {
174
+ display: inline-block;
175
+ }
176
+
177
+ ul, ol {
178
+ padding-left: 30px;
179
+ }
180
+
181
+ ul :first-child, ol :first-child {
182
+ margin-top: 0;
183
+ }
184
+
185
+ ul :last-child, ol :last-child {
186
+ margin-bottom: 0;
187
+ }
188
+
189
+ dl {
190
+ padding: 0;
191
+ }
192
+
193
+ dl dt {
194
+ font-size: 14px;
195
+ font-weight: bold;
196
+ font-style: italic;
197
+ padding: 0;
198
+ margin: 15px 0 5px;
199
+ }
200
+
201
+ dl dt:first-child {
202
+ padding: 0;
203
+ }
204
+
205
+ dl dt > :first-child {
206
+ margin-top: 0;
207
+ }
208
+
209
+ dl dt > :last-child {
210
+ margin-bottom: 0;
211
+ }
212
+
213
+ dl dd {
214
+ margin: 0 0 15px;
215
+ padding: 0 15px;
216
+ }
217
+
218
+ dl dd > :first-child {
219
+ margin-top: 0;
220
+ }
221
+
222
+ dl dd > :last-child {
223
+ margin-bottom: 0;
224
+ }
225
+
226
+ blockquote {
227
+ border-left: 4px solid #dddddd;
228
+ padding: 0 15px;
229
+ color: #777777;
230
+ }
231
+
232
+ blockquote > :first-child {
233
+ margin-top: 0;
234
+ }
235
+
236
+ blockquote > :last-child {
237
+ margin-bottom: 0;
238
+ }
239
+
240
+ table {
241
+ padding: 0;
242
+ }
243
+ table tr {
244
+ border-top: 1px solid #cccccc;
245
+ background-color: white;
246
+ margin: 0;
247
+ padding: 0;
248
+ }
249
+
250
+ table tr:nth-child(2n) {
251
+ background-color: #f8f8f8;
252
+ }
253
+
254
+ table tr th {
255
+ font-weight: bold;
256
+ border: 1px solid #cccccc;
257
+ text-align: left;
258
+ margin: 0;
259
+ padding: 6px 13px;
260
+ }
261
+
262
+ table tr td {
263
+ border: 1px solid #cccccc;
264
+ text-align: left;
265
+ margin: 0;
266
+ padding: 6px 13px;
267
+ }
268
+
269
+ table tr th :first-child, table tr td :first-child {
270
+ margin-top: 0;
271
+ }
272
+
273
+ table tr th :last-child, table tr td :last-child {
274
+ margin-bottom: 0;
275
+ }
276
+
277
+ img {
278
+ max-width: 100%;
279
+ }
280
+
281
+ span.frame {
282
+ display: block;
283
+ overflow: hidden;
284
+ }
285
+
286
+ span.frame > span {
287
+ border: 1px solid #dddddd;
288
+ display: block;
289
+ float: left;
290
+ overflow: hidden;
291
+ margin: 13px 0 0;
292
+ padding: 7px;
293
+ width: auto;
294
+ }
295
+
296
+ span.frame span img {
297
+ display: block;
298
+ float: left;
299
+ }
300
+
301
+ span.frame span span {
302
+ clear: both;
303
+ color: #333333;
304
+ display: block;
305
+ padding: 5px 0 0;
306
+ }
307
+
308
+ span.align-center {
309
+ display: block;
310
+ overflow: hidden;
311
+ clear: both;
312
+ }
313
+
314
+ span.align-center > span {
315
+ display: block;
316
+ overflow: hidden;
317
+ margin: 13px auto 0;
318
+ text-align: center;
319
+ }
320
+
321
+ span.align-center span img {
322
+ margin: 0 auto;
323
+ text-align: center;
324
+ }
325
+
326
+ span.align-right {
327
+ display: block;
328
+ overflow: hidden;
329
+ clear: both;
330
+ }
331
+
332
+ span.align-right > span {
333
+ display: block;
334
+ overflow: hidden;
335
+ margin: 13px 0 0;
336
+ text-align: right;
337
+ }
338
+
339
+ span.align-right span img {
340
+ margin: 0;
341
+ text-align: right;
342
+ }
343
+
344
+ span.float-left {
345
+ display: block;
346
+ margin-right: 13px;
347
+ overflow: hidden;
348
+ float: left;
349
+ }
350
+
351
+ span.float-left span {
352
+ margin: 13px 0 0;
353
+ }
354
+
355
+ span.float-right {
356
+ display: block;
357
+ margin-left: 13px;
358
+ overflow: hidden;
359
+ float: right;
360
+ }
361
+
362
+ span.float-right > span {
363
+ display: block;
364
+ overflow: hidden;
365
+ margin: 13px auto 0;
366
+ text-align: right;
367
+ }
368
+
369
+ code, tt {
370
+ margin: 0 2px;
371
+ padding: 0 5px;
372
+ white-space: nowrap;
373
+ border-radius: 3px;
374
+ }
375
+
376
+ pre code {
377
+ margin: 0;
378
+ padding: 0;
379
+ white-space: pre;
380
+ border: none;
381
+ background: transparent;
382
+ }
383
+
384
+ .highlight pre {
385
+ font-size: 13px;
386
+ line-height: 19px;
387
+ overflow: auto;
388
+ padding: 6px 10px;
389
+ border-radius: 3px;
390
+ }
391
+
392
+ pre {
393
+ font-size: 13px;
394
+ line-height: 19px;
395
+ overflow: auto;
396
+ padding: 6px 10px;
397
+ border-radius: 3px;
398
+ }
399
+
400
+ pre code, pre tt {
401
+ background-color: transparent;
402
+ border: none;
403
+ }
404
+
405
+ body {
406
+ max-width: 600px;
407
+ }
audiotools/core/templates/widget.html ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div id='PLAYER_ID' class='player' style="max-width: MAX_WIDTH;">
2
+ <div class='spectrogram' style="padding-top: PADDING_AMOUNT;">
3
+ <div class='overlay'></div>
4
+ <div class='underlay'>
5
+ <img>
6
+ </div>
7
+ </div>
8
+
9
+ <div class='audio-controls'>
10
+ <button id="playpause" disabled class='playpause' title="play">
11
+ <svg class='play-img' width="14px" height="19px" viewBox="0 0 14 19">
12
+ <polygon id="Triangle" fill="#000000" transform="translate(9, 9.5) rotate(90) translate(-7, -9.5) " points="7 2.5 16.5 16.5 -2.5 16.5"></polygon>
13
+ </svg>
14
+ <svg class='pause-img' width="16px" height="19px" viewBox="0 0 16 19">
15
+ <g fill="#000000" stroke="#000000">
16
+ <rect id="Rectangle" x="0.5" y="0.5" width="4" height="18"></rect>
17
+ <rect id="Rectangle" x="11.5" y="0.5" width="4" height="18"></rect>
18
+ </g>
19
+ </svg>
20
+ </button>
21
+
22
+ <audio class='play'>
23
+ <source id='src'>
24
+ </audio>
25
+ <div class='response'>
26
+ <canvas class='response-canvas'></canvas>
27
+ </div>
28
+
29
+ <button id="download" class='download' title="download">
30
+ <svg class='download-img' x="0px" y="0px" viewBox="0 0 29.978 29.978" style="enable-background:new 0 0 29.978 29.978;" xml:space="preserve">
31
+ <g>
32
+ <path d="M25.462,19.105v6.848H4.515v-6.848H0.489v8.861c0,1.111,0.9,2.012,2.016,2.012h24.967c1.115,0,2.016-0.9,2.016-2.012
33
+ v-8.861H25.462z"/>
34
+ <path d="M14.62,18.426l-5.764-6.965c0,0-0.877-0.828,0.074-0.828s3.248,0,3.248,0s0-0.557,0-1.416c0-2.449,0-6.906,0-8.723
35
+ c0,0-0.129-0.494,0.615-0.494c0.75,0,4.035,0,4.572,0c0.536,0,0.524,0.416,0.524,0.416c0,1.762,0,6.373,0,8.742
36
+ c0,0.768,0,1.266,0,1.266s1.842,0,2.998,0c1.154,0,0.285,0.867,0.285,0.867s-4.904,6.51-5.588,7.193
37
+ C15.092,18.979,14.62,18.426,14.62,18.426z"/>
38
+ </g>
39
+ </svg>
40
+ </button>
41
+ </div>
42
+ </div>
43
+
44
+ <script>
45
+ var PLAYER_ID = new Player('PLAYER_ID')
46
+ PLAYER_ID.load(
47
+ "AUDIO_SRC",
48
+ "IMAGE_SRC",
49
+ "LEVELS_SRC"
50
+ )
51
+ window.addEventListener("resize", function() {PLAYER_ID.redraw()})
52
+ </script>
audiotools/core/util.py ADDED
@@ -0,0 +1,671 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import csv
2
+ import glob
3
+ import math
4
+ import numbers
5
+ import os
6
+ import random
7
+ import typing
8
+ from contextlib import contextmanager
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Dict
12
+ from typing import List
13
+
14
+ import numpy as np
15
+ import torch
16
+ import torchaudio
17
+ from flatten_dict import flatten
18
+ from flatten_dict import unflatten
19
+
20
+
21
+ @dataclass
22
+ class Info:
23
+ """Shim for torchaudio.info API changes."""
24
+
25
+ sample_rate: float
26
+ num_frames: int
27
+
28
+ @property
29
+ def duration(self) -> float:
30
+ return self.num_frames / self.sample_rate
31
+
32
+
33
+ def info(audio_path: str):
34
+ """Shim for torchaudio.info to make 0.7.2 API match 0.8.0.
35
+
36
+ Parameters
37
+ ----------
38
+ audio_path : str
39
+ Path to audio file.
40
+ """
41
+ # try default backend first, then fallback to soundfile
42
+ try:
43
+ info = torchaudio.info(str(audio_path))
44
+ except: # pragma: no cover
45
+ info = torchaudio.backend.soundfile_backend.info(str(audio_path))
46
+
47
+ if isinstance(info, tuple): # pragma: no cover
48
+ signal_info = info[0]
49
+ info = Info(sample_rate=signal_info.rate, num_frames=signal_info.length)
50
+ else:
51
+ info = Info(sample_rate=info.sample_rate, num_frames=info.num_frames)
52
+
53
+ return info
54
+
55
+
56
+ def ensure_tensor(
57
+ x: typing.Union[np.ndarray, torch.Tensor, float, int],
58
+ ndim: int = None,
59
+ batch_size: int = None,
60
+ ):
61
+ """Ensures that the input ``x`` is a tensor of specified
62
+ dimensions and batch size.
63
+
64
+ Parameters
65
+ ----------
66
+ x : typing.Union[np.ndarray, torch.Tensor, float, int]
67
+ Data that will become a tensor on its way out.
68
+ ndim : int, optional
69
+ How many dimensions should be in the output, by default None
70
+ batch_size : int, optional
71
+ The batch size of the output, by default None
72
+
73
+ Returns
74
+ -------
75
+ torch.Tensor
76
+ Modified version of ``x`` as a tensor.
77
+ """
78
+ if not torch.is_tensor(x):
79
+ x = torch.as_tensor(x)
80
+ if ndim is not None:
81
+ assert x.ndim <= ndim
82
+ while x.ndim < ndim:
83
+ x = x.unsqueeze(-1)
84
+ if batch_size is not None:
85
+ if x.shape[0] != batch_size:
86
+ shape = list(x.shape)
87
+ shape[0] = batch_size
88
+ x = x.expand(*shape)
89
+ return x
90
+
91
+
92
+ def _get_value(other):
93
+ from . import AudioSignal
94
+
95
+ if isinstance(other, AudioSignal):
96
+ return other.audio_data
97
+ return other
98
+
99
+
100
+ def hz_to_bin(hz: torch.Tensor, n_fft: int, sample_rate: int):
101
+ """Closest frequency bin given a frequency, number
102
+ of bins, and a sampling rate.
103
+
104
+ Parameters
105
+ ----------
106
+ hz : torch.Tensor
107
+ Tensor of frequencies in Hz.
108
+ n_fft : int
109
+ Number of FFT bins.
110
+ sample_rate : int
111
+ Sample rate of audio.
112
+
113
+ Returns
114
+ -------
115
+ torch.Tensor
116
+ Closest bins to the data.
117
+ """
118
+ shape = hz.shape
119
+ hz = hz.flatten()
120
+ freqs = torch.linspace(0, sample_rate / 2, 2 + n_fft // 2)
121
+ hz[hz > sample_rate / 2] = sample_rate / 2
122
+
123
+ closest = (hz[None, :] - freqs[:, None]).abs()
124
+ closest_bins = closest.min(dim=0).indices
125
+
126
+ return closest_bins.reshape(*shape)
127
+
128
+
129
+ def random_state(seed: typing.Union[int, np.random.RandomState]):
130
+ """
131
+ Turn seed into a np.random.RandomState instance.
132
+
133
+ Parameters
134
+ ----------
135
+ seed : typing.Union[int, np.random.RandomState] or None
136
+ If seed is None, return the RandomState singleton used by np.random.
137
+ If seed is an int, return a new RandomState instance seeded with seed.
138
+ If seed is already a RandomState instance, return it.
139
+ Otherwise raise ValueError.
140
+
141
+ Returns
142
+ -------
143
+ np.random.RandomState
144
+ Random state object.
145
+
146
+ Raises
147
+ ------
148
+ ValueError
149
+ If seed is not valid, an error is thrown.
150
+ """
151
+ if seed is None or seed is np.random:
152
+ return np.random.mtrand._rand
153
+ elif isinstance(seed, (numbers.Integral, np.integer, int)):
154
+ return np.random.RandomState(seed)
155
+ elif isinstance(seed, np.random.RandomState):
156
+ return seed
157
+ else:
158
+ raise ValueError(
159
+ "%r cannot be used to seed a numpy.random.RandomState" " instance" % seed
160
+ )
161
+
162
+
163
+ def seed(random_seed, set_cudnn=False):
164
+ """
165
+ Seeds all random states with the same random seed
166
+ for reproducibility. Seeds ``numpy``, ``random`` and ``torch``
167
+ random generators.
168
+ For full reproducibility, two further options must be set
169
+ according to the torch documentation:
170
+ https://pytorch.org/docs/stable/notes/randomness.html
171
+ To do this, ``set_cudnn`` must be True. It defaults to
172
+ False, since setting it to True results in a performance
173
+ hit.
174
+
175
+ Args:
176
+ random_seed (int): integer corresponding to random seed to
177
+ use.
178
+ set_cudnn (bool): Whether or not to set cudnn into determinstic
179
+ mode and off of benchmark mode. Defaults to False.
180
+ """
181
+
182
+ torch.manual_seed(random_seed)
183
+ np.random.seed(random_seed)
184
+ random.seed(random_seed)
185
+
186
+ if set_cudnn:
187
+ torch.backends.cudnn.deterministic = True
188
+ torch.backends.cudnn.benchmark = False
189
+
190
+
191
+ @contextmanager
192
+ def _close_temp_files(tmpfiles: list):
193
+ """Utility function for creating a context and closing all temporary files
194
+ once the context is exited. For correct functionality, all temporary file
195
+ handles created inside the context must be appended to the ```tmpfiles```
196
+ list.
197
+
198
+ This function is taken wholesale from Scaper.
199
+
200
+ Parameters
201
+ ----------
202
+ tmpfiles : list
203
+ List of temporary file handles
204
+ """
205
+
206
+ def _close():
207
+ for t in tmpfiles:
208
+ try:
209
+ t.close()
210
+ os.unlink(t.name)
211
+ except:
212
+ pass
213
+
214
+ try:
215
+ yield
216
+ except: # pragma: no cover
217
+ _close()
218
+ raise
219
+ _close()
220
+
221
+
222
+ AUDIO_EXTENSIONS = [".wav", ".flac", ".mp3", ".mp4"]
223
+
224
+
225
+ def find_audio(folder: str, ext: List[str] = AUDIO_EXTENSIONS):
226
+ """Finds all audio files in a directory recursively.
227
+ Returns a list.
228
+
229
+ Parameters
230
+ ----------
231
+ folder : str
232
+ Folder to look for audio files in, recursively.
233
+ ext : List[str], optional
234
+ Extensions to look for without the ., by default
235
+ ``['.wav', '.flac', '.mp3', '.mp4']``.
236
+ """
237
+ folder = Path(folder)
238
+ # Take care of case where user has passed in an audio file directly
239
+ # into one of the calling functions.
240
+ if str(folder).endswith(tuple(ext)):
241
+ # if, however, there's a glob in the path, we need to
242
+ # return the glob, not the file.
243
+ if "*" in str(folder):
244
+ return glob.glob(str(folder), recursive=("**" in str(folder)))
245
+ else:
246
+ return [folder]
247
+
248
+ files = []
249
+ for x in ext:
250
+ files += folder.glob(f"**/*{x}")
251
+ return files
252
+
253
+
254
+ def read_sources(
255
+ sources: List[str],
256
+ remove_empty: bool = True,
257
+ relative_path: str = "",
258
+ ext: List[str] = AUDIO_EXTENSIONS,
259
+ ):
260
+ """Reads audio sources that can either be folders
261
+ full of audio files, or CSV files that contain paths
262
+ to audio files. CSV files that adhere to the expected
263
+ format can be generated by
264
+ :py:func:`audiotools.data.preprocess.create_csv`.
265
+
266
+ Parameters
267
+ ----------
268
+ sources : List[str]
269
+ List of audio sources to be converted into a
270
+ list of lists of audio files.
271
+ remove_empty : bool, optional
272
+ Whether or not to remove rows with an empty "path"
273
+ from each CSV file, by default True.
274
+
275
+ Returns
276
+ -------
277
+ list
278
+ List of lists of rows of CSV files.
279
+ """
280
+ files = []
281
+ relative_path = Path(relative_path)
282
+ for source in sources:
283
+ source = str(source)
284
+ _files = []
285
+ if source.endswith(".csv"):
286
+ with open(source, "r") as f:
287
+ reader = csv.DictReader(f)
288
+ for x in reader:
289
+ if remove_empty and x["path"] == "":
290
+ continue
291
+ if x["path"] != "":
292
+ x["path"] = str(relative_path / x["path"])
293
+ _files.append(x)
294
+ else:
295
+ for x in find_audio(source, ext=ext):
296
+ x = str(relative_path / x)
297
+ _files.append({"path": x})
298
+ files.append(sorted(_files, key=lambda x: x["path"]))
299
+ return files
300
+
301
+
302
+ def choose_from_list_of_lists(
303
+ state: np.random.RandomState, list_of_lists: list, p: float = None
304
+ ):
305
+ """Choose a single item from a list of lists.
306
+
307
+ Parameters
308
+ ----------
309
+ state : np.random.RandomState
310
+ Random state to use when choosing an item.
311
+ list_of_lists : list
312
+ A list of lists from which items will be drawn.
313
+ p : float, optional
314
+ Probabilities of each list, by default None
315
+
316
+ Returns
317
+ -------
318
+ typing.Any
319
+ An item from the list of lists.
320
+ """
321
+ source_idx = state.choice(list(range(len(list_of_lists))), p=p)
322
+ item_idx = state.randint(len(list_of_lists[source_idx]))
323
+ return list_of_lists[source_idx][item_idx], source_idx, item_idx
324
+
325
+
326
+ @contextmanager
327
+ def chdir(newdir: typing.Union[Path, str]):
328
+ """
329
+ Context manager for switching directories to run a
330
+ function. Useful for when you want to use relative
331
+ paths to different runs.
332
+
333
+ Parameters
334
+ ----------
335
+ newdir : typing.Union[Path, str]
336
+ Directory to switch to.
337
+ """
338
+ curdir = os.getcwd()
339
+ try:
340
+ os.chdir(newdir)
341
+ yield
342
+ finally:
343
+ os.chdir(curdir)
344
+
345
+
346
+ def prepare_batch(batch: typing.Union[dict, list, torch.Tensor], device: str = "cpu"):
347
+ """Moves items in a batch (typically generated by a DataLoader as a list
348
+ or a dict) to the specified device. This works even if dictionaries
349
+ are nested.
350
+
351
+ Parameters
352
+ ----------
353
+ batch : typing.Union[dict, list, torch.Tensor]
354
+ Batch, typically generated by a dataloader, that will be moved to
355
+ the device.
356
+ device : str, optional
357
+ Device to move batch to, by default "cpu"
358
+
359
+ Returns
360
+ -------
361
+ typing.Union[dict, list, torch.Tensor]
362
+ Batch with all values moved to the specified device.
363
+ """
364
+ if isinstance(batch, dict):
365
+ batch = flatten(batch)
366
+ for key, val in batch.items():
367
+ try:
368
+ batch[key] = val.to(device)
369
+ except:
370
+ pass
371
+ batch = unflatten(batch)
372
+ elif torch.is_tensor(batch):
373
+ batch = batch.to(device)
374
+ elif isinstance(batch, list):
375
+ for i in range(len(batch)):
376
+ try:
377
+ batch[i] = batch[i].to(device)
378
+ except:
379
+ pass
380
+ return batch
381
+
382
+
383
+ def sample_from_dist(dist_tuple: tuple, state: np.random.RandomState = None):
384
+ """Samples from a distribution defined by a tuple. The first
385
+ item in the tuple is the distribution type, and the rest of the
386
+ items are arguments to that distribution. The distribution function
387
+ is gotten from the ``np.random.RandomState`` object.
388
+
389
+ Parameters
390
+ ----------
391
+ dist_tuple : tuple
392
+ Distribution tuple
393
+ state : np.random.RandomState, optional
394
+ Random state, or seed to use, by default None
395
+
396
+ Returns
397
+ -------
398
+ typing.Union[float, int, str]
399
+ Draw from the distribution.
400
+
401
+ Examples
402
+ --------
403
+ Sample from a uniform distribution:
404
+
405
+ >>> dist_tuple = ("uniform", 0, 1)
406
+ >>> sample_from_dist(dist_tuple)
407
+
408
+ Sample from a constant distribution:
409
+
410
+ >>> dist_tuple = ("const", 0)
411
+ >>> sample_from_dist(dist_tuple)
412
+
413
+ Sample from a normal distribution:
414
+
415
+ >>> dist_tuple = ("normal", 0, 0.5)
416
+ >>> sample_from_dist(dist_tuple)
417
+
418
+ """
419
+ if dist_tuple[0] == "const":
420
+ return dist_tuple[1]
421
+ state = random_state(state)
422
+ dist_fn = getattr(state, dist_tuple[0])
423
+ return dist_fn(*dist_tuple[1:])
424
+
425
+
426
+ def collate(list_of_dicts: list, n_splits: int = None):
427
+ """Collates a list of dictionaries (e.g. as returned by a
428
+ dataloader) into a dictionary with batched values. This routine
429
+ uses the default torch collate function for everything
430
+ except AudioSignal objects, which are handled by the
431
+ :py:func:`audiotools.core.audio_signal.AudioSignal.batch`
432
+ function.
433
+
434
+ This function takes n_splits to enable splitting a batch
435
+ into multiple sub-batches for the purposes of gradient accumulation,
436
+ etc.
437
+
438
+ Parameters
439
+ ----------
440
+ list_of_dicts : list
441
+ List of dictionaries to be collated.
442
+ n_splits : int
443
+ Number of splits to make when creating the batches (split into
444
+ sub-batches). Useful for things like gradient accumulation.
445
+
446
+ Returns
447
+ -------
448
+ dict
449
+ Dictionary containing batched data.
450
+ """
451
+
452
+ from . import AudioSignal
453
+
454
+ batches = []
455
+ list_len = len(list_of_dicts)
456
+
457
+ return_list = False if n_splits is None else True
458
+ n_splits = 1 if n_splits is None else n_splits
459
+ n_items = int(math.ceil(list_len / n_splits))
460
+
461
+ for i in range(0, list_len, n_items):
462
+ # Flatten the dictionaries to avoid recursion.
463
+ list_of_dicts_ = [flatten(d) for d in list_of_dicts[i : i + n_items]]
464
+ dict_of_lists = {
465
+ k: [dic[k] for dic in list_of_dicts_] for k in list_of_dicts_[0]
466
+ }
467
+
468
+ batch = {}
469
+ for k, v in dict_of_lists.items():
470
+ if isinstance(v, list):
471
+ if all(isinstance(s, AudioSignal) for s in v):
472
+ batch[k] = AudioSignal.batch(v, pad_signals=True)
473
+ else:
474
+ # Borrow the default collate fn from torch.
475
+ batch[k] = torch.utils.data._utils.collate.default_collate(v)
476
+ batches.append(unflatten(batch))
477
+
478
+ batches = batches[0] if not return_list else batches
479
+ return batches
480
+
481
+
482
+ BASE_SIZE = 864
483
+ DEFAULT_FIG_SIZE = (9, 3)
484
+
485
+
486
+ def format_figure(
487
+ fig_size: tuple = None,
488
+ title: str = None,
489
+ fig=None,
490
+ format_axes: bool = True,
491
+ format: bool = True,
492
+ font_color: str = "white",
493
+ ):
494
+ """Prettifies the spectrogram and waveform plots. A title
495
+ can be inset into the top right corner, and the axes can be
496
+ inset into the figure, allowing the data to take up the entire
497
+ image. Used in
498
+
499
+ - :py:func:`audiotools.core.display.DisplayMixin.specshow`
500
+ - :py:func:`audiotools.core.display.DisplayMixin.waveplot`
501
+ - :py:func:`audiotools.core.display.DisplayMixin.wavespec`
502
+
503
+ Parameters
504
+ ----------
505
+ fig_size : tuple, optional
506
+ Size of figure, by default (9, 3)
507
+ title : str, optional
508
+ Title to inset in top right, by default None
509
+ fig : matplotlib.figure.Figure, optional
510
+ Figure object, if None ``plt.gcf()`` will be used, by default None
511
+ format_axes : bool, optional
512
+ Format the axes to be inside the figure, by default True
513
+ format : bool, optional
514
+ This formatting can be skipped entirely by passing ``format=False``
515
+ to any of the plotting functions that use this formater, by default True
516
+ font_color : str, optional
517
+ Color of font of axes, by default "white"
518
+ """
519
+ import matplotlib
520
+ import matplotlib.pyplot as plt
521
+
522
+ if fig_size is None:
523
+ fig_size = DEFAULT_FIG_SIZE
524
+ if not format:
525
+ return
526
+ if fig is None:
527
+ fig = plt.gcf()
528
+ fig.set_size_inches(*fig_size)
529
+ axs = fig.axes
530
+
531
+ pixels = (fig.get_size_inches() * fig.dpi)[0]
532
+ font_scale = pixels / BASE_SIZE
533
+
534
+ if format_axes:
535
+ axs = fig.axes
536
+
537
+ for ax in axs:
538
+ ymin, _ = ax.get_ylim()
539
+ xmin, _ = ax.get_xlim()
540
+
541
+ ticks = ax.get_yticks()
542
+ for t in ticks[2:-1]:
543
+ t = axs[0].annotate(
544
+ f"{(t / 1000):2.1f}k",
545
+ xy=(xmin, t),
546
+ xycoords="data",
547
+ xytext=(5, -5),
548
+ textcoords="offset points",
549
+ ha="left",
550
+ va="top",
551
+ color=font_color,
552
+ fontsize=12 * font_scale,
553
+ alpha=0.75,
554
+ )
555
+
556
+ ticks = ax.get_xticks()[2:]
557
+ for t in ticks[:-1]:
558
+ t = axs[0].annotate(
559
+ f"{t:2.1f}s",
560
+ xy=(t, ymin),
561
+ xycoords="data",
562
+ xytext=(5, 5),
563
+ textcoords="offset points",
564
+ ha="center",
565
+ va="bottom",
566
+ color=font_color,
567
+ fontsize=12 * font_scale,
568
+ alpha=0.75,
569
+ )
570
+
571
+ ax.margins(0, 0)
572
+ ax.set_axis_off()
573
+ ax.xaxis.set_major_locator(plt.NullLocator())
574
+ ax.yaxis.set_major_locator(plt.NullLocator())
575
+
576
+ plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0)
577
+
578
+ if title is not None:
579
+ t = axs[0].annotate(
580
+ title,
581
+ xy=(1, 1),
582
+ xycoords="axes fraction",
583
+ fontsize=20 * font_scale,
584
+ xytext=(-5, -5),
585
+ textcoords="offset points",
586
+ ha="right",
587
+ va="top",
588
+ color="white",
589
+ )
590
+ t.set_bbox(dict(facecolor="black", alpha=0.5, edgecolor="black"))
591
+
592
+
593
+ def generate_chord_dataset(
594
+ max_voices: int = 8,
595
+ sample_rate: int = 44100,
596
+ num_items: int = 5,
597
+ duration: float = 1.0,
598
+ min_note: str = "C2",
599
+ max_note: str = "C6",
600
+ output_dir: Path = "chords",
601
+ ):
602
+ """
603
+ Generates a toy multitrack dataset of chords, synthesized from sine waves.
604
+
605
+
606
+ Parameters
607
+ ----------
608
+ max_voices : int, optional
609
+ Maximum number of voices in a chord, by default 8
610
+ sample_rate : int, optional
611
+ Sample rate of audio, by default 44100
612
+ num_items : int, optional
613
+ Number of items to generate, by default 5
614
+ duration : float, optional
615
+ Duration of each item, by default 1.0
616
+ min_note : str, optional
617
+ Minimum note in the dataset, by default "C2"
618
+ max_note : str, optional
619
+ Maximum note in the dataset, by default "C6"
620
+ output_dir : Path, optional
621
+ Directory to save the dataset, by default "chords"
622
+
623
+ """
624
+ import librosa
625
+ from . import AudioSignal
626
+ from ..data.preprocess import create_csv
627
+
628
+ min_midi = librosa.note_to_midi(min_note)
629
+ max_midi = librosa.note_to_midi(max_note)
630
+
631
+ tracks = []
632
+ for idx in range(num_items):
633
+ track = {}
634
+ # figure out how many voices to put in this track
635
+ num_voices = random.randint(1, max_voices)
636
+ for voice_idx in range(num_voices):
637
+ # choose some random params
638
+ midinote = random.randint(min_midi, max_midi)
639
+ dur = random.uniform(0.85 * duration, duration)
640
+
641
+ sig = AudioSignal.wave(
642
+ frequency=librosa.midi_to_hz(midinote),
643
+ duration=dur,
644
+ sample_rate=sample_rate,
645
+ shape="sine",
646
+ )
647
+ track[f"voice_{voice_idx}"] = sig
648
+ tracks.append(track)
649
+
650
+ # save the tracks to disk
651
+ output_dir = Path(output_dir)
652
+ output_dir.mkdir(exist_ok=True)
653
+ for idx, track in enumerate(tracks):
654
+ track_dir = output_dir / f"track_{idx}"
655
+ track_dir.mkdir(exist_ok=True)
656
+ for voice_name, sig in track.items():
657
+ sig.write(track_dir / f"{voice_name}.wav")
658
+
659
+ all_voices = list(set([k for track in tracks for k in track.keys()]))
660
+ voice_lists = {voice: [] for voice in all_voices}
661
+ for track in tracks:
662
+ for voice_name in all_voices:
663
+ if voice_name in track:
664
+ voice_lists[voice_name].append(track[voice_name].path_to_file)
665
+ else:
666
+ voice_lists[voice_name].append("")
667
+
668
+ for voice_name, paths in voice_lists.items():
669
+ create_csv(paths, output_dir / f"{voice_name}.csv", loudness=True)
670
+
671
+ return output_dir
audiotools/core/whisper.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+
4
+ class WhisperMixin:
5
+ is_initialized = False
6
+
7
+ def setup_whisper(
8
+ self,
9
+ pretrained_model_name_or_path: str = "openai/whisper-base.en",
10
+ device: str = torch.device("cuda" if torch.cuda.is_available() else "cpu"),
11
+ ):
12
+ from transformers import WhisperForConditionalGeneration
13
+ from transformers import WhisperProcessor
14
+
15
+ self.whisper_device = device
16
+ self.whisper_processor = WhisperProcessor.from_pretrained(
17
+ pretrained_model_name_or_path
18
+ )
19
+ self.whisper_model = WhisperForConditionalGeneration.from_pretrained(
20
+ pretrained_model_name_or_path
21
+ ).to(self.whisper_device)
22
+ self.is_initialized = True
23
+
24
+ def get_whisper_features(self) -> torch.Tensor:
25
+ """Preprocess audio signal as per the whisper model's training config.
26
+
27
+ Returns
28
+ -------
29
+ torch.Tensor
30
+ The prepinput features of the audio signal. Shape: (1, channels, seq_len)
31
+ """
32
+ import torch
33
+
34
+ if not self.is_initialized:
35
+ self.setup_whisper()
36
+
37
+ signal = self.to(self.device)
38
+ raw_speech = list(
39
+ (
40
+ signal.clone()
41
+ .resample(self.whisper_processor.feature_extractor.sampling_rate)
42
+ .audio_data[:, 0, :]
43
+ .numpy()
44
+ )
45
+ )
46
+
47
+ with torch.inference_mode():
48
+ input_features = self.whisper_processor(
49
+ raw_speech,
50
+ sampling_rate=self.whisper_processor.feature_extractor.sampling_rate,
51
+ return_tensors="pt",
52
+ ).input_features
53
+
54
+ return input_features
55
+
56
+ def get_whisper_transcript(self) -> str:
57
+ """Get the transcript of the audio signal using the whisper model.
58
+
59
+ Returns
60
+ -------
61
+ str
62
+ The transcript of the audio signal, including special tokens such as <|startoftranscript|> and <|endoftext|>.
63
+ """
64
+
65
+ if not self.is_initialized:
66
+ self.setup_whisper()
67
+
68
+ input_features = self.get_whisper_features()
69
+
70
+ with torch.inference_mode():
71
+ input_features = input_features.to(self.whisper_device)
72
+ generated_ids = self.whisper_model.generate(inputs=input_features)
73
+
74
+ transcription = self.whisper_processor.batch_decode(generated_ids)
75
+ return transcription[0]
76
+
77
+ def get_whisper_embeddings(self) -> torch.Tensor:
78
+ """Get the last hidden state embeddings of the audio signal using the whisper model.
79
+
80
+ Returns
81
+ -------
82
+ torch.Tensor
83
+ The Whisper embeddings of the audio signal. Shape: (1, seq_len, hidden_size)
84
+ """
85
+ import torch
86
+
87
+ if not self.is_initialized:
88
+ self.setup_whisper()
89
+
90
+ input_features = self.get_whisper_features()
91
+ encoder = self.whisper_model.get_encoder()
92
+
93
+ with torch.inference_mode():
94
+ input_features = input_features.to(self.whisper_device)
95
+ embeddings = encoder(input_features)
96
+
97
+ return embeddings.last_hidden_state
audiotools/data/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from . import datasets
2
+ from . import preprocess
3
+ from . import transforms
audiotools/data/datasets.py ADDED
@@ -0,0 +1,517 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ from typing import Callable
3
+ from typing import Dict
4
+ from typing import List
5
+ from typing import Union
6
+
7
+ import numpy as np
8
+ from torch.utils.data import SequentialSampler
9
+ from torch.utils.data.distributed import DistributedSampler
10
+
11
+ from ..core import AudioSignal
12
+ from ..core import util
13
+
14
+
15
+ class AudioLoader:
16
+ """Loads audio endlessly from a list of audio sources
17
+ containing paths to audio files. Audio sources can be
18
+ folders full of audio files (which are found via file
19
+ extension) or by providing a CSV file which contains paths
20
+ to audio files.
21
+
22
+ Parameters
23
+ ----------
24
+ sources : List[str], optional
25
+ Sources containing folders, or CSVs with
26
+ paths to audio files, by default None
27
+ weights : List[float], optional
28
+ Weights to sample audio files from each source, by default None
29
+ relative_path : str, optional
30
+ Path audio should be loaded relative to, by default ""
31
+ transform : Callable, optional
32
+ Transform to instantiate alongside audio sample,
33
+ by default None
34
+ ext : List[str]
35
+ List of extensions to find audio within each source by. Can
36
+ also be a file name (e.g. "vocals.wav"). by default
37
+ ``['.wav', '.flac', '.mp3', '.mp4']``.
38
+ shuffle: bool
39
+ Whether to shuffle the files within the dataloader. Defaults to True.
40
+ shuffle_state: int
41
+ State to use to seed the shuffle of the files.
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ sources: List[str] = None,
47
+ weights: List[float] = None,
48
+ transform: Callable = None,
49
+ relative_path: str = "",
50
+ ext: List[str] = util.AUDIO_EXTENSIONS,
51
+ shuffle: bool = True,
52
+ shuffle_state: int = 0,
53
+ ):
54
+ self.audio_lists = util.read_sources(
55
+ sources, relative_path=relative_path, ext=ext
56
+ )
57
+
58
+ self.audio_indices = [
59
+ (src_idx, item_idx)
60
+ for src_idx, src in enumerate(self.audio_lists)
61
+ for item_idx in range(len(src))
62
+ ]
63
+ if shuffle:
64
+ state = util.random_state(shuffle_state)
65
+ state.shuffle(self.audio_indices)
66
+
67
+ self.sources = sources
68
+ self.weights = weights
69
+ self.transform = transform
70
+
71
+ def __call__(
72
+ self,
73
+ state,
74
+ sample_rate: int,
75
+ duration: float,
76
+ loudness_cutoff: float = -40,
77
+ num_channels: int = 1,
78
+ offset: float = None,
79
+ source_idx: int = None,
80
+ item_idx: int = None,
81
+ global_idx: int = None,
82
+ ):
83
+ if source_idx is not None and item_idx is not None:
84
+ try:
85
+ audio_info = self.audio_lists[source_idx][item_idx]
86
+ except:
87
+ audio_info = {"path": "none"}
88
+ elif global_idx is not None:
89
+ source_idx, item_idx = self.audio_indices[
90
+ global_idx % len(self.audio_indices)
91
+ ]
92
+ audio_info = self.audio_lists[source_idx][item_idx]
93
+ else:
94
+ audio_info, source_idx, item_idx = util.choose_from_list_of_lists(
95
+ state, self.audio_lists, p=self.weights
96
+ )
97
+
98
+ path = audio_info["path"]
99
+ signal = AudioSignal.zeros(duration, sample_rate, num_channels)
100
+
101
+ if path != "none":
102
+ if offset is None:
103
+ signal = AudioSignal.salient_excerpt(
104
+ path,
105
+ duration=duration,
106
+ state=state,
107
+ loudness_cutoff=loudness_cutoff,
108
+ )
109
+ else:
110
+ signal = AudioSignal(
111
+ path,
112
+ offset=offset,
113
+ duration=duration,
114
+ )
115
+
116
+ if num_channels == 1:
117
+ signal = signal.to_mono()
118
+ signal = signal.resample(sample_rate)
119
+
120
+ if signal.duration < duration:
121
+ signal = signal.zero_pad_to(int(duration * sample_rate))
122
+
123
+ for k, v in audio_info.items():
124
+ signal.metadata[k] = v
125
+
126
+ item = {
127
+ "signal": signal,
128
+ "source_idx": source_idx,
129
+ "item_idx": item_idx,
130
+ "source": str(self.sources[source_idx]),
131
+ "path": str(path),
132
+ }
133
+ if self.transform is not None:
134
+ item["transform_args"] = self.transform.instantiate(state, signal=signal)
135
+ return item
136
+
137
+
138
+ def default_matcher(x, y):
139
+ return Path(x).parent == Path(y).parent
140
+
141
+
142
+ def align_lists(lists, matcher: Callable = default_matcher):
143
+ longest_list = lists[np.argmax([len(l) for l in lists])]
144
+ for i, x in enumerate(longest_list):
145
+ for l in lists:
146
+ if i >= len(l):
147
+ l.append({"path": "none"})
148
+ elif not matcher(l[i]["path"], x["path"]):
149
+ l.insert(i, {"path": "none"})
150
+ return lists
151
+
152
+
153
+ class AudioDataset:
154
+ """Loads audio from multiple loaders (with associated transforms)
155
+ for a specified number of samples. Excerpts are drawn randomly
156
+ of the specified duration, above a specified loudness threshold
157
+ and are resampled on the fly to the desired sample rate
158
+ (if it is different from the audio source sample rate).
159
+
160
+ This takes either a single AudioLoader object,
161
+ a dictionary of AudioLoader objects, or a dictionary of AudioLoader
162
+ objects. Each AudioLoader is called by the dataset, and the
163
+ result is placed in the output dictionary. A transform can also be
164
+ specified for the entire dataset, rather than for each specific
165
+ loader. This transform can be applied to the output of all the
166
+ loaders if desired.
167
+
168
+ AudioLoader objects can be specified as aligned, which means the
169
+ loaders correspond to multitrack audio (e.g. a vocals, bass,
170
+ drums, and other loader for multitrack music mixtures).
171
+
172
+
173
+ Parameters
174
+ ----------
175
+ loaders : Union[AudioLoader, List[AudioLoader], Dict[str, AudioLoader]]
176
+ AudioLoaders to sample audio from.
177
+ sample_rate : int
178
+ Desired sample rate.
179
+ n_examples : int, optional
180
+ Number of examples (length of dataset), by default 1000
181
+ duration : float, optional
182
+ Duration of audio samples, by default 0.5
183
+ loudness_cutoff : float, optional
184
+ Loudness cutoff threshold for audio samples, by default -40
185
+ num_channels : int, optional
186
+ Number of channels in output audio, by default 1
187
+ transform : Callable, optional
188
+ Transform to instantiate alongside each dataset item, by default None
189
+ aligned : bool, optional
190
+ Whether the loaders should be sampled in an aligned manner (e.g. same
191
+ offset, duration, and matched file name), by default False
192
+ shuffle_loaders : bool, optional
193
+ Whether to shuffle the loaders before sampling from them, by default False
194
+ matcher : Callable
195
+ How to match files from adjacent audio lists (e.g. for a multitrack audio loader),
196
+ by default uses the parent directory of each file.
197
+ without_replacement : bool
198
+ Whether to choose files with or without replacement, by default True.
199
+
200
+
201
+ Examples
202
+ --------
203
+ >>> from audiotools.data.datasets import AudioLoader
204
+ >>> from audiotools.data.datasets import AudioDataset
205
+ >>> from audiotools import transforms as tfm
206
+ >>> import numpy as np
207
+ >>>
208
+ >>> loaders = [
209
+ >>> AudioLoader(
210
+ >>> sources=[f"tests/audio/spk"],
211
+ >>> transform=tfm.Equalizer(),
212
+ >>> ext=["wav"],
213
+ >>> )
214
+ >>> for i in range(5)
215
+ >>> ]
216
+ >>>
217
+ >>> dataset = AudioDataset(
218
+ >>> loaders = loaders,
219
+ >>> sample_rate = 44100,
220
+ >>> duration = 1.0,
221
+ >>> transform = tfm.RescaleAudio(),
222
+ >>> )
223
+ >>>
224
+ >>> item = dataset[np.random.randint(len(dataset))]
225
+ >>>
226
+ >>> for i in range(len(loaders)):
227
+ >>> item[i]["signal"] = loaders[i].transform(
228
+ >>> item[i]["signal"], **item[i]["transform_args"]
229
+ >>> )
230
+ >>> item[i]["signal"].widget(i)
231
+ >>>
232
+ >>> mix = sum([item[i]["signal"] for i in range(len(loaders))])
233
+ >>> mix = dataset.transform(mix, **item["transform_args"])
234
+ >>> mix.widget("mix")
235
+
236
+ Below is an example of how one could load MUSDB multitrack data:
237
+
238
+ >>> import audiotools as at
239
+ >>> from pathlib import Path
240
+ >>> from audiotools import transforms as tfm
241
+ >>> import numpy as np
242
+ >>> import torch
243
+ >>>
244
+ >>> def build_dataset(
245
+ >>> sample_rate: int = 44100,
246
+ >>> duration: float = 5.0,
247
+ >>> musdb_path: str = "~/.data/musdb/",
248
+ >>> ):
249
+ >>> musdb_path = Path(musdb_path).expanduser()
250
+ >>> loaders = {
251
+ >>> src: at.datasets.AudioLoader(
252
+ >>> sources=[musdb_path],
253
+ >>> transform=tfm.Compose(
254
+ >>> tfm.VolumeNorm(("uniform", -20, -10)),
255
+ >>> tfm.Silence(prob=0.1),
256
+ >>> ),
257
+ >>> ext=[f"{src}.wav"],
258
+ >>> )
259
+ >>> for src in ["vocals", "bass", "drums", "other"]
260
+ >>> }
261
+ >>>
262
+ >>> dataset = at.datasets.AudioDataset(
263
+ >>> loaders=loaders,
264
+ >>> sample_rate=sample_rate,
265
+ >>> duration=duration,
266
+ >>> num_channels=1,
267
+ >>> aligned=True,
268
+ >>> transform=tfm.RescaleAudio(),
269
+ >>> shuffle_loaders=True,
270
+ >>> )
271
+ >>> return dataset, list(loaders.keys())
272
+ >>>
273
+ >>> train_data, sources = build_dataset()
274
+ >>> dataloader = torch.utils.data.DataLoader(
275
+ >>> train_data,
276
+ >>> batch_size=16,
277
+ >>> num_workers=0,
278
+ >>> collate_fn=train_data.collate,
279
+ >>> )
280
+ >>> batch = next(iter(dataloader))
281
+ >>>
282
+ >>> for k in sources:
283
+ >>> src = batch[k]
284
+ >>> src["transformed"] = train_data.loaders[k].transform(
285
+ >>> src["signal"].clone(), **src["transform_args"]
286
+ >>> )
287
+ >>>
288
+ >>> mixture = sum(batch[k]["transformed"] for k in sources)
289
+ >>> mixture = train_data.transform(mixture, **batch["transform_args"])
290
+ >>>
291
+ >>> # Say a model takes the mix and gives back (n_batch, n_src, n_time).
292
+ >>> # Construct the targets:
293
+ >>> targets = at.AudioSignal.batch([batch[k]["transformed"] for k in sources], dim=1)
294
+
295
+ Similarly, here's example code for loading Slakh data:
296
+
297
+ >>> import audiotools as at
298
+ >>> from pathlib import Path
299
+ >>> from audiotools import transforms as tfm
300
+ >>> import numpy as np
301
+ >>> import torch
302
+ >>> import glob
303
+ >>>
304
+ >>> def build_dataset(
305
+ >>> sample_rate: int = 16000,
306
+ >>> duration: float = 10.0,
307
+ >>> slakh_path: str = "~/.data/slakh/",
308
+ >>> ):
309
+ >>> slakh_path = Path(slakh_path).expanduser()
310
+ >>>
311
+ >>> # Find the max number of sources in Slakh
312
+ >>> src_names = [x.name for x in list(slakh_path.glob("**/*.wav")) if "S" in str(x.name)]
313
+ >>> n_sources = len(list(set(src_names)))
314
+ >>>
315
+ >>> loaders = {
316
+ >>> f"S{i:02d}": at.datasets.AudioLoader(
317
+ >>> sources=[slakh_path],
318
+ >>> transform=tfm.Compose(
319
+ >>> tfm.VolumeNorm(("uniform", -20, -10)),
320
+ >>> tfm.Silence(prob=0.1),
321
+ >>> ),
322
+ >>> ext=[f"S{i:02d}.wav"],
323
+ >>> )
324
+ >>> for i in range(n_sources)
325
+ >>> }
326
+ >>> dataset = at.datasets.AudioDataset(
327
+ >>> loaders=loaders,
328
+ >>> sample_rate=sample_rate,
329
+ >>> duration=duration,
330
+ >>> num_channels=1,
331
+ >>> aligned=True,
332
+ >>> transform=tfm.RescaleAudio(),
333
+ >>> shuffle_loaders=False,
334
+ >>> )
335
+ >>>
336
+ >>> return dataset, list(loaders.keys())
337
+ >>>
338
+ >>> train_data, sources = build_dataset()
339
+ >>> dataloader = torch.utils.data.DataLoader(
340
+ >>> train_data,
341
+ >>> batch_size=16,
342
+ >>> num_workers=0,
343
+ >>> collate_fn=train_data.collate,
344
+ >>> )
345
+ >>> batch = next(iter(dataloader))
346
+ >>>
347
+ >>> for k in sources:
348
+ >>> src = batch[k]
349
+ >>> src["transformed"] = train_data.loaders[k].transform(
350
+ >>> src["signal"].clone(), **src["transform_args"]
351
+ >>> )
352
+ >>>
353
+ >>> mixture = sum(batch[k]["transformed"] for k in sources)
354
+ >>> mixture = train_data.transform(mixture, **batch["transform_args"])
355
+
356
+ """
357
+
358
+ def __init__(
359
+ self,
360
+ loaders: Union[AudioLoader, List[AudioLoader], Dict[str, AudioLoader]],
361
+ sample_rate: int,
362
+ n_examples: int = 1000,
363
+ duration: float = 0.5,
364
+ offset: float = None,
365
+ loudness_cutoff: float = -40,
366
+ num_channels: int = 1,
367
+ transform: Callable = None,
368
+ aligned: bool = False,
369
+ shuffle_loaders: bool = False,
370
+ matcher: Callable = default_matcher,
371
+ without_replacement: bool = True,
372
+ ):
373
+ # Internally we convert loaders to a dictionary
374
+ if isinstance(loaders, list):
375
+ loaders = {i: l for i, l in enumerate(loaders)}
376
+ elif isinstance(loaders, AudioLoader):
377
+ loaders = {0: loaders}
378
+
379
+ self.loaders = loaders
380
+ self.loudness_cutoff = loudness_cutoff
381
+ self.num_channels = num_channels
382
+
383
+ self.length = n_examples
384
+ self.transform = transform
385
+ self.sample_rate = sample_rate
386
+ self.duration = duration
387
+ self.offset = offset
388
+ self.aligned = aligned
389
+ self.shuffle_loaders = shuffle_loaders
390
+ self.without_replacement = without_replacement
391
+
392
+ if aligned:
393
+ loaders_list = list(loaders.values())
394
+ for i in range(len(loaders_list[0].audio_lists)):
395
+ input_lists = [l.audio_lists[i] for l in loaders_list]
396
+ # Alignment happens in-place
397
+ align_lists(input_lists, matcher)
398
+
399
+ def __getitem__(self, idx):
400
+ state = util.random_state(idx)
401
+ offset = None if self.offset is None else self.offset
402
+ item = {}
403
+
404
+ keys = list(self.loaders.keys())
405
+ if self.shuffle_loaders:
406
+ state.shuffle(keys)
407
+
408
+ loader_kwargs = {
409
+ "state": state,
410
+ "sample_rate": self.sample_rate,
411
+ "duration": self.duration,
412
+ "loudness_cutoff": self.loudness_cutoff,
413
+ "num_channels": self.num_channels,
414
+ "global_idx": idx if self.without_replacement else None,
415
+ }
416
+
417
+ # Draw item from first loader
418
+ loader = self.loaders[keys[0]]
419
+ item[keys[0]] = loader(**loader_kwargs)
420
+
421
+ for key in keys[1:]:
422
+ loader = self.loaders[key]
423
+ if self.aligned:
424
+ # Path mapper takes the current loader + everything
425
+ # returned by the first loader.
426
+ offset = item[keys[0]]["signal"].metadata["offset"]
427
+ loader_kwargs.update(
428
+ {
429
+ "offset": offset,
430
+ "source_idx": item[keys[0]]["source_idx"],
431
+ "item_idx": item[keys[0]]["item_idx"],
432
+ }
433
+ )
434
+ item[key] = loader(**loader_kwargs)
435
+
436
+ # Sort dictionary back into original order
437
+ keys = list(self.loaders.keys())
438
+ item = {k: item[k] for k in keys}
439
+
440
+ item["idx"] = idx
441
+ if self.transform is not None:
442
+ item["transform_args"] = self.transform.instantiate(
443
+ state=state, signal=item[keys[0]]["signal"]
444
+ )
445
+
446
+ # If there's only one loader, pop it up
447
+ # to the main dictionary, instead of keeping it
448
+ # nested.
449
+ if len(keys) == 1:
450
+ item.update(item.pop(keys[0]))
451
+
452
+ return item
453
+
454
+ def __len__(self):
455
+ return self.length
456
+
457
+ @staticmethod
458
+ def collate(list_of_dicts: Union[list, dict], n_splits: int = None):
459
+ """Collates items drawn from this dataset. Uses
460
+ :py:func:`audiotools.core.util.collate`.
461
+
462
+ Parameters
463
+ ----------
464
+ list_of_dicts : typing.Union[list, dict]
465
+ Data drawn from each item.
466
+ n_splits : int
467
+ Number of splits to make when creating the batches (split into
468
+ sub-batches). Useful for things like gradient accumulation.
469
+
470
+ Returns
471
+ -------
472
+ dict
473
+ Dictionary of batched data.
474
+ """
475
+ return util.collate(list_of_dicts, n_splits=n_splits)
476
+
477
+
478
+ class ConcatDataset(AudioDataset):
479
+ def __init__(self, datasets: list):
480
+ self.datasets = datasets
481
+
482
+ def __len__(self):
483
+ return sum([len(d) for d in self.datasets])
484
+
485
+ def __getitem__(self, idx):
486
+ dataset = self.datasets[idx % len(self.datasets)]
487
+ return dataset[idx // len(self.datasets)]
488
+
489
+
490
+ class ResumableDistributedSampler(DistributedSampler): # pragma: no cover
491
+ """Distributed sampler that can be resumed from a given start index."""
492
+
493
+ def __init__(self, dataset, start_idx: int = None, **kwargs):
494
+ super().__init__(dataset, **kwargs)
495
+ # Start index, allows to resume an experiment at the index it was
496
+ self.start_idx = start_idx // self.num_replicas if start_idx is not None else 0
497
+
498
+ def __iter__(self):
499
+ for i, idx in enumerate(super().__iter__()):
500
+ if i >= self.start_idx:
501
+ yield idx
502
+ self.start_idx = 0 # set the index back to 0 so for the next epoch
503
+
504
+
505
+ class ResumableSequentialSampler(SequentialSampler): # pragma: no cover
506
+ """Sequential sampler that can be resumed from a given start index."""
507
+
508
+ def __init__(self, dataset, start_idx: int = None, **kwargs):
509
+ super().__init__(dataset, **kwargs)
510
+ # Start index, allows to resume an experiment at the index it was
511
+ self.start_idx = start_idx if start_idx is not None else 0
512
+
513
+ def __iter__(self):
514
+ for i, idx in enumerate(super().__iter__()):
515
+ if i >= self.start_idx:
516
+ yield idx
517
+ self.start_idx = 0 # set the index back to 0 so for the next epoch
audiotools/data/preprocess.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import csv
2
+ import os
3
+ from pathlib import Path
4
+
5
+ from tqdm import tqdm
6
+
7
+ from ..core import AudioSignal
8
+
9
+
10
+ def create_csv(
11
+ audio_files: list, output_csv: Path, loudness: bool = False, data_path: str = None
12
+ ):
13
+ """Converts a folder of audio files to a CSV file. If ``loudness = True``,
14
+ the output of this function will create a CSV file that looks something
15
+ like:
16
+
17
+ .. csv-table::
18
+ :header: path,loudness
19
+
20
+ daps/produced/f1_script1_produced.wav,-16.299999237060547
21
+ daps/produced/f1_script2_produced.wav,-16.600000381469727
22
+ daps/produced/f1_script3_produced.wav,-17.299999237060547
23
+ daps/produced/f1_script4_produced.wav,-16.100000381469727
24
+ daps/produced/f1_script5_produced.wav,-16.700000762939453
25
+ daps/produced/f3_script1_produced.wav,-16.5
26
+
27
+ .. note::
28
+ The paths above are written relative to the ``data_path`` argument
29
+ which defaults to the environment variable ``PATH_TO_DATA`` if
30
+ it isn't passed to this function, and defaults to the empty string
31
+ if that environment variable is not set.
32
+
33
+ You can produce a CSV file from a directory of audio files via:
34
+
35
+ >>> import audiotools
36
+ >>> directory = ...
37
+ >>> audio_files = audiotools.util.find_audio(directory)
38
+ >>> output_path = "train.csv"
39
+ >>> audiotools.data.preprocess.create_csv(
40
+ >>> audio_files, output_csv, loudness=True
41
+ >>> )
42
+
43
+ Note that you can create empty rows in the CSV file by passing an empty
44
+ string or None in the ``audio_files`` list. This is useful if you want to
45
+ sync multiple CSV files in a multitrack setting. The loudness of these
46
+ empty rows will be set to -inf.
47
+
48
+ Parameters
49
+ ----------
50
+ audio_files : list
51
+ List of audio files.
52
+ output_csv : Path
53
+ Output CSV, with each row containing the relative path of every file
54
+ to ``data_path``, if specified (defaults to None).
55
+ loudness : bool
56
+ Compute loudness of entire file and store alongside path.
57
+ """
58
+
59
+ info = []
60
+ pbar = tqdm(audio_files)
61
+ for af in pbar:
62
+ af = Path(af)
63
+ pbar.set_description(f"Processing {af.name}")
64
+ _info = {}
65
+ if af.name == "":
66
+ _info["path"] = ""
67
+ if loudness:
68
+ _info["loudness"] = -float("inf")
69
+ else:
70
+ _info["path"] = af.relative_to(data_path) if data_path is not None else af
71
+ if loudness:
72
+ _info["loudness"] = AudioSignal(af).ffmpeg_loudness().item()
73
+
74
+ info.append(_info)
75
+
76
+ with open(output_csv, "w") as f:
77
+ writer = csv.DictWriter(f, fieldnames=list(info[0].keys()))
78
+ writer.writeheader()
79
+
80
+ for item in info:
81
+ writer.writerow(item)
audiotools/data/transforms.py ADDED
@@ -0,0 +1,1592 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ from contextlib import contextmanager
3
+ from inspect import signature
4
+ from typing import List
5
+
6
+ import numpy as np
7
+ import torch
8
+ from flatten_dict import flatten
9
+ from flatten_dict import unflatten
10
+ from numpy.random import RandomState
11
+
12
+ from .. import ml
13
+ from ..core import AudioSignal
14
+ from ..core import util
15
+ from .datasets import AudioLoader
16
+
17
+ tt = torch.tensor
18
+ """Shorthand for converting things to torch.tensor."""
19
+
20
+
21
+ class BaseTransform:
22
+ """This is the base class for all transforms that are implemented
23
+ in this library. Transforms have two main operations: ``transform``
24
+ and ``instantiate``.
25
+
26
+ ``instantiate`` sets the parameters randomly
27
+ from distribution tuples for each parameter. For example, for the
28
+ ``BackgroundNoise`` transform, the signal-to-noise ratio (``snr``)
29
+ is chosen randomly by instantiate. By default, it chosen uniformly
30
+ between 10.0 and 30.0 (the tuple is set to ``("uniform", 10.0, 30.0)``).
31
+
32
+ ``transform`` applies the transform using the instantiated parameters.
33
+ A simple example is as follows:
34
+
35
+ >>> seed = 0
36
+ >>> signal = ...
37
+ >>> transform = transforms.NoiseFloor(db = ("uniform", -50.0, -30.0))
38
+ >>> kwargs = transform.instantiate()
39
+ >>> output = transform(signal.clone(), **kwargs)
40
+
41
+ By breaking apart the instantiation of parameters from the actual audio
42
+ processing of the transform, we can make things more reproducible, while
43
+ also applying the transform on batches of data efficiently on GPU,
44
+ rather than on individual audio samples.
45
+
46
+ .. note::
47
+ We call ``signal.clone()`` for the input to the ``transform`` function
48
+ because signals are modified in-place! If you don't clone the signal,
49
+ you will lose the original data.
50
+
51
+ Parameters
52
+ ----------
53
+ keys : list, optional
54
+ Keys that the transform looks for when
55
+ calling ``self.transform``, by default []. In general this is
56
+ set automatically, and you won't need to manipulate this argument.
57
+ name : str, optional
58
+ Name of this transform, used to identify it in the dictionary
59
+ produced by ``self.instantiate``, by default None
60
+ prob : float, optional
61
+ Probability of applying this transform, by default 1.0
62
+
63
+ Examples
64
+ --------
65
+
66
+ >>> seed = 0
67
+ >>>
68
+ >>> audio_path = "tests/audio/spk/f10_script4_produced.wav"
69
+ >>> signal = AudioSignal(audio_path, offset=10, duration=2)
70
+ >>> transform = tfm.Compose(
71
+ >>> [
72
+ >>> tfm.RoomImpulseResponse(sources=["tests/audio/irs.csv"]),
73
+ >>> tfm.BackgroundNoise(sources=["tests/audio/noises.csv"]),
74
+ >>> ],
75
+ >>> )
76
+ >>>
77
+ >>> kwargs = transform.instantiate(seed, signal)
78
+ >>> output = transform(signal, **kwargs)
79
+
80
+ """
81
+
82
+ def __init__(self, keys: list = [], name: str = None, prob: float = 1.0):
83
+ # Get keys from the _transform signature.
84
+ tfm_keys = list(signature(self._transform).parameters.keys())
85
+
86
+ # Filter out signal and kwargs keys.
87
+ ignore_keys = ["signal", "kwargs"]
88
+ tfm_keys = [k for k in tfm_keys if k not in ignore_keys]
89
+
90
+ # Combine keys specified by the child class, the keys found in
91
+ # _transform signature, and the mask key.
92
+ self.keys = keys + tfm_keys + ["mask"]
93
+
94
+ self.prob = prob
95
+
96
+ if name is None:
97
+ name = self.__class__.__name__
98
+ self.name = name
99
+
100
+ def _prepare(self, batch: dict):
101
+ sub_batch = batch[self.name]
102
+
103
+ for k in self.keys:
104
+ assert k in sub_batch.keys(), f"{k} not in batch"
105
+
106
+ return sub_batch
107
+
108
+ def _transform(self, signal):
109
+ return signal
110
+
111
+ def _instantiate(self, state: RandomState, signal: AudioSignal = None):
112
+ return {}
113
+
114
+ @staticmethod
115
+ def apply_mask(batch: dict, mask: torch.Tensor):
116
+ """Applies a mask to the batch.
117
+
118
+ Parameters
119
+ ----------
120
+ batch : dict
121
+ Batch whose values will be masked in the ``transform`` pass.
122
+ mask : torch.Tensor
123
+ Mask to apply to batch.
124
+
125
+ Returns
126
+ -------
127
+ dict
128
+ A dictionary that contains values only where ``mask = True``.
129
+ """
130
+ masked_batch = {k: v[mask] for k, v in flatten(batch).items()}
131
+ return unflatten(masked_batch)
132
+
133
+ def transform(self, signal: AudioSignal, **kwargs):
134
+ """Apply the transform to the audio signal,
135
+ with given keyword arguments.
136
+
137
+ Parameters
138
+ ----------
139
+ signal : AudioSignal
140
+ Signal that will be modified by the transforms in-place.
141
+ kwargs: dict
142
+ Keyword arguments to the specific transforms ``self._transform``
143
+ function.
144
+
145
+ Returns
146
+ -------
147
+ AudioSignal
148
+ Transformed AudioSignal.
149
+
150
+ Examples
151
+ --------
152
+
153
+ >>> for seed in range(10):
154
+ >>> kwargs = transform.instantiate(seed, signal)
155
+ >>> output = transform(signal.clone(), **kwargs)
156
+
157
+ """
158
+ tfm_kwargs = self._prepare(kwargs)
159
+ mask = tfm_kwargs["mask"]
160
+
161
+ if torch.any(mask):
162
+ tfm_kwargs = self.apply_mask(tfm_kwargs, mask)
163
+ tfm_kwargs = {k: v for k, v in tfm_kwargs.items() if k != "mask"}
164
+ signal[mask] = self._transform(signal[mask], **tfm_kwargs)
165
+
166
+ return signal
167
+
168
+ def __call__(self, *args, **kwargs):
169
+ return self.transform(*args, **kwargs)
170
+
171
+ def instantiate(
172
+ self,
173
+ state: RandomState = None,
174
+ signal: AudioSignal = None,
175
+ ):
176
+ """Instantiates parameters for the transform.
177
+
178
+ Parameters
179
+ ----------
180
+ state : RandomState, optional
181
+ _description_, by default None
182
+ signal : AudioSignal, optional
183
+ _description_, by default None
184
+
185
+ Returns
186
+ -------
187
+ dict
188
+ Dictionary containing instantiated arguments for every keyword
189
+ argument to ``self._transform``.
190
+
191
+ Examples
192
+ --------
193
+
194
+ >>> for seed in range(10):
195
+ >>> kwargs = transform.instantiate(seed, signal)
196
+ >>> output = transform(signal.clone(), **kwargs)
197
+
198
+ """
199
+ state = util.random_state(state)
200
+
201
+ # Not all instantiates need the signal. Check if signal
202
+ # is needed before passing it in, so that the end-user
203
+ # doesn't need to have variables they're not using flowing
204
+ # into their function.
205
+ needs_signal = "signal" in set(signature(self._instantiate).parameters.keys())
206
+ kwargs = {}
207
+ if needs_signal:
208
+ kwargs = {"signal": signal}
209
+
210
+ # Instantiate the parameters for the transform.
211
+ params = self._instantiate(state, **kwargs)
212
+ for k in list(params.keys()):
213
+ v = params[k]
214
+ if isinstance(v, (AudioSignal, torch.Tensor, dict)):
215
+ params[k] = v
216
+ else:
217
+ params[k] = tt(v)
218
+ mask = state.rand() <= self.prob
219
+ params[f"mask"] = tt(mask)
220
+
221
+ # Put the params into a nested dictionary that will be
222
+ # used later when calling the transform. This is to avoid
223
+ # collisions in the dictionary.
224
+ params = {self.name: params}
225
+
226
+ return params
227
+
228
+ def batch_instantiate(
229
+ self,
230
+ states: list = None,
231
+ signal: AudioSignal = None,
232
+ ):
233
+ """Instantiates arguments for every item in a batch,
234
+ given a list of states. Each state in the list
235
+ corresponds to one item in the batch.
236
+
237
+ Parameters
238
+ ----------
239
+ states : list, optional
240
+ List of states, by default None
241
+ signal : AudioSignal, optional
242
+ AudioSignal to pass to the ``self.instantiate`` section
243
+ if it is needed for this transform, by default None
244
+
245
+ Returns
246
+ -------
247
+ dict
248
+ Collated dictionary of arguments.
249
+
250
+ Examples
251
+ --------
252
+
253
+ >>> batch_size = 4
254
+ >>> signal = AudioSignal(audio_path, offset=10, duration=2)
255
+ >>> signal_batch = AudioSignal.batch([signal.clone() for _ in range(batch_size)])
256
+ >>>
257
+ >>> states = [seed + idx for idx in list(range(batch_size))]
258
+ >>> kwargs = transform.batch_instantiate(states, signal_batch)
259
+ >>> batch_output = transform(signal_batch, **kwargs)
260
+ """
261
+ kwargs = []
262
+ for state in states:
263
+ kwargs.append(self.instantiate(state, signal))
264
+ kwargs = util.collate(kwargs)
265
+ return kwargs
266
+
267
+
268
+ class Identity(BaseTransform):
269
+ """This transform just returns the original signal."""
270
+
271
+ pass
272
+
273
+
274
+ class SpectralTransform(BaseTransform):
275
+ """Spectral transforms require STFT data to exist, since manipulations
276
+ of the STFT require the spectrogram. This just calls ``stft`` before
277
+ the transform is called, and calls ``istft`` after the transform is
278
+ called so that the audio data is written to after the spectral
279
+ manipulation.
280
+ """
281
+
282
+ def transform(self, signal, **kwargs):
283
+ signal.stft()
284
+ super().transform(signal, **kwargs)
285
+ signal.istft()
286
+ return signal
287
+
288
+
289
+ class Compose(BaseTransform):
290
+ """Compose applies transforms in sequence, one after the other. The
291
+ transforms are passed in as positional arguments or as a list like so:
292
+
293
+ >>> transform = tfm.Compose(
294
+ >>> [
295
+ >>> tfm.RoomImpulseResponse(sources=["tests/audio/irs.csv"]),
296
+ >>> tfm.BackgroundNoise(sources=["tests/audio/noises.csv"]),
297
+ >>> ],
298
+ >>> )
299
+
300
+ This will convolve the signal with a room impulse response, and then
301
+ add background noise to the signal. Instantiate instantiates
302
+ all the parameters for every transform in the transform list so the
303
+ interface for using the Compose transform is the same as everything
304
+ else:
305
+
306
+ >>> kwargs = transform.instantiate()
307
+ >>> output = transform(signal.clone(), **kwargs)
308
+
309
+ Under the hood, the transform maps each transform to a unique name
310
+ under the hood of the form ``{position}.{name}``, where ``position``
311
+ is the index of the transform in the list. ``Compose`` can nest
312
+ within other ``Compose`` transforms, like so:
313
+
314
+ >>> preprocess = transforms.Compose(
315
+ >>> tfm.GlobalVolumeNorm(),
316
+ >>> tfm.CrossTalk(),
317
+ >>> name="preprocess",
318
+ >>> )
319
+ >>> augment = transforms.Compose(
320
+ >>> tfm.RoomImpulseResponse(),
321
+ >>> tfm.BackgroundNoise(),
322
+ >>> name="augment",
323
+ >>> )
324
+ >>> postprocess = transforms.Compose(
325
+ >>> tfm.VolumeChange(),
326
+ >>> tfm.RescaleAudio(),
327
+ >>> tfm.ShiftPhase(),
328
+ >>> name="postprocess",
329
+ >>> )
330
+ >>> transform = transforms.Compose(preprocess, augment, postprocess),
331
+
332
+ This defines 3 composed transforms, and then composes them in sequence
333
+ with one another.
334
+
335
+ Parameters
336
+ ----------
337
+ *transforms : list
338
+ List of transforms to apply
339
+ name : str, optional
340
+ Name of this transform, used to identify it in the dictionary
341
+ produced by ``self.instantiate``, by default None
342
+ prob : float, optional
343
+ Probability of applying this transform, by default 1.0
344
+ """
345
+
346
+ def __init__(self, *transforms: list, name: str = None, prob: float = 1.0):
347
+ if isinstance(transforms[0], list):
348
+ transforms = transforms[0]
349
+
350
+ for i, tfm in enumerate(transforms):
351
+ tfm.name = f"{i}.{tfm.name}"
352
+
353
+ keys = [tfm.name for tfm in transforms]
354
+ super().__init__(keys=keys, name=name, prob=prob)
355
+
356
+ self.transforms = transforms
357
+ self.transforms_to_apply = keys
358
+
359
+ @contextmanager
360
+ def filter(self, *names: list):
361
+ """This can be used to skip transforms entirely when applying
362
+ the sequence of transforms to a signal. For example, take
363
+ the following transforms with the names ``preprocess, augment, postprocess``.
364
+
365
+ >>> preprocess = transforms.Compose(
366
+ >>> tfm.GlobalVolumeNorm(),
367
+ >>> tfm.CrossTalk(),
368
+ >>> name="preprocess",
369
+ >>> )
370
+ >>> augment = transforms.Compose(
371
+ >>> tfm.RoomImpulseResponse(),
372
+ >>> tfm.BackgroundNoise(),
373
+ >>> name="augment",
374
+ >>> )
375
+ >>> postprocess = transforms.Compose(
376
+ >>> tfm.VolumeChange(),
377
+ >>> tfm.RescaleAudio(),
378
+ >>> tfm.ShiftPhase(),
379
+ >>> name="postprocess",
380
+ >>> )
381
+ >>> transform = transforms.Compose(preprocess, augment, postprocess)
382
+
383
+ If we wanted to apply all 3 to a signal, we do:
384
+
385
+ >>> kwargs = transform.instantiate()
386
+ >>> output = transform(signal.clone(), **kwargs)
387
+
388
+ But if we only wanted to apply the ``preprocess`` and ``postprocess``
389
+ transforms to the signal, we do:
390
+
391
+ >>> with transform_fn.filter("preprocess", "postprocess"):
392
+ >>> output = transform(signal.clone(), **kwargs)
393
+
394
+ Parameters
395
+ ----------
396
+ *names : list
397
+ List of transforms, identified by name, to apply to signal.
398
+ """
399
+ old_transforms = self.transforms_to_apply
400
+ self.transforms_to_apply = names
401
+ yield
402
+ self.transforms_to_apply = old_transforms
403
+
404
+ def _transform(self, signal, **kwargs):
405
+ for transform in self.transforms:
406
+ if any([x in transform.name for x in self.transforms_to_apply]):
407
+ signal = transform(signal, **kwargs)
408
+ return signal
409
+
410
+ def _instantiate(self, state: RandomState, signal: AudioSignal = None):
411
+ parameters = {}
412
+ for transform in self.transforms:
413
+ parameters.update(transform.instantiate(state, signal=signal))
414
+ return parameters
415
+
416
+ def __getitem__(self, idx):
417
+ return self.transforms[idx]
418
+
419
+ def __len__(self):
420
+ return len(self.transforms)
421
+
422
+ def __iter__(self):
423
+ for transform in self.transforms:
424
+ yield transform
425
+
426
+
427
+ class Choose(Compose):
428
+ """Choose logic is the same as :py:func:`audiotools.data.transforms.Compose`,
429
+ but instead of applying all the transforms in sequence, it applies just a single transform,
430
+ which is chosen for each item in the batch.
431
+
432
+ Parameters
433
+ ----------
434
+ *transforms : list
435
+ List of transforms to apply
436
+ weights : list
437
+ Probability of choosing any specific transform.
438
+ name : str, optional
439
+ Name of this transform, used to identify it in the dictionary
440
+ produced by ``self.instantiate``, by default None
441
+ prob : float, optional
442
+ Probability of applying this transform, by default 1.0
443
+
444
+ Examples
445
+ --------
446
+
447
+ >>> transforms.Choose(tfm.LowPass(), tfm.HighPass())
448
+ """
449
+
450
+ def __init__(
451
+ self,
452
+ *transforms: list,
453
+ weights: list = None,
454
+ name: str = None,
455
+ prob: float = 1.0,
456
+ ):
457
+ super().__init__(*transforms, name=name, prob=prob)
458
+
459
+ if weights is None:
460
+ _len = len(self.transforms)
461
+ weights = [1 / _len for _ in range(_len)]
462
+ self.weights = np.array(weights)
463
+
464
+ def _instantiate(self, state: RandomState, signal: AudioSignal = None):
465
+ kwargs = super()._instantiate(state, signal)
466
+ tfm_idx = list(range(len(self.transforms)))
467
+ tfm_idx = state.choice(tfm_idx, p=self.weights)
468
+ one_hot = []
469
+ for i, t in enumerate(self.transforms):
470
+ mask = kwargs[t.name]["mask"]
471
+ if mask.item():
472
+ kwargs[t.name]["mask"] = tt(i == tfm_idx)
473
+ one_hot.append(kwargs[t.name]["mask"])
474
+ kwargs["one_hot"] = one_hot
475
+ return kwargs
476
+
477
+
478
+ class Repeat(Compose):
479
+ """Repeatedly applies a given transform ``n_repeat`` times."
480
+
481
+ Parameters
482
+ ----------
483
+ transform : BaseTransform
484
+ Transform to repeat.
485
+ n_repeat : int, optional
486
+ Number of times to repeat transform, by default 1
487
+ """
488
+
489
+ def __init__(
490
+ self,
491
+ transform,
492
+ n_repeat: int = 1,
493
+ name: str = None,
494
+ prob: float = 1.0,
495
+ ):
496
+ transforms = [copy.copy(transform) for _ in range(n_repeat)]
497
+ super().__init__(transforms, name=name, prob=prob)
498
+
499
+ self.n_repeat = n_repeat
500
+
501
+
502
+ class RepeatUpTo(Choose):
503
+ """Repeatedly applies a given transform up to ``max_repeat`` times."
504
+
505
+ Parameters
506
+ ----------
507
+ transform : BaseTransform
508
+ Transform to repeat.
509
+ max_repeat : int, optional
510
+ Max number of times to repeat transform, by default 1
511
+ weights : list
512
+ Probability of choosing any specific number up to ``max_repeat``.
513
+ """
514
+
515
+ def __init__(
516
+ self,
517
+ transform,
518
+ max_repeat: int = 5,
519
+ weights: list = None,
520
+ name: str = None,
521
+ prob: float = 1.0,
522
+ ):
523
+ transforms = []
524
+ for n in range(1, max_repeat):
525
+ transforms.append(Repeat(transform, n_repeat=n))
526
+ super().__init__(transforms, name=name, prob=prob, weights=weights)
527
+
528
+ self.max_repeat = max_repeat
529
+
530
+
531
+ class ClippingDistortion(BaseTransform):
532
+ """Adds clipping distortion to signal. Corresponds
533
+ to :py:func:`audiotools.core.effects.EffectMixin.clip_distortion`.
534
+
535
+ Parameters
536
+ ----------
537
+ perc : tuple, optional
538
+ Clipping percentile. Values are between 0.0 to 1.0.
539
+ Typical values are 0.1 or below, by default ("uniform", 0.0, 0.1)
540
+ name : str, optional
541
+ Name of this transform, used to identify it in the dictionary
542
+ produced by ``self.instantiate``, by default None
543
+ prob : float, optional
544
+ Probability of applying this transform, by default 1.0
545
+ """
546
+
547
+ def __init__(
548
+ self,
549
+ perc: tuple = ("uniform", 0.0, 0.1),
550
+ name: str = None,
551
+ prob: float = 1.0,
552
+ ):
553
+ super().__init__(name=name, prob=prob)
554
+
555
+ self.perc = perc
556
+
557
+ def _instantiate(self, state: RandomState):
558
+ return {"perc": util.sample_from_dist(self.perc, state)}
559
+
560
+ def _transform(self, signal, perc):
561
+ return signal.clip_distortion(perc)
562
+
563
+
564
+ class Equalizer(BaseTransform):
565
+ """Applies an equalization curve to the audio signal. Corresponds
566
+ to :py:func:`audiotools.core.effects.EffectMixin.equalizer`.
567
+
568
+ Parameters
569
+ ----------
570
+ eq_amount : tuple, optional
571
+ The maximum dB cut to apply to the audio in any band,
572
+ by default ("const", 1.0 dB)
573
+ n_bands : int, optional
574
+ Number of bands in EQ, by default 6
575
+ name : str, optional
576
+ Name of this transform, used to identify it in the dictionary
577
+ produced by ``self.instantiate``, by default None
578
+ prob : float, optional
579
+ Probability of applying this transform, by default 1.0
580
+ """
581
+
582
+ def __init__(
583
+ self,
584
+ eq_amount: tuple = ("const", 1.0),
585
+ n_bands: int = 6,
586
+ name: str = None,
587
+ prob: float = 1.0,
588
+ ):
589
+ super().__init__(name=name, prob=prob)
590
+
591
+ self.eq_amount = eq_amount
592
+ self.n_bands = n_bands
593
+
594
+ def _instantiate(self, state: RandomState):
595
+ eq_amount = util.sample_from_dist(self.eq_amount, state)
596
+ eq = -eq_amount * state.rand(self.n_bands)
597
+ return {"eq": eq}
598
+
599
+ def _transform(self, signal, eq):
600
+ return signal.equalizer(eq)
601
+
602
+
603
+ class Quantization(BaseTransform):
604
+ """Applies quantization to the input waveform. Corresponds
605
+ to :py:func:`audiotools.core.effects.EffectMixin.quantization`.
606
+
607
+ Parameters
608
+ ----------
609
+ channels : tuple, optional
610
+ Number of evenly spaced quantization channels to quantize
611
+ to, by default ("choice", [8, 32, 128, 256, 1024])
612
+ name : str, optional
613
+ Name of this transform, used to identify it in the dictionary
614
+ produced by ``self.instantiate``, by default None
615
+ prob : float, optional
616
+ Probability of applying this transform, by default 1.0
617
+ """
618
+
619
+ def __init__(
620
+ self,
621
+ channels: tuple = ("choice", [8, 32, 128, 256, 1024]),
622
+ name: str = None,
623
+ prob: float = 1.0,
624
+ ):
625
+ super().__init__(name=name, prob=prob)
626
+
627
+ self.channels = channels
628
+
629
+ def _instantiate(self, state: RandomState):
630
+ return {"channels": util.sample_from_dist(self.channels, state)}
631
+
632
+ def _transform(self, signal, channels):
633
+ return signal.quantization(channels)
634
+
635
+
636
+ class MuLawQuantization(BaseTransform):
637
+ """Applies mu-law quantization to the input waveform. Corresponds
638
+ to :py:func:`audiotools.core.effects.EffectMixin.mulaw_quantization`.
639
+
640
+ Parameters
641
+ ----------
642
+ channels : tuple, optional
643
+ Number of mu-law spaced quantization channels to quantize
644
+ to, by default ("choice", [8, 32, 128, 256, 1024])
645
+ name : str, optional
646
+ Name of this transform, used to identify it in the dictionary
647
+ produced by ``self.instantiate``, by default None
648
+ prob : float, optional
649
+ Probability of applying this transform, by default 1.0
650
+ """
651
+
652
+ def __init__(
653
+ self,
654
+ channels: tuple = ("choice", [8, 32, 128, 256, 1024]),
655
+ name: str = None,
656
+ prob: float = 1.0,
657
+ ):
658
+ super().__init__(name=name, prob=prob)
659
+
660
+ self.channels = channels
661
+
662
+ def _instantiate(self, state: RandomState):
663
+ return {"channels": util.sample_from_dist(self.channels, state)}
664
+
665
+ def _transform(self, signal, channels):
666
+ return signal.mulaw_quantization(channels)
667
+
668
+
669
+ class NoiseFloor(BaseTransform):
670
+ """Adds a noise floor of Gaussian noise to the signal at a specified
671
+ dB.
672
+
673
+ Parameters
674
+ ----------
675
+ db : tuple, optional
676
+ Level of noise to add to signal, by default ("const", -50.0)
677
+ name : str, optional
678
+ Name of this transform, used to identify it in the dictionary
679
+ produced by ``self.instantiate``, by default None
680
+ prob : float, optional
681
+ Probability of applying this transform, by default 1.0
682
+ """
683
+
684
+ def __init__(
685
+ self,
686
+ db: tuple = ("const", -50.0),
687
+ name: str = None,
688
+ prob: float = 1.0,
689
+ ):
690
+ super().__init__(name=name, prob=prob)
691
+
692
+ self.db = db
693
+
694
+ def _instantiate(self, state: RandomState, signal: AudioSignal):
695
+ db = util.sample_from_dist(self.db, state)
696
+ audio_data = state.randn(signal.num_channels, signal.signal_length)
697
+ nz_signal = AudioSignal(audio_data, signal.sample_rate)
698
+ nz_signal.normalize(db)
699
+ return {"nz_signal": nz_signal}
700
+
701
+ def _transform(self, signal, nz_signal):
702
+ # Clone bg_signal so that transform can be repeatedly applied
703
+ # to different signals with the same effect.
704
+ return signal + nz_signal
705
+
706
+
707
+ class BackgroundNoise(BaseTransform):
708
+ """Adds background noise from audio specified by a set of CSV files.
709
+ A valid CSV file looks like, and is typically generated by
710
+ :py:func:`audiotools.data.preprocess.create_csv`:
711
+
712
+ .. csv-table::
713
+ :header: path
714
+
715
+ room_tone/m6_script2_clean.wav
716
+ room_tone/m6_script2_cleanraw.wav
717
+ room_tone/m6_script2_ipad_balcony1.wav
718
+ room_tone/m6_script2_ipad_bedroom1.wav
719
+ room_tone/m6_script2_ipad_confroom1.wav
720
+ room_tone/m6_script2_ipad_confroom2.wav
721
+ room_tone/m6_script2_ipad_livingroom1.wav
722
+ room_tone/m6_script2_ipad_office1.wav
723
+
724
+ .. note::
725
+ All paths are relative to an environment variable called ``PATH_TO_DATA``,
726
+ so that CSV files are portable across machines where data may be
727
+ located in different places.
728
+
729
+ This transform calls :py:func:`audiotools.core.effects.EffectMixin.mix`
730
+ and :py:func:`audiotools.core.effects.EffectMixin.equalizer` under the
731
+ hood.
732
+
733
+ Parameters
734
+ ----------
735
+ snr : tuple, optional
736
+ Signal-to-noise ratio, by default ("uniform", 10.0, 30.0)
737
+ sources : List[str], optional
738
+ Sources containing folders, or CSVs with paths to audio files,
739
+ by default None
740
+ weights : List[float], optional
741
+ Weights to sample audio files from each source, by default None
742
+ eq_amount : tuple, optional
743
+ Amount of equalization to apply, by default ("const", 1.0)
744
+ n_bands : int, optional
745
+ Number of bands in equalizer, by default 3
746
+ name : str, optional
747
+ Name of this transform, used to identify it in the dictionary
748
+ produced by ``self.instantiate``, by default None
749
+ prob : float, optional
750
+ Probability of applying this transform, by default 1.0
751
+ loudness_cutoff : float, optional
752
+ Loudness cutoff when loading from audio files, by default None
753
+ """
754
+
755
+ def __init__(
756
+ self,
757
+ snr: tuple = ("uniform", 10.0, 30.0),
758
+ sources: List[str] = None,
759
+ weights: List[float] = None,
760
+ eq_amount: tuple = ("const", 1.0),
761
+ n_bands: int = 3,
762
+ name: str = None,
763
+ prob: float = 1.0,
764
+ loudness_cutoff: float = None,
765
+ ):
766
+ super().__init__(name=name, prob=prob)
767
+
768
+ self.snr = snr
769
+ self.eq_amount = eq_amount
770
+ self.n_bands = n_bands
771
+ self.loader = AudioLoader(sources, weights)
772
+ self.loudness_cutoff = loudness_cutoff
773
+
774
+ def _instantiate(self, state: RandomState, signal: AudioSignal):
775
+ eq_amount = util.sample_from_dist(self.eq_amount, state)
776
+ eq = -eq_amount * state.rand(self.n_bands)
777
+ snr = util.sample_from_dist(self.snr, state)
778
+
779
+ bg_signal = self.loader(
780
+ state,
781
+ signal.sample_rate,
782
+ duration=signal.signal_duration,
783
+ loudness_cutoff=self.loudness_cutoff,
784
+ num_channels=signal.num_channels,
785
+ )["signal"]
786
+
787
+ return {"eq": eq, "bg_signal": bg_signal, "snr": snr}
788
+
789
+ def _transform(self, signal, bg_signal, snr, eq):
790
+ # Clone bg_signal so that transform can be repeatedly applied
791
+ # to different signals with the same effect.
792
+ return signal.mix(bg_signal.clone(), snr, eq)
793
+
794
+
795
+ class CrossTalk(BaseTransform):
796
+ """Adds crosstalk between speakers, whose audio is drawn from a CSV file
797
+ that was produced via :py:func:`audiotools.data.preprocess.create_csv`.
798
+
799
+ This transform calls :py:func:`audiotools.core.effects.EffectMixin.mix`
800
+ under the hood.
801
+
802
+ Parameters
803
+ ----------
804
+ snr : tuple, optional
805
+ How loud cross-talk speaker is relative to original signal in dB,
806
+ by default ("uniform", 0.0, 10.0)
807
+ sources : List[str], optional
808
+ Sources containing folders, or CSVs with paths to audio files,
809
+ by default None
810
+ weights : List[float], optional
811
+ Weights to sample audio files from each source, by default None
812
+ name : str, optional
813
+ Name of this transform, used to identify it in the dictionary
814
+ produced by ``self.instantiate``, by default None
815
+ prob : float, optional
816
+ Probability of applying this transform, by default 1.0
817
+ loudness_cutoff : float, optional
818
+ Loudness cutoff when loading from audio files, by default -40
819
+ """
820
+
821
+ def __init__(
822
+ self,
823
+ snr: tuple = ("uniform", 0.0, 10.0),
824
+ sources: List[str] = None,
825
+ weights: List[float] = None,
826
+ name: str = None,
827
+ prob: float = 1.0,
828
+ loudness_cutoff: float = -40,
829
+ ):
830
+ super().__init__(name=name, prob=prob)
831
+
832
+ self.snr = snr
833
+ self.loader = AudioLoader(sources, weights)
834
+ self.loudness_cutoff = loudness_cutoff
835
+
836
+ def _instantiate(self, state: RandomState, signal: AudioSignal):
837
+ snr = util.sample_from_dist(self.snr, state)
838
+ crosstalk_signal = self.loader(
839
+ state,
840
+ signal.sample_rate,
841
+ duration=signal.signal_duration,
842
+ loudness_cutoff=self.loudness_cutoff,
843
+ num_channels=signal.num_channels,
844
+ )["signal"]
845
+
846
+ return {"crosstalk_signal": crosstalk_signal, "snr": snr}
847
+
848
+ def _transform(self, signal, crosstalk_signal, snr):
849
+ # Clone bg_signal so that transform can be repeatedly applied
850
+ # to different signals with the same effect.
851
+ loudness = signal.loudness()
852
+ mix = signal.mix(crosstalk_signal.clone(), snr)
853
+ mix.normalize(loudness)
854
+ return mix
855
+
856
+
857
+ class RoomImpulseResponse(BaseTransform):
858
+ """Convolves signal with a room impulse response, at a specified
859
+ direct-to-reverberant ratio, with equalization applied. Room impulse
860
+ response data is drawn from a CSV file that was produced via
861
+ :py:func:`audiotools.data.preprocess.create_csv`.
862
+
863
+ This transform calls :py:func:`audiotools.core.effects.EffectMixin.apply_ir`
864
+ under the hood.
865
+
866
+ Parameters
867
+ ----------
868
+ drr : tuple, optional
869
+ _description_, by default ("uniform", 0.0, 30.0)
870
+ sources : List[str], optional
871
+ Sources containing folders, or CSVs with paths to audio files,
872
+ by default None
873
+ weights : List[float], optional
874
+ Weights to sample audio files from each source, by default None
875
+ eq_amount : tuple, optional
876
+ Amount of equalization to apply, by default ("const", 1.0)
877
+ n_bands : int, optional
878
+ Number of bands in equalizer, by default 6
879
+ name : str, optional
880
+ Name of this transform, used to identify it in the dictionary
881
+ produced by ``self.instantiate``, by default None
882
+ prob : float, optional
883
+ Probability of applying this transform, by default 1.0
884
+ use_original_phase : bool, optional
885
+ Whether or not to use the original phase, by default False
886
+ offset : float, optional
887
+ Offset from each impulse response file to use, by default 0.0
888
+ duration : float, optional
889
+ Duration of each impulse response, by default 1.0
890
+ """
891
+
892
+ def __init__(
893
+ self,
894
+ drr: tuple = ("uniform", 0.0, 30.0),
895
+ sources: List[str] = None,
896
+ weights: List[float] = None,
897
+ eq_amount: tuple = ("const", 1.0),
898
+ n_bands: int = 6,
899
+ name: str = None,
900
+ prob: float = 1.0,
901
+ use_original_phase: bool = False,
902
+ offset: float = 0.0,
903
+ duration: float = 1.0,
904
+ ):
905
+ super().__init__(name=name, prob=prob)
906
+
907
+ self.drr = drr
908
+ self.eq_amount = eq_amount
909
+ self.n_bands = n_bands
910
+ self.use_original_phase = use_original_phase
911
+
912
+ self.loader = AudioLoader(sources, weights)
913
+ self.offset = offset
914
+ self.duration = duration
915
+
916
+ def _instantiate(self, state: RandomState, signal: AudioSignal = None):
917
+ eq_amount = util.sample_from_dist(self.eq_amount, state)
918
+ eq = -eq_amount * state.rand(self.n_bands)
919
+ drr = util.sample_from_dist(self.drr, state)
920
+
921
+ ir_signal = self.loader(
922
+ state,
923
+ signal.sample_rate,
924
+ offset=self.offset,
925
+ duration=self.duration,
926
+ loudness_cutoff=None,
927
+ num_channels=signal.num_channels,
928
+ )["signal"]
929
+ ir_signal.zero_pad_to(signal.sample_rate)
930
+
931
+ return {"eq": eq, "ir_signal": ir_signal, "drr": drr}
932
+
933
+ def _transform(self, signal, ir_signal, drr, eq):
934
+ # Clone ir_signal so that transform can be repeatedly applied
935
+ # to different signals with the same effect.
936
+ return signal.apply_ir(
937
+ ir_signal.clone(), drr, eq, use_original_phase=self.use_original_phase
938
+ )
939
+
940
+
941
+ class VolumeChange(BaseTransform):
942
+ """Changes the volume of the input signal.
943
+
944
+ Uses :py:func:`audiotools.core.effects.EffectMixin.volume_change`.
945
+
946
+ Parameters
947
+ ----------
948
+ db : tuple, optional
949
+ Change in volume in decibels, by default ("uniform", -12.0, 0.0)
950
+ name : str, optional
951
+ Name of this transform, used to identify it in the dictionary
952
+ produced by ``self.instantiate``, by default None
953
+ prob : float, optional
954
+ Probability of applying this transform, by default 1.0
955
+ """
956
+
957
+ def __init__(
958
+ self,
959
+ db: tuple = ("uniform", -12.0, 0.0),
960
+ name: str = None,
961
+ prob: float = 1.0,
962
+ ):
963
+ super().__init__(name=name, prob=prob)
964
+ self.db = db
965
+
966
+ def _instantiate(self, state: RandomState):
967
+ return {"db": util.sample_from_dist(self.db, state)}
968
+
969
+ def _transform(self, signal, db):
970
+ return signal.volume_change(db)
971
+
972
+
973
+ class VolumeNorm(BaseTransform):
974
+ """Normalizes the volume of the excerpt to a specified decibel.
975
+
976
+ Uses :py:func:`audiotools.core.effects.EffectMixin.normalize`.
977
+
978
+ Parameters
979
+ ----------
980
+ db : tuple, optional
981
+ dB to normalize signal to, by default ("const", -24)
982
+ name : str, optional
983
+ Name of this transform, used to identify it in the dictionary
984
+ produced by ``self.instantiate``, by default None
985
+ prob : float, optional
986
+ Probability of applying this transform, by default 1.0
987
+ """
988
+
989
+ def __init__(
990
+ self,
991
+ db: tuple = ("const", -24),
992
+ name: str = None,
993
+ prob: float = 1.0,
994
+ ):
995
+ super().__init__(name=name, prob=prob)
996
+
997
+ self.db = db
998
+
999
+ def _instantiate(self, state: RandomState):
1000
+ return {"db": util.sample_from_dist(self.db, state)}
1001
+
1002
+ def _transform(self, signal, db):
1003
+ return signal.normalize(db)
1004
+
1005
+
1006
+ class GlobalVolumeNorm(BaseTransform):
1007
+ """Similar to :py:func:`audiotools.data.transforms.VolumeNorm`, this
1008
+ transform also normalizes the volume of a signal, but it uses
1009
+ the volume of the entire audio file the loaded excerpt comes from,
1010
+ rather than the volume of just the excerpt. The volume of the
1011
+ entire audio file is expected in ``signal.metadata["loudness"]``.
1012
+ If loading audio from a CSV generated by :py:func:`audiotools.data.preprocess.create_csv`
1013
+ with ``loudness = True``, like the following:
1014
+
1015
+ .. csv-table::
1016
+ :header: path,loudness
1017
+
1018
+ daps/produced/f1_script1_produced.wav,-16.299999237060547
1019
+ daps/produced/f1_script2_produced.wav,-16.600000381469727
1020
+ daps/produced/f1_script3_produced.wav,-17.299999237060547
1021
+ daps/produced/f1_script4_produced.wav,-16.100000381469727
1022
+ daps/produced/f1_script5_produced.wav,-16.700000762939453
1023
+ daps/produced/f3_script1_produced.wav,-16.5
1024
+
1025
+ The ``AudioLoader`` will automatically load the loudness column into
1026
+ the metadata of the signal.
1027
+
1028
+ Uses :py:func:`audiotools.core.effects.EffectMixin.volume_change`.
1029
+
1030
+ Parameters
1031
+ ----------
1032
+ db : tuple, optional
1033
+ dB to normalize signal to, by default ("const", -24)
1034
+ name : str, optional
1035
+ Name of this transform, used to identify it in the dictionary
1036
+ produced by ``self.instantiate``, by default None
1037
+ prob : float, optional
1038
+ Probability of applying this transform, by default 1.0
1039
+ """
1040
+
1041
+ def __init__(
1042
+ self,
1043
+ db: tuple = ("const", -24),
1044
+ name: str = None,
1045
+ prob: float = 1.0,
1046
+ ):
1047
+ super().__init__(name=name, prob=prob)
1048
+
1049
+ self.db = db
1050
+
1051
+ def _instantiate(self, state: RandomState, signal: AudioSignal):
1052
+ if "loudness" not in signal.metadata:
1053
+ db_change = 0.0
1054
+ elif float(signal.metadata["loudness"]) == float("-inf"):
1055
+ db_change = 0.0
1056
+ else:
1057
+ db = util.sample_from_dist(self.db, state)
1058
+ db_change = db - float(signal.metadata["loudness"])
1059
+
1060
+ return {"db": db_change}
1061
+
1062
+ def _transform(self, signal, db):
1063
+ return signal.volume_change(db)
1064
+
1065
+
1066
+ class Silence(BaseTransform):
1067
+ """Zeros out the signal with some probability.
1068
+
1069
+ Parameters
1070
+ ----------
1071
+ name : str, optional
1072
+ Name of this transform, used to identify it in the dictionary
1073
+ produced by ``self.instantiate``, by default None
1074
+ prob : float, optional
1075
+ Probability of applying this transform, by default 0.1
1076
+ """
1077
+
1078
+ def __init__(self, name: str = None, prob: float = 0.1):
1079
+ super().__init__(name=name, prob=prob)
1080
+
1081
+ def _transform(self, signal):
1082
+ _loudness = signal._loudness
1083
+ signal = AudioSignal(
1084
+ torch.zeros_like(signal.audio_data),
1085
+ sample_rate=signal.sample_rate,
1086
+ stft_params=signal.stft_params,
1087
+ )
1088
+ # So that the amound of noise added is as if it wasn't silenced.
1089
+ # TODO: improve this hack
1090
+ signal._loudness = _loudness
1091
+
1092
+ return signal
1093
+
1094
+
1095
+ class LowPass(BaseTransform):
1096
+ """Applies a LowPass filter.
1097
+
1098
+ Uses :py:func:`audiotools.core.dsp.DSPMixin.low_pass`.
1099
+
1100
+ Parameters
1101
+ ----------
1102
+ cutoff : tuple, optional
1103
+ Cutoff frequency distribution,
1104
+ by default ``("choice", [4000, 8000, 16000])``
1105
+ zeros : int, optional
1106
+ Number of zero-crossings in filter, argument to
1107
+ ``julius.LowPassFilters``, by default 51
1108
+ name : str, optional
1109
+ Name of this transform, used to identify it in the dictionary
1110
+ produced by ``self.instantiate``, by default None
1111
+ prob : float, optional
1112
+ Probability of applying this transform, by default 1.0
1113
+ """
1114
+
1115
+ def __init__(
1116
+ self,
1117
+ cutoff: tuple = ("choice", [4000, 8000, 16000]),
1118
+ zeros: int = 51,
1119
+ name: str = None,
1120
+ prob: float = 1,
1121
+ ):
1122
+ super().__init__(name=name, prob=prob)
1123
+
1124
+ self.cutoff = cutoff
1125
+ self.zeros = zeros
1126
+
1127
+ def _instantiate(self, state: RandomState):
1128
+ return {"cutoff": util.sample_from_dist(self.cutoff, state)}
1129
+
1130
+ def _transform(self, signal, cutoff):
1131
+ return signal.low_pass(cutoff, zeros=self.zeros)
1132
+
1133
+
1134
+ class HighPass(BaseTransform):
1135
+ """Applies a HighPass filter.
1136
+
1137
+ Uses :py:func:`audiotools.core.dsp.DSPMixin.high_pass`.
1138
+
1139
+ Parameters
1140
+ ----------
1141
+ cutoff : tuple, optional
1142
+ Cutoff frequency distribution,
1143
+ by default ``("choice", [50, 100, 250, 500, 1000])``
1144
+ zeros : int, optional
1145
+ Number of zero-crossings in filter, argument to
1146
+ ``julius.LowPassFilters``, by default 51
1147
+ name : str, optional
1148
+ Name of this transform, used to identify it in the dictionary
1149
+ produced by ``self.instantiate``, by default None
1150
+ prob : float, optional
1151
+ Probability of applying this transform, by default 1.0
1152
+ """
1153
+
1154
+ def __init__(
1155
+ self,
1156
+ cutoff: tuple = ("choice", [50, 100, 250, 500, 1000]),
1157
+ zeros: int = 51,
1158
+ name: str = None,
1159
+ prob: float = 1,
1160
+ ):
1161
+ super().__init__(name=name, prob=prob)
1162
+
1163
+ self.cutoff = cutoff
1164
+ self.zeros = zeros
1165
+
1166
+ def _instantiate(self, state: RandomState):
1167
+ return {"cutoff": util.sample_from_dist(self.cutoff, state)}
1168
+
1169
+ def _transform(self, signal, cutoff):
1170
+ return signal.high_pass(cutoff, zeros=self.zeros)
1171
+
1172
+
1173
+ class RescaleAudio(BaseTransform):
1174
+ """Rescales the audio so it is in between ``-val`` and ``val``
1175
+ only if the original audio exceeds those bounds. Useful if
1176
+ transforms have caused the audio to clip.
1177
+
1178
+ Uses :py:func:`audiotools.core.effects.EffectMixin.ensure_max_of_audio`.
1179
+
1180
+ Parameters
1181
+ ----------
1182
+ val : float, optional
1183
+ Max absolute value of signal, by default 1.0
1184
+ name : str, optional
1185
+ Name of this transform, used to identify it in the dictionary
1186
+ produced by ``self.instantiate``, by default None
1187
+ prob : float, optional
1188
+ Probability of applying this transform, by default 1.0
1189
+ """
1190
+
1191
+ def __init__(self, val: float = 1.0, name: str = None, prob: float = 1):
1192
+ super().__init__(name=name, prob=prob)
1193
+
1194
+ self.val = val
1195
+
1196
+ def _transform(self, signal):
1197
+ return signal.ensure_max_of_audio(self.val)
1198
+
1199
+
1200
+ class ShiftPhase(SpectralTransform):
1201
+ """Shifts the phase of the audio.
1202
+
1203
+ Uses :py:func:`audiotools.core.dsp.DSPMixin.shift)phase`.
1204
+
1205
+ Parameters
1206
+ ----------
1207
+ shift : tuple, optional
1208
+ How much to shift phase by, by default ("uniform", -np.pi, np.pi)
1209
+ name : str, optional
1210
+ Name of this transform, used to identify it in the dictionary
1211
+ produced by ``self.instantiate``, by default None
1212
+ prob : float, optional
1213
+ Probability of applying this transform, by default 1.0
1214
+ """
1215
+
1216
+ def __init__(
1217
+ self,
1218
+ shift: tuple = ("uniform", -np.pi, np.pi),
1219
+ name: str = None,
1220
+ prob: float = 1,
1221
+ ):
1222
+ super().__init__(name=name, prob=prob)
1223
+ self.shift = shift
1224
+
1225
+ def _instantiate(self, state: RandomState):
1226
+ return {"shift": util.sample_from_dist(self.shift, state)}
1227
+
1228
+ def _transform(self, signal, shift):
1229
+ return signal.shift_phase(shift)
1230
+
1231
+
1232
+ class InvertPhase(ShiftPhase):
1233
+ """Inverts the phase of the audio.
1234
+
1235
+ Uses :py:func:`audiotools.core.dsp.DSPMixin.shift_phase`.
1236
+
1237
+ Parameters
1238
+ ----------
1239
+ name : str, optional
1240
+ Name of this transform, used to identify it in the dictionary
1241
+ produced by ``self.instantiate``, by default None
1242
+ prob : float, optional
1243
+ Probability of applying this transform, by default 1.0
1244
+ """
1245
+
1246
+ def __init__(self, name: str = None, prob: float = 1):
1247
+ super().__init__(shift=("const", np.pi), name=name, prob=prob)
1248
+
1249
+
1250
+ class CorruptPhase(SpectralTransform):
1251
+ """Corrupts the phase of the audio.
1252
+
1253
+ Uses :py:func:`audiotools.core.dsp.DSPMixin.corrupt_phase`.
1254
+
1255
+ Parameters
1256
+ ----------
1257
+ scale : tuple, optional
1258
+ How much to corrupt phase by, by default ("uniform", 0, np.pi)
1259
+ name : str, optional
1260
+ Name of this transform, used to identify it in the dictionary
1261
+ produced by ``self.instantiate``, by default None
1262
+ prob : float, optional
1263
+ Probability of applying this transform, by default 1.0
1264
+ """
1265
+
1266
+ def __init__(
1267
+ self, scale: tuple = ("uniform", 0, np.pi), name: str = None, prob: float = 1
1268
+ ):
1269
+ super().__init__(name=name, prob=prob)
1270
+ self.scale = scale
1271
+
1272
+ def _instantiate(self, state: RandomState, signal: AudioSignal = None):
1273
+ scale = util.sample_from_dist(self.scale, state)
1274
+ corruption = state.normal(scale=scale, size=signal.phase.shape[1:])
1275
+ return {"corruption": corruption.astype("float32")}
1276
+
1277
+ def _transform(self, signal, corruption):
1278
+ return signal.shift_phase(shift=corruption)
1279
+
1280
+
1281
+ class FrequencyMask(SpectralTransform):
1282
+ """Masks a band of frequencies at a center frequency
1283
+ from the audio.
1284
+
1285
+ Uses :py:func:`audiotools.core.dsp.DSPMixin.mask_frequencies`.
1286
+
1287
+ Parameters
1288
+ ----------
1289
+ f_center : tuple, optional
1290
+ Center frequency between 0.0 and 1.0 (Nyquist), by default ("uniform", 0.0, 1.0)
1291
+ f_width : tuple, optional
1292
+ Width of zero'd out band, by default ("const", 0.1)
1293
+ name : str, optional
1294
+ Name of this transform, used to identify it in the dictionary
1295
+ produced by ``self.instantiate``, by default None
1296
+ prob : float, optional
1297
+ Probability of applying this transform, by default 1.0
1298
+ """
1299
+
1300
+ def __init__(
1301
+ self,
1302
+ f_center: tuple = ("uniform", 0.0, 1.0),
1303
+ f_width: tuple = ("const", 0.1),
1304
+ name: str = None,
1305
+ prob: float = 1,
1306
+ ):
1307
+ super().__init__(name=name, prob=prob)
1308
+ self.f_center = f_center
1309
+ self.f_width = f_width
1310
+
1311
+ def _instantiate(self, state: RandomState, signal: AudioSignal):
1312
+ f_center = util.sample_from_dist(self.f_center, state)
1313
+ f_width = util.sample_from_dist(self.f_width, state)
1314
+
1315
+ fmin = max(f_center - (f_width / 2), 0.0)
1316
+ fmax = min(f_center + (f_width / 2), 1.0)
1317
+
1318
+ fmin_hz = (signal.sample_rate / 2) * fmin
1319
+ fmax_hz = (signal.sample_rate / 2) * fmax
1320
+
1321
+ return {"fmin_hz": fmin_hz, "fmax_hz": fmax_hz}
1322
+
1323
+ def _transform(self, signal, fmin_hz: float, fmax_hz: float):
1324
+ return signal.mask_frequencies(fmin_hz=fmin_hz, fmax_hz=fmax_hz)
1325
+
1326
+
1327
+ class TimeMask(SpectralTransform):
1328
+ """Masks out contiguous time-steps from signal.
1329
+
1330
+ Uses :py:func:`audiotools.core.dsp.DSPMixin.mask_timesteps`.
1331
+
1332
+ Parameters
1333
+ ----------
1334
+ t_center : tuple, optional
1335
+ Center time in terms of 0.0 and 1.0 (duration of signal),
1336
+ by default ("uniform", 0.0, 1.0)
1337
+ t_width : tuple, optional
1338
+ Width of dropped out portion, by default ("const", 0.025)
1339
+ name : str, optional
1340
+ Name of this transform, used to identify it in the dictionary
1341
+ produced by ``self.instantiate``, by default None
1342
+ prob : float, optional
1343
+ Probability of applying this transform, by default 1.0
1344
+ """
1345
+
1346
+ def __init__(
1347
+ self,
1348
+ t_center: tuple = ("uniform", 0.0, 1.0),
1349
+ t_width: tuple = ("const", 0.025),
1350
+ name: str = None,
1351
+ prob: float = 1,
1352
+ ):
1353
+ super().__init__(name=name, prob=prob)
1354
+ self.t_center = t_center
1355
+ self.t_width = t_width
1356
+
1357
+ def _instantiate(self, state: RandomState, signal: AudioSignal):
1358
+ t_center = util.sample_from_dist(self.t_center, state)
1359
+ t_width = util.sample_from_dist(self.t_width, state)
1360
+
1361
+ tmin = max(t_center - (t_width / 2), 0.0)
1362
+ tmax = min(t_center + (t_width / 2), 1.0)
1363
+
1364
+ tmin_s = signal.signal_duration * tmin
1365
+ tmax_s = signal.signal_duration * tmax
1366
+ return {"tmin_s": tmin_s, "tmax_s": tmax_s}
1367
+
1368
+ def _transform(self, signal, tmin_s: float, tmax_s: float):
1369
+ return signal.mask_timesteps(tmin_s=tmin_s, tmax_s=tmax_s)
1370
+
1371
+
1372
+ class MaskLowMagnitudes(SpectralTransform):
1373
+ """Masks low magnitude regions out of signal.
1374
+
1375
+ Uses :py:func:`audiotools.core.dsp.DSPMixin.mask_low_magnitudes`.
1376
+
1377
+ Parameters
1378
+ ----------
1379
+ db_cutoff : tuple, optional
1380
+ Decibel value for which things below it will be masked away,
1381
+ by default ("uniform", -10, 10)
1382
+ name : str, optional
1383
+ Name of this transform, used to identify it in the dictionary
1384
+ produced by ``self.instantiate``, by default None
1385
+ prob : float, optional
1386
+ Probability of applying this transform, by default 1.0
1387
+ """
1388
+
1389
+ def __init__(
1390
+ self,
1391
+ db_cutoff: tuple = ("uniform", -10, 10),
1392
+ name: str = None,
1393
+ prob: float = 1,
1394
+ ):
1395
+ super().__init__(name=name, prob=prob)
1396
+ self.db_cutoff = db_cutoff
1397
+
1398
+ def _instantiate(self, state: RandomState, signal: AudioSignal = None):
1399
+ return {"db_cutoff": util.sample_from_dist(self.db_cutoff, state)}
1400
+
1401
+ def _transform(self, signal, db_cutoff: float):
1402
+ return signal.mask_low_magnitudes(db_cutoff)
1403
+
1404
+
1405
+ class Smoothing(BaseTransform):
1406
+ """Convolves the signal with a smoothing window.
1407
+
1408
+ Uses :py:func:`audiotools.core.effects.EffectMixin.convolve`.
1409
+
1410
+ Parameters
1411
+ ----------
1412
+ window_type : tuple, optional
1413
+ Type of window to use, by default ("const", "average")
1414
+ window_length : tuple, optional
1415
+ Length of smoothing window, by
1416
+ default ("choice", [8, 16, 32, 64, 128, 256, 512])
1417
+ name : str, optional
1418
+ Name of this transform, used to identify it in the dictionary
1419
+ produced by ``self.instantiate``, by default None
1420
+ prob : float, optional
1421
+ Probability of applying this transform, by default 1.0
1422
+ """
1423
+
1424
+ def __init__(
1425
+ self,
1426
+ window_type: tuple = ("const", "average"),
1427
+ window_length: tuple = ("choice", [8, 16, 32, 64, 128, 256, 512]),
1428
+ name: str = None,
1429
+ prob: float = 1,
1430
+ ):
1431
+ super().__init__(name=name, prob=prob)
1432
+ self.window_type = window_type
1433
+ self.window_length = window_length
1434
+
1435
+ def _instantiate(self, state: RandomState, signal: AudioSignal = None):
1436
+ window_type = util.sample_from_dist(self.window_type, state)
1437
+ window_length = util.sample_from_dist(self.window_length, state)
1438
+ window = signal.get_window(
1439
+ window_type=window_type, window_length=window_length, device="cpu"
1440
+ )
1441
+ return {"window": AudioSignal(window, signal.sample_rate)}
1442
+
1443
+ def _transform(self, signal, window):
1444
+ sscale = signal.audio_data.abs().max(dim=-1, keepdim=True).values
1445
+ sscale[sscale == 0.0] = 1.0
1446
+
1447
+ out = signal.convolve(window)
1448
+
1449
+ oscale = out.audio_data.abs().max(dim=-1, keepdim=True).values
1450
+ oscale[oscale == 0.0] = 1.0
1451
+
1452
+ out = out * (sscale / oscale)
1453
+ return out
1454
+
1455
+
1456
+ class TimeNoise(TimeMask):
1457
+ """Similar to :py:func:`audiotools.data.transforms.TimeMask`, but
1458
+ replaces with noise instead of zeros.
1459
+
1460
+ Parameters
1461
+ ----------
1462
+ t_center : tuple, optional
1463
+ Center time in terms of 0.0 and 1.0 (duration of signal),
1464
+ by default ("uniform", 0.0, 1.0)
1465
+ t_width : tuple, optional
1466
+ Width of dropped out portion, by default ("const", 0.025)
1467
+ name : str, optional
1468
+ Name of this transform, used to identify it in the dictionary
1469
+ produced by ``self.instantiate``, by default None
1470
+ prob : float, optional
1471
+ Probability of applying this transform, by default 1.0
1472
+ """
1473
+
1474
+ def __init__(
1475
+ self,
1476
+ t_center: tuple = ("uniform", 0.0, 1.0),
1477
+ t_width: tuple = ("const", 0.025),
1478
+ name: str = None,
1479
+ prob: float = 1,
1480
+ ):
1481
+ super().__init__(t_center=t_center, t_width=t_width, name=name, prob=prob)
1482
+
1483
+ def _transform(self, signal, tmin_s: float, tmax_s: float):
1484
+ signal = signal.mask_timesteps(tmin_s=tmin_s, tmax_s=tmax_s, val=0.0)
1485
+ mag, phase = signal.magnitude, signal.phase
1486
+
1487
+ mag_r, phase_r = torch.randn_like(mag), torch.randn_like(phase)
1488
+ mask = (mag == 0.0) * (phase == 0.0)
1489
+
1490
+ mag[mask] = mag_r[mask]
1491
+ phase[mask] = phase_r[mask]
1492
+
1493
+ signal.magnitude = mag
1494
+ signal.phase = phase
1495
+ return signal
1496
+
1497
+
1498
+ class FrequencyNoise(FrequencyMask):
1499
+ """Similar to :py:func:`audiotools.data.transforms.FrequencyMask`, but
1500
+ replaces with noise instead of zeros.
1501
+
1502
+ Parameters
1503
+ ----------
1504
+ f_center : tuple, optional
1505
+ Center frequency between 0.0 and 1.0 (Nyquist), by default ("uniform", 0.0, 1.0)
1506
+ f_width : tuple, optional
1507
+ Width of zero'd out band, by default ("const", 0.1)
1508
+ name : str, optional
1509
+ Name of this transform, used to identify it in the dictionary
1510
+ produced by ``self.instantiate``, by default None
1511
+ prob : float, optional
1512
+ Probability of applying this transform, by default 1.0
1513
+ """
1514
+
1515
+ def __init__(
1516
+ self,
1517
+ f_center: tuple = ("uniform", 0.0, 1.0),
1518
+ f_width: tuple = ("const", 0.1),
1519
+ name: str = None,
1520
+ prob: float = 1,
1521
+ ):
1522
+ super().__init__(f_center=f_center, f_width=f_width, name=name, prob=prob)
1523
+
1524
+ def _transform(self, signal, fmin_hz: float, fmax_hz: float):
1525
+ signal = signal.mask_frequencies(fmin_hz=fmin_hz, fmax_hz=fmax_hz)
1526
+ mag, phase = signal.magnitude, signal.phase
1527
+
1528
+ mag_r, phase_r = torch.randn_like(mag), torch.randn_like(phase)
1529
+ mask = (mag == 0.0) * (phase == 0.0)
1530
+
1531
+ mag[mask] = mag_r[mask]
1532
+ phase[mask] = phase_r[mask]
1533
+
1534
+ signal.magnitude = mag
1535
+ signal.phase = phase
1536
+ return signal
1537
+
1538
+
1539
+ class SpectralDenoising(Equalizer):
1540
+ """Applies denoising algorithm detailed in
1541
+ :py:func:`audiotools.ml.layers.spectral_gate.SpectralGate`,
1542
+ using a randomly generated noise signal for denoising.
1543
+
1544
+ Parameters
1545
+ ----------
1546
+ eq_amount : tuple, optional
1547
+ Amount of eq to apply to noise signal, by default ("const", 1.0)
1548
+ denoise_amount : tuple, optional
1549
+ Amount to denoise by, by default ("uniform", 0.8, 1.0)
1550
+ nz_volume : float, optional
1551
+ Volume of noise to denoise with, by default -40
1552
+ n_bands : int, optional
1553
+ Number of bands in equalizer, by default 6
1554
+ n_freq : int, optional
1555
+ Number of frequency bins to smooth by, by default 3
1556
+ n_time : int, optional
1557
+ Number of time bins to smooth by, by default 5
1558
+ name : str, optional
1559
+ Name of this transform, used to identify it in the dictionary
1560
+ produced by ``self.instantiate``, by default None
1561
+ prob : float, optional
1562
+ Probability of applying this transform, by default 1.0
1563
+ """
1564
+
1565
+ def __init__(
1566
+ self,
1567
+ eq_amount: tuple = ("const", 1.0),
1568
+ denoise_amount: tuple = ("uniform", 0.8, 1.0),
1569
+ nz_volume: float = -40,
1570
+ n_bands: int = 6,
1571
+ n_freq: int = 3,
1572
+ n_time: int = 5,
1573
+ name: str = None,
1574
+ prob: float = 1,
1575
+ ):
1576
+ super().__init__(eq_amount=eq_amount, n_bands=n_bands, name=name, prob=prob)
1577
+
1578
+ self.nz_volume = nz_volume
1579
+ self.denoise_amount = denoise_amount
1580
+ self.spectral_gate = ml.layers.SpectralGate(n_freq, n_time)
1581
+
1582
+ def _transform(self, signal, nz, eq, denoise_amount):
1583
+ nz = nz.normalize(self.nz_volume).equalizer(eq)
1584
+ self.spectral_gate = self.spectral_gate.to(signal.device)
1585
+ signal = self.spectral_gate(signal, nz, denoise_amount)
1586
+ return signal
1587
+
1588
+ def _instantiate(self, state: RandomState):
1589
+ kwargs = super()._instantiate(state)
1590
+ kwargs["denoise_amount"] = util.sample_from_dist(self.denoise_amount, state)
1591
+ kwargs["nz"] = AudioSignal(state.randn(22050), 44100)
1592
+ return kwargs
audiotools/metrics/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """
2
+ Functions for comparing AudioSignal objects to one another.
3
+ """ # fmt: skip
4
+ from . import distance
5
+ from . import quality
6
+ from . import spectral
audiotools/metrics/distance.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch import nn
3
+
4
+ from .. import AudioSignal
5
+
6
+
7
+ class L1Loss(nn.L1Loss):
8
+ """L1 Loss between AudioSignals. Defaults
9
+ to comparing ``audio_data``, but any
10
+ attribute of an AudioSignal can be used.
11
+
12
+ Parameters
13
+ ----------
14
+ attribute : str, optional
15
+ Attribute of signal to compare, defaults to ``audio_data``.
16
+ weight : float, optional
17
+ Weight of this loss, defaults to 1.0.
18
+ """
19
+
20
+ def __init__(self, attribute: str = "audio_data", weight: float = 1.0, **kwargs):
21
+ self.attribute = attribute
22
+ self.weight = weight
23
+ super().__init__(**kwargs)
24
+
25
+ def forward(self, x: AudioSignal, y: AudioSignal):
26
+ """
27
+ Parameters
28
+ ----------
29
+ x : AudioSignal
30
+ Estimate AudioSignal
31
+ y : AudioSignal
32
+ Reference AudioSignal
33
+
34
+ Returns
35
+ -------
36
+ torch.Tensor
37
+ L1 loss between AudioSignal attributes.
38
+ """
39
+ if isinstance(x, AudioSignal):
40
+ x = getattr(x, self.attribute)
41
+ y = getattr(y, self.attribute)
42
+ return super().forward(x, y)
43
+
44
+
45
+ class SISDRLoss(nn.Module):
46
+ """
47
+ Computes the Scale-Invariant Source-to-Distortion Ratio between a batch
48
+ of estimated and reference audio signals or aligned features.
49
+
50
+ Parameters
51
+ ----------
52
+ scaling : int, optional
53
+ Whether to use scale-invariant (True) or
54
+ signal-to-noise ratio (False), by default True
55
+ reduction : str, optional
56
+ How to reduce across the batch (either 'mean',
57
+ 'sum', or none).], by default ' mean'
58
+ zero_mean : int, optional
59
+ Zero mean the references and estimates before
60
+ computing the loss, by default True
61
+ clip_min : int, optional
62
+ The minimum possible loss value. Helps network
63
+ to not focus on making already good examples better, by default None
64
+ weight : float, optional
65
+ Weight of this loss, defaults to 1.0.
66
+ """
67
+
68
+ def __init__(
69
+ self,
70
+ scaling: int = True,
71
+ reduction: str = "mean",
72
+ zero_mean: int = True,
73
+ clip_min: int = None,
74
+ weight: float = 1.0,
75
+ ):
76
+ self.scaling = scaling
77
+ self.reduction = reduction
78
+ self.zero_mean = zero_mean
79
+ self.clip_min = clip_min
80
+ self.weight = weight
81
+ super().__init__()
82
+
83
+ def forward(self, x: AudioSignal, y: AudioSignal):
84
+ eps = 1e-8
85
+ # nb, nc, nt
86
+ if isinstance(x, AudioSignal):
87
+ references = x.audio_data
88
+ estimates = y.audio_data
89
+ else:
90
+ references = x
91
+ estimates = y
92
+
93
+ nb = references.shape[0]
94
+ references = references.reshape(nb, 1, -1).permute(0, 2, 1)
95
+ estimates = estimates.reshape(nb, 1, -1).permute(0, 2, 1)
96
+
97
+ # samples now on axis 1
98
+ if self.zero_mean:
99
+ mean_reference = references.mean(dim=1, keepdim=True)
100
+ mean_estimate = estimates.mean(dim=1, keepdim=True)
101
+ else:
102
+ mean_reference = 0
103
+ mean_estimate = 0
104
+
105
+ _references = references - mean_reference
106
+ _estimates = estimates - mean_estimate
107
+
108
+ references_projection = (_references**2).sum(dim=-2) + eps
109
+ references_on_estimates = (_estimates * _references).sum(dim=-2) + eps
110
+
111
+ scale = (
112
+ (references_on_estimates / references_projection).unsqueeze(1)
113
+ if self.scaling
114
+ else 1
115
+ )
116
+
117
+ e_true = scale * _references
118
+ e_res = _estimates - e_true
119
+
120
+ signal = (e_true**2).sum(dim=1)
121
+ noise = (e_res**2).sum(dim=1)
122
+ sdr = -10 * torch.log10(signal / noise + eps)
123
+
124
+ if self.clip_min is not None:
125
+ sdr = torch.clamp(sdr, min=self.clip_min)
126
+
127
+ if self.reduction == "mean":
128
+ sdr = sdr.mean()
129
+ elif self.reduction == "sum":
130
+ sdr = sdr.sum()
131
+ return sdr
audiotools/metrics/quality.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import numpy as np
4
+ import torch
5
+
6
+ from .. import AudioSignal
7
+
8
+
9
+ def stoi(
10
+ estimates: AudioSignal,
11
+ references: AudioSignal,
12
+ extended: int = False,
13
+ ):
14
+ """Short term objective intelligibility
15
+ Computes the STOI (See [1][2]) of a denoised signal compared to a clean
16
+ signal, The output is expected to have a monotonic relation with the
17
+ subjective speech-intelligibility, where a higher score denotes better
18
+ speech intelligibility. Uses pystoi under the hood.
19
+
20
+ Parameters
21
+ ----------
22
+ estimates : AudioSignal
23
+ Denoised speech
24
+ references : AudioSignal
25
+ Clean original speech
26
+ extended : int, optional
27
+ Boolean, whether to use the extended STOI described in [3], by default False
28
+
29
+ Returns
30
+ -------
31
+ Tensor[float]
32
+ Short time objective intelligibility measure between clean and
33
+ denoised speech
34
+
35
+ References
36
+ ----------
37
+ 1. C.H.Taal, R.C.Hendriks, R.Heusdens, J.Jensen 'A Short-Time
38
+ Objective Intelligibility Measure for Time-Frequency Weighted Noisy
39
+ Speech', ICASSP 2010, Texas, Dallas.
40
+ 2. C.H.Taal, R.C.Hendriks, R.Heusdens, J.Jensen 'An Algorithm for
41
+ Intelligibility Prediction of Time-Frequency Weighted Noisy Speech',
42
+ IEEE Transactions on Audio, Speech, and Language Processing, 2011.
43
+ 3. Jesper Jensen and Cees H. Taal, 'An Algorithm for Predicting the
44
+ Intelligibility of Speech Masked by Modulated Noise Maskers',
45
+ IEEE Transactions on Audio, Speech and Language Processing, 2016.
46
+ """
47
+ import pystoi
48
+
49
+ estimates = estimates.clone().to_mono()
50
+ references = references.clone().to_mono()
51
+
52
+ stois = []
53
+ for i in range(estimates.batch_size):
54
+ _stoi = pystoi.stoi(
55
+ references.audio_data[i, 0].detach().cpu().numpy(),
56
+ estimates.audio_data[i, 0].detach().cpu().numpy(),
57
+ references.sample_rate,
58
+ extended=extended,
59
+ )
60
+ stois.append(_stoi)
61
+ return torch.from_numpy(np.array(stois))
62
+
63
+
64
+ def pesq(
65
+ estimates: AudioSignal,
66
+ references: AudioSignal,
67
+ mode: str = "wb",
68
+ target_sr: float = 16000,
69
+ ):
70
+ """_summary_
71
+
72
+ Parameters
73
+ ----------
74
+ estimates : AudioSignal
75
+ Degraded AudioSignal
76
+ references : AudioSignal
77
+ Reference AudioSignal
78
+ mode : str, optional
79
+ 'wb' (wide-band) or 'nb' (narrow-band), by default "wb"
80
+ target_sr : float, optional
81
+ Target sample rate, by default 16000
82
+
83
+ Returns
84
+ -------
85
+ Tensor[float]
86
+ PESQ score: P.862.2 Prediction (MOS-LQO)
87
+ """
88
+ from pesq import pesq as pesq_fn
89
+
90
+ estimates = estimates.clone().to_mono().resample(target_sr)
91
+ references = references.clone().to_mono().resample(target_sr)
92
+
93
+ pesqs = []
94
+ for i in range(estimates.batch_size):
95
+ _pesq = pesq_fn(
96
+ estimates.sample_rate,
97
+ references.audio_data[i, 0].detach().cpu().numpy(),
98
+ estimates.audio_data[i, 0].detach().cpu().numpy(),
99
+ mode,
100
+ )
101
+ pesqs.append(_pesq)
102
+ return torch.from_numpy(np.array(pesqs))
103
+
104
+
105
+ def visqol(
106
+ estimates: AudioSignal,
107
+ references: AudioSignal,
108
+ mode: str = "audio",
109
+ ): # pragma: no cover
110
+ """ViSQOL score.
111
+
112
+ Parameters
113
+ ----------
114
+ estimates : AudioSignal
115
+ Degraded AudioSignal
116
+ references : AudioSignal
117
+ Reference AudioSignal
118
+ mode : str, optional
119
+ 'audio' or 'speech', by default 'audio'
120
+
121
+ Returns
122
+ -------
123
+ Tensor[float]
124
+ ViSQOL score (MOS-LQO)
125
+ """
126
+ from visqol import visqol_lib_py
127
+ from visqol.pb2 import visqol_config_pb2
128
+ from visqol.pb2 import similarity_result_pb2
129
+
130
+ config = visqol_config_pb2.VisqolConfig()
131
+ if mode == "audio":
132
+ target_sr = 48000
133
+ config.options.use_speech_scoring = False
134
+ svr_model_path = "libsvm_nu_svr_model.txt"
135
+ elif mode == "speech":
136
+ target_sr = 16000
137
+ config.options.use_speech_scoring = True
138
+ svr_model_path = "lattice_tcditugenmeetpackhref_ls2_nl60_lr12_bs2048_learn.005_ep2400_train1_7_raw.tflite"
139
+ else:
140
+ raise ValueError(f"Unrecognized mode: {mode}")
141
+ config.audio.sample_rate = target_sr
142
+ config.options.svr_model_path = os.path.join(
143
+ os.path.dirname(visqol_lib_py.__file__), "model", svr_model_path
144
+ )
145
+
146
+ api = visqol_lib_py.VisqolApi()
147
+ api.Create(config)
148
+
149
+ estimates = estimates.clone().to_mono().resample(target_sr)
150
+ references = references.clone().to_mono().resample(target_sr)
151
+
152
+ visqols = []
153
+ for i in range(estimates.batch_size):
154
+ _visqol = api.Measure(
155
+ references.audio_data[i, 0].detach().cpu().numpy().astype(float),
156
+ estimates.audio_data[i, 0].detach().cpu().numpy().astype(float),
157
+ )
158
+ visqols.append(_visqol.moslqo)
159
+ return torch.from_numpy(np.array(visqols))
audiotools/metrics/spectral.py ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import typing
2
+ from typing import List
3
+
4
+ import numpy as np
5
+ from torch import nn
6
+
7
+ from .. import AudioSignal
8
+ from .. import STFTParams
9
+
10
+
11
+ class MultiScaleSTFTLoss(nn.Module):
12
+ """Computes the multi-scale STFT loss from [1].
13
+
14
+ Parameters
15
+ ----------
16
+ window_lengths : List[int], optional
17
+ Length of each window of each STFT, by default [2048, 512]
18
+ loss_fn : typing.Callable, optional
19
+ How to compare each loss, by default nn.L1Loss()
20
+ clamp_eps : float, optional
21
+ Clamp on the log magnitude, below, by default 1e-5
22
+ mag_weight : float, optional
23
+ Weight of raw magnitude portion of loss, by default 1.0
24
+ log_weight : float, optional
25
+ Weight of log magnitude portion of loss, by default 1.0
26
+ pow : float, optional
27
+ Power to raise magnitude to before taking log, by default 2.0
28
+ weight : float, optional
29
+ Weight of this loss, by default 1.0
30
+ match_stride : bool, optional
31
+ Whether to match the stride of convolutional layers, by default False
32
+
33
+ References
34
+ ----------
35
+
36
+ 1. Engel, Jesse, Chenjie Gu, and Adam Roberts.
37
+ "DDSP: Differentiable Digital Signal Processing."
38
+ International Conference on Learning Representations. 2019.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ window_lengths: List[int] = [2048, 512],
44
+ loss_fn: typing.Callable = nn.L1Loss(),
45
+ clamp_eps: float = 1e-5,
46
+ mag_weight: float = 1.0,
47
+ log_weight: float = 1.0,
48
+ pow: float = 2.0,
49
+ weight: float = 1.0,
50
+ match_stride: bool = False,
51
+ window_type: str = None,
52
+ ):
53
+ super().__init__()
54
+ self.stft_params = [
55
+ STFTParams(
56
+ window_length=w,
57
+ hop_length=w // 4,
58
+ match_stride=match_stride,
59
+ window_type=window_type,
60
+ )
61
+ for w in window_lengths
62
+ ]
63
+ self.loss_fn = loss_fn
64
+ self.log_weight = log_weight
65
+ self.mag_weight = mag_weight
66
+ self.clamp_eps = clamp_eps
67
+ self.weight = weight
68
+ self.pow = pow
69
+
70
+ def forward(self, x: AudioSignal, y: AudioSignal):
71
+ """Computes multi-scale STFT between an estimate and a reference
72
+ signal.
73
+
74
+ Parameters
75
+ ----------
76
+ x : AudioSignal
77
+ Estimate signal
78
+ y : AudioSignal
79
+ Reference signal
80
+
81
+ Returns
82
+ -------
83
+ torch.Tensor
84
+ Multi-scale STFT loss.
85
+ """
86
+ loss = 0.0
87
+ for s in self.stft_params:
88
+ x.stft(s.window_length, s.hop_length, s.window_type)
89
+ y.stft(s.window_length, s.hop_length, s.window_type)
90
+ loss += self.log_weight * self.loss_fn(
91
+ x.magnitude.clamp(self.clamp_eps).pow(self.pow).log10(),
92
+ y.magnitude.clamp(self.clamp_eps).pow(self.pow).log10(),
93
+ )
94
+ loss += self.mag_weight * self.loss_fn(x.magnitude, y.magnitude)
95
+ return loss
96
+
97
+
98
+ class MelSpectrogramLoss(nn.Module):
99
+ """Compute distance between mel spectrograms. Can be used
100
+ in a multi-scale way.
101
+
102
+ Parameters
103
+ ----------
104
+ n_mels : List[int]
105
+ Number of mels per STFT, by default [150, 80],
106
+ window_lengths : List[int], optional
107
+ Length of each window of each STFT, by default [2048, 512]
108
+ loss_fn : typing.Callable, optional
109
+ How to compare each loss, by default nn.L1Loss()
110
+ clamp_eps : float, optional
111
+ Clamp on the log magnitude, below, by default 1e-5
112
+ mag_weight : float, optional
113
+ Weight of raw magnitude portion of loss, by default 1.0
114
+ log_weight : float, optional
115
+ Weight of log magnitude portion of loss, by default 1.0
116
+ pow : float, optional
117
+ Power to raise magnitude to before taking log, by default 2.0
118
+ weight : float, optional
119
+ Weight of this loss, by default 1.0
120
+ match_stride : bool, optional
121
+ Whether to match the stride of convolutional layers, by default False
122
+ """
123
+
124
+ def __init__(
125
+ self,
126
+ n_mels: List[int] = [150, 80],
127
+ window_lengths: List[int] = [2048, 512],
128
+ loss_fn: typing.Callable = nn.L1Loss(),
129
+ clamp_eps: float = 1e-5,
130
+ mag_weight: float = 1.0,
131
+ log_weight: float = 1.0,
132
+ pow: float = 2.0,
133
+ weight: float = 1.0,
134
+ match_stride: bool = False,
135
+ mel_fmin: List[float] = [0.0, 0.0],
136
+ mel_fmax: List[float] = [None, None],
137
+ window_type: str = None,
138
+ ):
139
+ super().__init__()
140
+ self.stft_params = [
141
+ STFTParams(
142
+ window_length=w,
143
+ hop_length=w // 4,
144
+ match_stride=match_stride,
145
+ window_type=window_type,
146
+ )
147
+ for w in window_lengths
148
+ ]
149
+ self.n_mels = n_mels
150
+ self.loss_fn = loss_fn
151
+ self.clamp_eps = clamp_eps
152
+ self.log_weight = log_weight
153
+ self.mag_weight = mag_weight
154
+ self.weight = weight
155
+ self.mel_fmin = mel_fmin
156
+ self.mel_fmax = mel_fmax
157
+ self.pow = pow
158
+
159
+ def forward(self, x: AudioSignal, y: AudioSignal):
160
+ """Computes mel loss between an estimate and a reference
161
+ signal.
162
+
163
+ Parameters
164
+ ----------
165
+ x : AudioSignal
166
+ Estimate signal
167
+ y : AudioSignal
168
+ Reference signal
169
+
170
+ Returns
171
+ -------
172
+ torch.Tensor
173
+ Mel loss.
174
+ """
175
+ loss = 0.0
176
+ for n_mels, fmin, fmax, s in zip(
177
+ self.n_mels, self.mel_fmin, self.mel_fmax, self.stft_params
178
+ ):
179
+ kwargs = {
180
+ "window_length": s.window_length,
181
+ "hop_length": s.hop_length,
182
+ "window_type": s.window_type,
183
+ }
184
+ x_mels = x.mel_spectrogram(n_mels, mel_fmin=fmin, mel_fmax=fmax, **kwargs)
185
+ y_mels = y.mel_spectrogram(n_mels, mel_fmin=fmin, mel_fmax=fmax, **kwargs)
186
+
187
+ loss += self.log_weight * self.loss_fn(
188
+ x_mels.clamp(self.clamp_eps).pow(self.pow).log10(),
189
+ y_mels.clamp(self.clamp_eps).pow(self.pow).log10(),
190
+ )
191
+ loss += self.mag_weight * self.loss_fn(x_mels, y_mels)
192
+ return loss
193
+
194
+
195
+ class PhaseLoss(nn.Module):
196
+ """Difference between phase spectrograms.
197
+
198
+ Parameters
199
+ ----------
200
+ window_length : int, optional
201
+ Length of STFT window, by default 2048
202
+ hop_length : int, optional
203
+ Hop length of STFT window, by default 512
204
+ weight : float, optional
205
+ Weight of loss, by default 1.0
206
+ """
207
+
208
+ def __init__(
209
+ self, window_length: int = 2048, hop_length: int = 512, weight: float = 1.0
210
+ ):
211
+ super().__init__()
212
+
213
+ self.weight = weight
214
+ self.stft_params = STFTParams(window_length, hop_length)
215
+
216
+ def forward(self, x: AudioSignal, y: AudioSignal):
217
+ """Computes phase loss between an estimate and a reference
218
+ signal.
219
+
220
+ Parameters
221
+ ----------
222
+ x : AudioSignal
223
+ Estimate signal
224
+ y : AudioSignal
225
+ Reference signal
226
+
227
+ Returns
228
+ -------
229
+ torch.Tensor
230
+ Phase loss.
231
+ """
232
+ s = self.stft_params
233
+ x.stft(s.window_length, s.hop_length, s.window_type)
234
+ y.stft(s.window_length, s.hop_length, s.window_type)
235
+
236
+ # Take circular difference
237
+ diff = x.phase - y.phase
238
+ diff[diff < -np.pi] += 2 * np.pi
239
+ diff[diff > np.pi] -= -2 * np.pi
240
+
241
+ # Scale true magnitude to weights in [0, 1]
242
+ x_min, x_max = x.magnitude.min(), x.magnitude.max()
243
+ weights = (x.magnitude - x_min) / (x_max - x_min)
244
+
245
+ # Take weighted mean of all phase errors
246
+ loss = ((weights * diff) ** 2).mean()
247
+ return loss
audiotools/ml/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from . import decorators
2
+ from . import layers
3
+ from .accelerator import Accelerator
4
+ from .experiment import Experiment
5
+ from .layers import BaseModel
audiotools/ml/accelerator.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import typing
3
+
4
+ import torch
5
+ import torch.distributed as dist
6
+ from torch.nn.parallel import DataParallel
7
+ from torch.nn.parallel import DistributedDataParallel
8
+
9
+ from ..data.datasets import ResumableDistributedSampler as DistributedSampler
10
+ from ..data.datasets import ResumableSequentialSampler as SequentialSampler
11
+
12
+
13
+ class Accelerator: # pragma: no cover
14
+ """This class is used to prepare models and dataloaders for
15
+ usage with DDP or DP. Use the functions prepare_model, prepare_dataloader to
16
+ prepare the respective objects. In the case of models, they are moved to
17
+ the appropriate GPU and SyncBatchNorm is applied to them. In the case of
18
+ dataloaders, a sampler is created and the dataloader is initialized with
19
+ that sampler.
20
+
21
+ If the world size is 1, prepare_model and prepare_dataloader are
22
+ no-ops. If the environment variable ``LOCAL_RANK`` is not set, then the
23
+ script was launched without ``torchrun``, and ``DataParallel``
24
+ will be used instead of ``DistributedDataParallel`` (not recommended), if
25
+ the world size (number of GPUs) is greater than 1.
26
+
27
+ Parameters
28
+ ----------
29
+ amp : bool, optional
30
+ Whether or not to enable automatic mixed precision, by default False
31
+ """
32
+
33
+ def __init__(self, amp: bool = False):
34
+ local_rank = os.getenv("LOCAL_RANK", None)
35
+ self.world_size = torch.cuda.device_count()
36
+
37
+ self.use_ddp = self.world_size > 1 and local_rank is not None
38
+ self.use_dp = self.world_size > 1 and local_rank is None
39
+ self.device = "cpu" if self.world_size == 0 else "cuda"
40
+
41
+ if self.use_ddp:
42
+ local_rank = int(local_rank)
43
+ dist.init_process_group(
44
+ "nccl",
45
+ init_method="env://",
46
+ world_size=self.world_size,
47
+ rank=local_rank,
48
+ )
49
+
50
+ self.local_rank = 0 if local_rank is None else local_rank
51
+ self.amp = amp
52
+
53
+ class DummyScaler:
54
+ def __init__(self):
55
+ pass
56
+
57
+ def step(self, optimizer):
58
+ optimizer.step()
59
+
60
+ def scale(self, loss):
61
+ return loss
62
+
63
+ def unscale_(self, optimizer):
64
+ return optimizer
65
+
66
+ def update(self):
67
+ pass
68
+
69
+ self.scaler = torch.cuda.amp.GradScaler() if amp else DummyScaler()
70
+ self.device_ctx = (
71
+ torch.cuda.device(self.local_rank) if torch.cuda.is_available() else None
72
+ )
73
+
74
+ def __enter__(self):
75
+ if self.device_ctx is not None:
76
+ self.device_ctx.__enter__()
77
+ return self
78
+
79
+ def __exit__(self, exc_type, exc_value, traceback):
80
+ if self.device_ctx is not None:
81
+ self.device_ctx.__exit__(exc_type, exc_value, traceback)
82
+
83
+ def prepare_model(self, model: torch.nn.Module, **kwargs):
84
+ """Prepares model for DDP or DP. The model is moved to
85
+ the device of the correct rank.
86
+
87
+ Parameters
88
+ ----------
89
+ model : torch.nn.Module
90
+ Model that is converted for DDP or DP.
91
+
92
+ Returns
93
+ -------
94
+ torch.nn.Module
95
+ Wrapped model, or original model if DDP and DP are turned off.
96
+ """
97
+ model = model.to(self.device)
98
+ if self.use_ddp:
99
+ model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model)
100
+ model = DistributedDataParallel(
101
+ model, device_ids=[self.local_rank], **kwargs
102
+ )
103
+ elif self.use_dp:
104
+ model = DataParallel(model, **kwargs)
105
+ return model
106
+
107
+ # Automatic mixed-precision utilities
108
+ def autocast(self, *args, **kwargs):
109
+ """Context manager for autocasting. Arguments
110
+ go to ``torch.cuda.amp.autocast``.
111
+ """
112
+ return torch.cuda.amp.autocast(self.amp, *args, **kwargs)
113
+
114
+ def backward(self, loss: torch.Tensor):
115
+ """Backwards pass, after scaling the loss if ``amp`` is
116
+ enabled.
117
+
118
+ Parameters
119
+ ----------
120
+ loss : torch.Tensor
121
+ Loss value.
122
+ """
123
+ self.scaler.scale(loss).backward()
124
+
125
+ def step(self, optimizer: torch.optim.Optimizer):
126
+ """Steps the optimizer, using a ``scaler`` if ``amp`` is
127
+ enabled.
128
+
129
+ Parameters
130
+ ----------
131
+ optimizer : torch.optim.Optimizer
132
+ Optimizer to step forward.
133
+ """
134
+ self.scaler.step(optimizer)
135
+
136
+ def update(self):
137
+ """Updates the scale factor."""
138
+ self.scaler.update()
139
+
140
+ def prepare_dataloader(
141
+ self, dataset: typing.Iterable, start_idx: int = None, **kwargs
142
+ ):
143
+ """Wraps a dataset with a DataLoader, using the correct sampler if DDP is
144
+ enabled.
145
+
146
+ Parameters
147
+ ----------
148
+ dataset : typing.Iterable
149
+ Dataset to build Dataloader around.
150
+ start_idx : int, optional
151
+ Start index of sampler, useful if resuming from some epoch,
152
+ by default None
153
+
154
+ Returns
155
+ -------
156
+ _type_
157
+ _description_
158
+ """
159
+
160
+ if self.use_ddp:
161
+ sampler = DistributedSampler(
162
+ dataset,
163
+ start_idx,
164
+ num_replicas=self.world_size,
165
+ rank=self.local_rank,
166
+ )
167
+ if "num_workers" in kwargs:
168
+ kwargs["num_workers"] = max(kwargs["num_workers"] // self.world_size, 1)
169
+ kwargs["batch_size"] = max(kwargs["batch_size"] // self.world_size, 1)
170
+ else:
171
+ sampler = SequentialSampler(dataset, start_idx)
172
+
173
+ dataloader = torch.utils.data.DataLoader(dataset, sampler=sampler, **kwargs)
174
+ return dataloader
175
+
176
+ @staticmethod
177
+ def unwrap(model):
178
+ """Unwraps the model if it was wrapped in DDP or DP, otherwise
179
+ just returns the model. Use this to unwrap the model returned by
180
+ :py:func:`audiotools.ml.accelerator.Accelerator.prepare_model`.
181
+ """
182
+ if hasattr(model, "module"):
183
+ return model.module
184
+ return model
audiotools/ml/decorators.py ADDED
@@ -0,0 +1,440 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import os
3
+ import time
4
+ from collections import defaultdict
5
+ from functools import wraps
6
+
7
+ import torch
8
+ import torch.distributed as dist
9
+ from rich import box
10
+ from rich.console import Console
11
+ from rich.console import Group
12
+ from rich.live import Live
13
+ from rich.markdown import Markdown
14
+ from rich.padding import Padding
15
+ from rich.panel import Panel
16
+ from rich.progress import BarColumn
17
+ from rich.progress import Progress
18
+ from rich.progress import SpinnerColumn
19
+ from rich.progress import TimeElapsedColumn
20
+ from rich.progress import TimeRemainingColumn
21
+ from rich.rule import Rule
22
+ from rich.table import Table
23
+ from torch.utils.tensorboard import SummaryWriter
24
+
25
+
26
+ # This is here so that the history can be pickled.
27
+ def default_list():
28
+ return []
29
+
30
+
31
+ class Mean:
32
+ """Keeps track of the running mean, along with the latest
33
+ value.
34
+ """
35
+
36
+ def __init__(self):
37
+ self.reset()
38
+
39
+ def __call__(self):
40
+ mean = self.total / max(self.count, 1)
41
+ return mean
42
+
43
+ def reset(self):
44
+ self.count = 0
45
+ self.total = 0
46
+
47
+ def update(self, val):
48
+ if math.isfinite(val):
49
+ self.count += 1
50
+ self.total += val
51
+
52
+
53
+ def when(condition):
54
+ """Runs a function only when the condition is met. The condition is
55
+ a function that is run.
56
+
57
+ Parameters
58
+ ----------
59
+ condition : Callable
60
+ Function to run to check whether or not to run the decorated
61
+ function.
62
+
63
+ Example
64
+ -------
65
+ Checkpoint only runs every 100 iterations, and only if the
66
+ local rank is 0.
67
+
68
+ >>> i = 0
69
+ >>> rank = 0
70
+ >>>
71
+ >>> @when(lambda: i % 100 == 0 and rank == 0)
72
+ >>> def checkpoint():
73
+ >>> print("Saving to /runs/exp1")
74
+ >>>
75
+ >>> for i in range(1000):
76
+ >>> checkpoint()
77
+
78
+ """
79
+
80
+ def decorator(fn):
81
+ @wraps(fn)
82
+ def decorated(*args, **kwargs):
83
+ if condition():
84
+ return fn(*args, **kwargs)
85
+
86
+ return decorated
87
+
88
+ return decorator
89
+
90
+
91
+ def timer(prefix: str = "time"):
92
+ """Adds execution time to the output dictionary of the decorated
93
+ function. The function decorated by this must output a dictionary.
94
+ The key added will follow the form "[prefix]/[name_of_function]"
95
+
96
+ Parameters
97
+ ----------
98
+ prefix : str, optional
99
+ The key added will follow the form "[prefix]/[name_of_function]",
100
+ by default "time".
101
+ """
102
+
103
+ def decorator(fn):
104
+ @wraps(fn)
105
+ def decorated(*args, **kwargs):
106
+ s = time.perf_counter()
107
+ output = fn(*args, **kwargs)
108
+ assert isinstance(output, dict)
109
+ e = time.perf_counter()
110
+ output[f"{prefix}/{fn.__name__}"] = e - s
111
+ return output
112
+
113
+ return decorated
114
+
115
+ return decorator
116
+
117
+
118
+ class Tracker:
119
+ """
120
+ A tracker class that helps to monitor the progress of training and logging the metrics.
121
+
122
+ Attributes
123
+ ----------
124
+ metrics : dict
125
+ A dictionary containing the metrics for each label.
126
+ history : dict
127
+ A dictionary containing the history of metrics for each label.
128
+ writer : SummaryWriter
129
+ A SummaryWriter object for logging the metrics.
130
+ rank : int
131
+ The rank of the current process.
132
+ step : int
133
+ The current step of the training.
134
+ tasks : dict
135
+ A dictionary containing the progress bars and tables for each label.
136
+ pbar : Progress
137
+ A progress bar object for displaying the progress.
138
+ consoles : list
139
+ A list of console objects for logging.
140
+ live : Live
141
+ A Live object for updating the display live.
142
+
143
+ Methods
144
+ -------
145
+ print(msg: str)
146
+ Prints the given message to all consoles.
147
+ update(label: str, fn_name: str)
148
+ Updates the progress bar and table for the given label.
149
+ done(label: str, title: str)
150
+ Resets the progress bar and table for the given label and prints the final result.
151
+ track(label: str, length: int, completed: int = 0, op: dist.ReduceOp = dist.ReduceOp.AVG, ddp_active: bool = "LOCAL_RANK" in os.environ)
152
+ A decorator for tracking the progress and metrics of a function.
153
+ log(label: str, value_type: str = "value", history: bool = True)
154
+ A decorator for logging the metrics of a function.
155
+ is_best(label: str, key: str) -> bool
156
+ Checks if the latest value of the given key in the label is the best so far.
157
+ state_dict() -> dict
158
+ Returns a dictionary containing the state of the tracker.
159
+ load_state_dict(state_dict: dict) -> Tracker
160
+ Loads the state of the tracker from the given state dictionary.
161
+ """
162
+
163
+ def __init__(
164
+ self,
165
+ writer: SummaryWriter = None,
166
+ log_file: str = None,
167
+ rank: int = 0,
168
+ console_width: int = 100,
169
+ step: int = 0,
170
+ ):
171
+ """
172
+ Initializes the Tracker object.
173
+
174
+ Parameters
175
+ ----------
176
+ writer : SummaryWriter, optional
177
+ A SummaryWriter object for logging the metrics, by default None.
178
+ log_file : str, optional
179
+ The path to the log file, by default None.
180
+ rank : int, optional
181
+ The rank of the current process, by default 0.
182
+ console_width : int, optional
183
+ The width of the console, by default 100.
184
+ step : int, optional
185
+ The current step of the training, by default 0.
186
+ """
187
+ self.metrics = {}
188
+ self.history = {}
189
+ self.writer = writer
190
+ self.rank = rank
191
+ self.step = step
192
+
193
+ # Create progress bars etc.
194
+ self.tasks = {}
195
+ self.pbar = Progress(
196
+ SpinnerColumn(),
197
+ "[progress.description]{task.description}",
198
+ "{task.completed}/{task.total}",
199
+ BarColumn(),
200
+ TimeElapsedColumn(),
201
+ "/",
202
+ TimeRemainingColumn(),
203
+ )
204
+ self.consoles = [Console(width=console_width)]
205
+ self.live = Live(console=self.consoles[0], refresh_per_second=10)
206
+ if log_file is not None:
207
+ self.consoles.append(Console(width=console_width, file=open(log_file, "a")))
208
+
209
+ def print(self, msg):
210
+ """
211
+ Prints the given message to all consoles.
212
+
213
+ Parameters
214
+ ----------
215
+ msg : str
216
+ The message to be printed.
217
+ """
218
+ if self.rank == 0:
219
+ for c in self.consoles:
220
+ c.log(msg)
221
+
222
+ def update(self, label, fn_name):
223
+ """
224
+ Updates the progress bar and table for the given label.
225
+
226
+ Parameters
227
+ ----------
228
+ label : str
229
+ The label of the progress bar and table to be updated.
230
+ fn_name : str
231
+ The name of the function associated with the label.
232
+ """
233
+ if self.rank == 0:
234
+ self.pbar.advance(self.tasks[label]["pbar"])
235
+
236
+ # Create table
237
+ table = Table(title=label, expand=True, box=box.MINIMAL)
238
+ table.add_column("key", style="cyan")
239
+ table.add_column("value", style="bright_blue")
240
+ table.add_column("mean", style="bright_green")
241
+
242
+ keys = self.metrics[label]["value"].keys()
243
+ for k in keys:
244
+ value = self.metrics[label]["value"][k]
245
+ mean = self.metrics[label]["mean"][k]()
246
+ table.add_row(k, f"{value:10.6f}", f"{mean:10.6f}")
247
+
248
+ self.tasks[label]["table"] = table
249
+ tables = [t["table"] for t in self.tasks.values()]
250
+ group = Group(*tables, self.pbar)
251
+ self.live.update(
252
+ Group(
253
+ Padding("", (0, 0)),
254
+ Rule(f"[italic]{fn_name}()", style="white"),
255
+ Padding("", (0, 0)),
256
+ Panel.fit(
257
+ group, padding=(0, 5), title="[b]Progress", border_style="blue"
258
+ ),
259
+ )
260
+ )
261
+
262
+ def done(self, label: str, title: str):
263
+ """
264
+ Resets the progress bar and table for the given label and prints the final result.
265
+
266
+ Parameters
267
+ ----------
268
+ label : str
269
+ The label of the progress bar and table to be reset.
270
+ title : str
271
+ The title to be displayed when printing the final result.
272
+ """
273
+ for label in self.metrics:
274
+ for v in self.metrics[label]["mean"].values():
275
+ v.reset()
276
+
277
+ if self.rank == 0:
278
+ self.pbar.reset(self.tasks[label]["pbar"])
279
+ tables = [t["table"] for t in self.tasks.values()]
280
+ group = Group(Markdown(f"# {title}"), *tables, self.pbar)
281
+ self.print(group)
282
+
283
+ def track(
284
+ self,
285
+ label: str,
286
+ length: int,
287
+ completed: int = 0,
288
+ op: dist.ReduceOp = dist.ReduceOp.AVG,
289
+ ddp_active: bool = "LOCAL_RANK" in os.environ,
290
+ ):
291
+ """
292
+ A decorator for tracking the progress and metrics of a function.
293
+
294
+ Parameters
295
+ ----------
296
+ label : str
297
+ The label to be associated with the progress and metrics.
298
+ length : int
299
+ The total number of iterations to be completed.
300
+ completed : int, optional
301
+ The number of iterations already completed, by default 0.
302
+ op : dist.ReduceOp, optional
303
+ The reduce operation to be used, by default dist.ReduceOp.AVG.
304
+ ddp_active : bool, optional
305
+ Whether the DistributedDataParallel is active, by default "LOCAL_RANK" in os.environ.
306
+ """
307
+ self.tasks[label] = {
308
+ "pbar": self.pbar.add_task(
309
+ f"[white]Iteration ({label})", total=length, completed=completed
310
+ ),
311
+ "table": Table(),
312
+ }
313
+ self.metrics[label] = {
314
+ "value": defaultdict(),
315
+ "mean": defaultdict(lambda: Mean()),
316
+ }
317
+
318
+ def decorator(fn):
319
+ @wraps(fn)
320
+ def decorated(*args, **kwargs):
321
+ output = fn(*args, **kwargs)
322
+ if not isinstance(output, dict):
323
+ self.update(label, fn.__name__)
324
+ return output
325
+ # Collect across all DDP processes
326
+ scalar_keys = []
327
+ for k, v in output.items():
328
+ if isinstance(v, (int, float)):
329
+ v = torch.tensor([v])
330
+ if not torch.is_tensor(v):
331
+ continue
332
+ if ddp_active and v.is_cuda: # pragma: no cover
333
+ dist.all_reduce(v, op=op)
334
+ output[k] = v.detach()
335
+ if torch.numel(v) == 1:
336
+ scalar_keys.append(k)
337
+ output[k] = v.item()
338
+
339
+ # Save the outputs to tracker
340
+ for k, v in output.items():
341
+ if k not in scalar_keys:
342
+ continue
343
+ self.metrics[label]["value"][k] = v
344
+ # Update the running mean
345
+ self.metrics[label]["mean"][k].update(v)
346
+
347
+ self.update(label, fn.__name__)
348
+ return output
349
+
350
+ return decorated
351
+
352
+ return decorator
353
+
354
+ def log(self, label: str, value_type: str = "value", history: bool = True):
355
+ """
356
+ A decorator for logging the metrics of a function.
357
+
358
+ Parameters
359
+ ----------
360
+ label : str
361
+ The label to be associated with the logging.
362
+ value_type : str, optional
363
+ The type of value to be logged, by default "value".
364
+ history : bool, optional
365
+ Whether to save the history of the metrics, by default True.
366
+ """
367
+ assert value_type in ["mean", "value"]
368
+ if history:
369
+ if label not in self.history:
370
+ self.history[label] = defaultdict(default_list)
371
+
372
+ def decorator(fn):
373
+ @wraps(fn)
374
+ def decorated(*args, **kwargs):
375
+ output = fn(*args, **kwargs)
376
+ if self.rank == 0:
377
+ nonlocal value_type, label
378
+ metrics = self.metrics[label][value_type]
379
+ for k, v in metrics.items():
380
+ v = v() if isinstance(v, Mean) else v
381
+ if self.writer is not None:
382
+ self.writer.add_scalar(f"{k}/{label}", v, self.step)
383
+ if label in self.history:
384
+ self.history[label][k].append(v)
385
+
386
+ if label in self.history:
387
+ self.history[label]["step"].append(self.step)
388
+
389
+ return output
390
+
391
+ return decorated
392
+
393
+ return decorator
394
+
395
+ def is_best(self, label, key):
396
+ """
397
+ Checks if the latest value of the given key in the label is the best so far.
398
+
399
+ Parameters
400
+ ----------
401
+ label : str
402
+ The label of the metrics to be checked.
403
+ key : str
404
+ The key of the metric to be checked.
405
+
406
+ Returns
407
+ -------
408
+ bool
409
+ True if the latest value is the best so far, otherwise False.
410
+ """
411
+ return self.history[label][key][-1] == min(self.history[label][key])
412
+
413
+ def state_dict(self):
414
+ """
415
+ Returns a dictionary containing the state of the tracker.
416
+
417
+ Returns
418
+ -------
419
+ dict
420
+ A dictionary containing the history and step of the tracker.
421
+ """
422
+ return {"history": self.history, "step": self.step}
423
+
424
+ def load_state_dict(self, state_dict):
425
+ """
426
+ Loads the state of the tracker from the given state dictionary.
427
+
428
+ Parameters
429
+ ----------
430
+ state_dict : dict
431
+ A dictionary containing the history and step of the tracker.
432
+
433
+ Returns
434
+ -------
435
+ Tracker
436
+ The tracker object with the loaded state.
437
+ """
438
+ self.history = state_dict["history"]
439
+ self.step = state_dict["step"]
440
+ return self
audiotools/ml/experiment.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Useful class for Experiment tracking, and ensuring code is
3
+ saved alongside files.
4
+ """ # fmt: skip
5
+ import datetime
6
+ import os
7
+ import shlex
8
+ import shutil
9
+ import subprocess
10
+ import typing
11
+ from pathlib import Path
12
+
13
+ import randomname
14
+
15
+
16
+ class Experiment:
17
+ """This class contains utilities for managing experiments.
18
+ It is a context manager, that when you enter it, changes
19
+ your directory to a specified experiment folder (which
20
+ optionally can have an automatically generated experiment
21
+ name, or a specified one), and changes the CUDA device used
22
+ to the specified device (or devices).
23
+
24
+ Parameters
25
+ ----------
26
+ exp_directory : str
27
+ Folder where all experiments are saved, by default "runs/".
28
+ exp_name : str, optional
29
+ Name of the experiment, by default uses the current time, date, and
30
+ hostname to save.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ exp_directory: str = "runs/",
36
+ exp_name: str = None,
37
+ ):
38
+ if exp_name is None:
39
+ exp_name = self.generate_exp_name()
40
+ exp_dir = Path(exp_directory) / exp_name
41
+ exp_dir.mkdir(parents=True, exist_ok=True)
42
+
43
+ self.exp_dir = exp_dir
44
+ self.exp_name = exp_name
45
+ self.git_tracked_files = (
46
+ subprocess.check_output(
47
+ shlex.split("git ls-tree --full-tree --name-only -r HEAD")
48
+ )
49
+ .decode("utf-8")
50
+ .splitlines()
51
+ )
52
+ self.parent_directory = Path(".").absolute()
53
+
54
+ def __enter__(self):
55
+ self.prev_dir = os.getcwd()
56
+ os.chdir(self.exp_dir)
57
+ return self
58
+
59
+ def __exit__(self, exc_type, exc_value, traceback):
60
+ os.chdir(self.prev_dir)
61
+
62
+ @staticmethod
63
+ def generate_exp_name():
64
+ """Generates a random experiment name based on the date
65
+ and a randomly generated adjective-noun tuple.
66
+
67
+ Returns
68
+ -------
69
+ str
70
+ Randomly generated experiment name.
71
+ """
72
+ date = datetime.datetime.now().strftime("%y%m%d")
73
+ name = f"{date}-{randomname.get_name()}"
74
+ return name
75
+
76
+ def snapshot(self, filter_fn: typing.Callable = lambda f: True):
77
+ """Captures a full snapshot of all the files tracked by git at the time
78
+ the experiment is run. It also captures the diff against the committed
79
+ code as a separate file.
80
+
81
+ Parameters
82
+ ----------
83
+ filter_fn : typing.Callable, optional
84
+ Function that can be used to exclude some files
85
+ from the snapshot, by default accepts all files
86
+ """
87
+ for f in self.git_tracked_files:
88
+ if filter_fn(f):
89
+ Path(f).parent.mkdir(parents=True, exist_ok=True)
90
+ shutil.copyfile(self.parent_directory / f, f)
audiotools/ml/layers/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from .base import BaseModel
2
+ from .spectral_gate import SpectralGate
audiotools/ml/layers/base.py ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import inspect
2
+ import shutil
3
+ import tempfile
4
+ import typing
5
+ from pathlib import Path
6
+
7
+ import torch
8
+ from torch import nn
9
+
10
+
11
+ class BaseModel(nn.Module):
12
+ """This is a class that adds useful save/load functionality to a
13
+ ``torch.nn.Module`` object. ``BaseModel`` objects can be saved
14
+ as ``torch.package`` easily, making them super easy to port between
15
+ machines without requiring a ton of dependencies. Files can also be
16
+ saved as just weights, in the standard way.
17
+
18
+ >>> class Model(ml.BaseModel):
19
+ >>> def __init__(self, arg1: float = 1.0):
20
+ >>> super().__init__()
21
+ >>> self.arg1 = arg1
22
+ >>> self.linear = nn.Linear(1, 1)
23
+ >>>
24
+ >>> def forward(self, x):
25
+ >>> return self.linear(x)
26
+ >>>
27
+ >>> model1 = Model()
28
+ >>>
29
+ >>> with tempfile.NamedTemporaryFile(suffix=".pth") as f:
30
+ >>> model1.save(
31
+ >>> f.name,
32
+ >>> )
33
+ >>> model2 = Model.load(f.name)
34
+ >>> out2 = seed_and_run(model2, x)
35
+ >>> assert torch.allclose(out1, out2)
36
+ >>>
37
+ >>> model1.save(f.name, package=True)
38
+ >>> model2 = Model.load(f.name)
39
+ >>> model2.save(f.name, package=False)
40
+ >>> model3 = Model.load(f.name)
41
+ >>> out3 = seed_and_run(model3, x)
42
+ >>>
43
+ >>> with tempfile.TemporaryDirectory() as d:
44
+ >>> model1.save_to_folder(d, {"data": 1.0})
45
+ >>> Model.load_from_folder(d)
46
+
47
+ """
48
+
49
+ EXTERN = [
50
+ "audiotools.**",
51
+ "tqdm",
52
+ "__main__",
53
+ "numpy.**",
54
+ "julius.**",
55
+ "torchaudio.**",
56
+ "scipy.**",
57
+ "einops",
58
+ ]
59
+ """Names of libraries that are external to the torch.package saving mechanism.
60
+ Source code from these libraries will not be packaged into the model. This can
61
+ be edited by the user of this class by editing ``model.EXTERN``."""
62
+ INTERN = []
63
+ """Names of libraries that are internal to the torch.package saving mechanism.
64
+ Source code from these libraries will be saved alongside the model."""
65
+
66
+ def save(
67
+ self,
68
+ path: str,
69
+ metadata: dict = None,
70
+ package: bool = True,
71
+ intern: list = [],
72
+ extern: list = [],
73
+ mock: list = [],
74
+ ):
75
+ """Saves the model, either as a torch package, or just as
76
+ weights, alongside some specified metadata.
77
+
78
+ Parameters
79
+ ----------
80
+ path : str
81
+ Path to save model to.
82
+ metadata : dict, optional
83
+ Any metadata to save alongside the model,
84
+ by default None
85
+ package : bool, optional
86
+ Whether to use ``torch.package`` to save the model in
87
+ a format that is portable, by default True
88
+ intern : list, optional
89
+ List of additional libraries that are internal
90
+ to the model, used with torch.package, by default []
91
+ extern : list, optional
92
+ List of additional libraries that are external to
93
+ the model, used with torch.package, by default []
94
+ mock : list, optional
95
+ List of libraries to mock, used with torch.package,
96
+ by default []
97
+
98
+ Returns
99
+ -------
100
+ str
101
+ Path to saved model.
102
+ """
103
+ sig = inspect.signature(self.__class__)
104
+ args = {}
105
+
106
+ for key, val in sig.parameters.items():
107
+ arg_val = val.default
108
+ if arg_val is not inspect.Parameter.empty:
109
+ args[key] = arg_val
110
+
111
+ # Look up attibutes in self, and if any of them are in args,
112
+ # overwrite them in args.
113
+ for attribute in dir(self):
114
+ if attribute in args:
115
+ args[attribute] = getattr(self, attribute)
116
+
117
+ metadata = {} if metadata is None else metadata
118
+ metadata["kwargs"] = args
119
+ if not hasattr(self, "metadata"):
120
+ self.metadata = {}
121
+ self.metadata.update(metadata)
122
+
123
+ if not package:
124
+ state_dict = {"state_dict": self.state_dict(), "metadata": metadata}
125
+ torch.save(state_dict, path)
126
+ else:
127
+ self._save_package(path, intern=intern, extern=extern, mock=mock)
128
+
129
+ return path
130
+
131
+ @property
132
+ def device(self):
133
+ """Gets the device the model is on by looking at the device of
134
+ the first parameter. May not be valid if model is split across
135
+ multiple devices.
136
+ """
137
+ return list(self.parameters())[0].device
138
+
139
+ @classmethod
140
+ def load(
141
+ cls,
142
+ location: str,
143
+ *args,
144
+ package_name: str = None,
145
+ strict: bool = False,
146
+ **kwargs,
147
+ ):
148
+ """Load model from a path. Tries first to load as a package, and if
149
+ that fails, tries to load as weights. The arguments to the class are
150
+ specified inside the model weights file.
151
+
152
+ Parameters
153
+ ----------
154
+ location : str
155
+ Path to file.
156
+ package_name : str, optional
157
+ Name of package, by default ``cls.__name__``.
158
+ strict : bool, optional
159
+ Ignore unmatched keys, by default False
160
+ kwargs : dict
161
+ Additional keyword arguments to the model instantiation, if
162
+ not loading from package.
163
+
164
+ Returns
165
+ -------
166
+ BaseModel
167
+ A model that inherits from BaseModel.
168
+ """
169
+ try:
170
+ model = cls._load_package(location, package_name=package_name)
171
+ except:
172
+ model_dict = torch.load(location, "cpu")
173
+ metadata = model_dict["metadata"]
174
+ metadata["kwargs"].update(kwargs)
175
+
176
+ sig = inspect.signature(cls)
177
+ class_keys = list(sig.parameters.keys())
178
+ for k in list(metadata["kwargs"].keys()):
179
+ if k not in class_keys:
180
+ metadata["kwargs"].pop(k)
181
+
182
+ model = cls(*args, **metadata["kwargs"])
183
+ model.load_state_dict(model_dict["state_dict"], strict=strict)
184
+ model.metadata = metadata
185
+
186
+ return model
187
+
188
+ def _save_package(self, path, intern=[], extern=[], mock=[], **kwargs):
189
+ package_name = type(self).__name__
190
+ resource_name = f"{type(self).__name__}.pth"
191
+
192
+ # Below is for loading and re-saving a package.
193
+ if hasattr(self, "importer"):
194
+ kwargs["importer"] = (self.importer, torch.package.sys_importer)
195
+ del self.importer
196
+
197
+ # Why do we use a tempfile, you ask?
198
+ # It's so we can load a packaged model and then re-save
199
+ # it to the same location. torch.package throws an
200
+ # error if it's loading and writing to the same
201
+ # file (this is undocumented).
202
+ with tempfile.NamedTemporaryFile(suffix=".pth") as f:
203
+ with torch.package.PackageExporter(f.name, **kwargs) as exp:
204
+ exp.intern(self.INTERN + intern)
205
+ exp.mock(mock)
206
+ exp.extern(self.EXTERN + extern)
207
+ exp.save_pickle(package_name, resource_name, self)
208
+
209
+ if hasattr(self, "metadata"):
210
+ exp.save_pickle(
211
+ package_name, f"{package_name}.metadata", self.metadata
212
+ )
213
+
214
+ shutil.copyfile(f.name, path)
215
+
216
+ # Must reset the importer back to `self` if it existed
217
+ # so that you can save the model again!
218
+ if "importer" in kwargs:
219
+ self.importer = kwargs["importer"][0]
220
+ return path
221
+
222
+ @classmethod
223
+ def _load_package(cls, path, package_name=None):
224
+ package_name = cls.__name__ if package_name is None else package_name
225
+ resource_name = f"{package_name}.pth"
226
+
227
+ imp = torch.package.PackageImporter(path)
228
+ model = imp.load_pickle(package_name, resource_name, "cpu")
229
+ try:
230
+ model.metadata = imp.load_pickle(package_name, f"{package_name}.metadata")
231
+ except: # pragma: no cover
232
+ pass
233
+ model.importer = imp
234
+
235
+ return model
236
+
237
+ def save_to_folder(
238
+ self,
239
+ folder: typing.Union[str, Path],
240
+ extra_data: dict = None,
241
+ package: bool = True,
242
+ ):
243
+ """Dumps a model into a folder, as both a package
244
+ and as weights, as well as anything specified in
245
+ ``extra_data``. ``extra_data`` is a dictionary of other
246
+ pickleable files, with the keys being the paths
247
+ to save them in. The model is saved under a subfolder
248
+ specified by the name of the class (e.g. ``folder/generator/[package, weights].pth``
249
+ if the model name was ``Generator``).
250
+
251
+ >>> with tempfile.TemporaryDirectory() as d:
252
+ >>> extra_data = {
253
+ >>> "optimizer.pth": optimizer.state_dict()
254
+ >>> }
255
+ >>> model.save_to_folder(d, extra_data)
256
+ >>> Model.load_from_folder(d)
257
+
258
+ Parameters
259
+ ----------
260
+ folder : typing.Union[str, Path]
261
+ _description_
262
+ extra_data : dict, optional
263
+ _description_, by default None
264
+
265
+ Returns
266
+ -------
267
+ str
268
+ Path to folder
269
+ """
270
+ extra_data = {} if extra_data is None else extra_data
271
+ model_name = type(self).__name__.lower()
272
+ target_base = Path(f"{folder}/{model_name}/")
273
+ target_base.mkdir(exist_ok=True, parents=True)
274
+
275
+ if package:
276
+ package_path = target_base / f"package.pth"
277
+ self.save(package_path)
278
+
279
+ weights_path = target_base / f"weights.pth"
280
+ self.save(weights_path, package=False)
281
+
282
+ for path, obj in extra_data.items():
283
+ torch.save(obj, target_base / path)
284
+
285
+ return target_base
286
+
287
+ @classmethod
288
+ def load_from_folder(
289
+ cls,
290
+ folder: typing.Union[str, Path],
291
+ package: bool = True,
292
+ strict: bool = False,
293
+ **kwargs,
294
+ ):
295
+ """Loads the model from a folder generated by
296
+ :py:func:`audiotools.ml.layers.base.BaseModel.save_to_folder`.
297
+ Like that function, this one looks for a subfolder that has
298
+ the name of the class (e.g. ``folder/generator/[package, weights].pth`` if the
299
+ model name was ``Generator``).
300
+
301
+ Parameters
302
+ ----------
303
+ folder : typing.Union[str, Path]
304
+ _description_
305
+ package : bool, optional
306
+ Whether to use ``torch.package`` to load the model,
307
+ loading the model from ``package.pth``.
308
+ strict : bool, optional
309
+ Ignore unmatched keys, by default False
310
+
311
+ Returns
312
+ -------
313
+ tuple
314
+ tuple of model and extra data as saved by
315
+ :py:func:`audiotools.ml.layers.base.BaseModel.save_to_folder`.
316
+ """
317
+ folder = Path(folder) / cls.__name__.lower()
318
+ model_pth = "package.pth" if package else "weights.pth"
319
+ model_pth = folder / model_pth
320
+
321
+ model = cls.load(model_pth, strict=strict)
322
+ extra_data = {}
323
+ excluded = ["package.pth", "weights.pth"]
324
+ files = [x for x in folder.glob("*") if x.is_file() and x.name not in excluded]
325
+ for f in files:
326
+ extra_data[f.name] = torch.load(f, **kwargs)
327
+
328
+ return model, extra_data
audiotools/ml/layers/spectral_gate.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn.functional as F
3
+ from torch import nn
4
+
5
+ from ...core import AudioSignal
6
+ from ...core import STFTParams
7
+ from ...core import util
8
+
9
+
10
+ class SpectralGate(nn.Module):
11
+ """Spectral gating algorithm for noise reduction,
12
+ as in Audacity/Ocenaudio. The steps are as follows:
13
+
14
+ 1. An FFT is calculated over the noise audio clip
15
+ 2. Statistics are calculated over FFT of the the noise
16
+ (in frequency)
17
+ 3. A threshold is calculated based upon the statistics
18
+ of the noise (and the desired sensitivity of the algorithm)
19
+ 4. An FFT is calculated over the signal
20
+ 5. A mask is determined by comparing the signal FFT to the
21
+ threshold
22
+ 6. The mask is smoothed with a filter over frequency and time
23
+ 7. The mask is appled to the FFT of the signal, and is inverted
24
+
25
+ Implementation inspired by Tim Sainburg's noisereduce:
26
+
27
+ https://timsainburg.com/noise-reduction-python.html
28
+
29
+ Parameters
30
+ ----------
31
+ n_freq : int, optional
32
+ Number of frequency bins to smooth by, by default 3
33
+ n_time : int, optional
34
+ Number of time bins to smooth by, by default 5
35
+ """
36
+
37
+ def __init__(self, n_freq: int = 3, n_time: int = 5):
38
+ super().__init__()
39
+
40
+ smoothing_filter = torch.outer(
41
+ torch.cat(
42
+ [
43
+ torch.linspace(0, 1, n_freq + 2)[:-1],
44
+ torch.linspace(1, 0, n_freq + 2),
45
+ ]
46
+ )[..., 1:-1],
47
+ torch.cat(
48
+ [
49
+ torch.linspace(0, 1, n_time + 2)[:-1],
50
+ torch.linspace(1, 0, n_time + 2),
51
+ ]
52
+ )[..., 1:-1],
53
+ )
54
+ smoothing_filter = smoothing_filter / smoothing_filter.sum()
55
+ smoothing_filter = smoothing_filter.unsqueeze(0).unsqueeze(0)
56
+ self.register_buffer("smoothing_filter", smoothing_filter)
57
+
58
+ def forward(
59
+ self,
60
+ audio_signal: AudioSignal,
61
+ nz_signal: AudioSignal,
62
+ denoise_amount: float = 1.0,
63
+ n_std: float = 3.0,
64
+ win_length: int = 2048,
65
+ hop_length: int = 512,
66
+ ):
67
+ """Perform noise reduction.
68
+
69
+ Parameters
70
+ ----------
71
+ audio_signal : AudioSignal
72
+ Audio signal that noise will be removed from.
73
+ nz_signal : AudioSignal, optional
74
+ Noise signal to compute noise statistics from.
75
+ denoise_amount : float, optional
76
+ Amount to denoise by, by default 1.0
77
+ n_std : float, optional
78
+ Number of standard deviations above which to consider
79
+ noise, by default 3.0
80
+ win_length : int, optional
81
+ Length of window for STFT, by default 2048
82
+ hop_length : int, optional
83
+ Hop length for STFT, by default 512
84
+
85
+ Returns
86
+ -------
87
+ AudioSignal
88
+ Denoised audio signal.
89
+ """
90
+ stft_params = STFTParams(win_length, hop_length, "sqrt_hann")
91
+
92
+ audio_signal = audio_signal.clone()
93
+ audio_signal.stft_data = None
94
+ audio_signal.stft_params = stft_params
95
+
96
+ nz_signal = nz_signal.clone()
97
+ nz_signal.stft_params = stft_params
98
+
99
+ nz_stft_db = 20 * nz_signal.magnitude.clamp(1e-4).log10()
100
+ nz_freq_mean = nz_stft_db.mean(keepdim=True, dim=-1)
101
+ nz_freq_std = nz_stft_db.std(keepdim=True, dim=-1)
102
+
103
+ nz_thresh = nz_freq_mean + nz_freq_std * n_std
104
+
105
+ stft_db = 20 * audio_signal.magnitude.clamp(1e-4).log10()
106
+ nb, nac, nf, nt = stft_db.shape
107
+ db_thresh = nz_thresh.expand(nb, nac, -1, nt)
108
+
109
+ stft_mask = (stft_db < db_thresh).float()
110
+ shape = stft_mask.shape
111
+
112
+ stft_mask = stft_mask.reshape(nb * nac, 1, nf, nt)
113
+ pad_tuple = (
114
+ self.smoothing_filter.shape[-2] // 2,
115
+ self.smoothing_filter.shape[-1] // 2,
116
+ )
117
+ stft_mask = F.conv2d(stft_mask, self.smoothing_filter, padding=pad_tuple)
118
+ stft_mask = stft_mask.reshape(*shape)
119
+ stft_mask *= util.ensure_tensor(denoise_amount, ndim=stft_mask.ndim).to(
120
+ audio_signal.device
121
+ )
122
+ stft_mask = 1 - stft_mask
123
+
124
+ audio_signal.stft_data *= stft_mask
125
+ audio_signal.istft()
126
+
127
+ return audio_signal
audiotools/post.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tempfile
2
+ import typing
3
+ import zipfile
4
+ from pathlib import Path
5
+
6
+ import markdown2 as md
7
+ import matplotlib.pyplot as plt
8
+ import torch
9
+ from IPython.display import HTML
10
+
11
+
12
+ def audio_table(
13
+ audio_dict: dict,
14
+ first_column: str = None,
15
+ format_fn: typing.Callable = None,
16
+ **kwargs,
17
+ ): # pragma: no cover
18
+ """Embeds an audio table into HTML, or as the output cell
19
+ in a notebook.
20
+
21
+ Parameters
22
+ ----------
23
+ audio_dict : dict
24
+ Dictionary of data to embed.
25
+ first_column : str, optional
26
+ The label for the first column of the table, by default None
27
+ format_fn : typing.Callable, optional
28
+ How to format the data, by default None
29
+
30
+ Returns
31
+ -------
32
+ str
33
+ Table as a string
34
+
35
+ Examples
36
+ --------
37
+
38
+ >>> audio_dict = {}
39
+ >>> for i in range(signal_batch.batch_size):
40
+ >>> audio_dict[i] = {
41
+ >>> "input": signal_batch[i],
42
+ >>> "output": output_batch[i]
43
+ >>> }
44
+ >>> audiotools.post.audio_zip(audio_dict)
45
+
46
+ """
47
+ from audiotools import AudioSignal
48
+
49
+ output = []
50
+ columns = None
51
+
52
+ def _default_format_fn(label, x, **kwargs):
53
+ if torch.is_tensor(x):
54
+ x = x.tolist()
55
+
56
+ if x is None:
57
+ return "."
58
+ elif isinstance(x, AudioSignal):
59
+ return x.embed(display=False, return_html=True, **kwargs)
60
+ else:
61
+ return str(x)
62
+
63
+ if format_fn is None:
64
+ format_fn = _default_format_fn
65
+
66
+ if first_column is None:
67
+ first_column = "."
68
+
69
+ for k, v in audio_dict.items():
70
+ if not isinstance(v, dict):
71
+ v = {"Audio": v}
72
+
73
+ v_keys = list(v.keys())
74
+ if columns is None:
75
+ columns = [first_column] + v_keys
76
+ output.append(" | ".join(columns))
77
+
78
+ layout = "|---" + len(v_keys) * "|:-:"
79
+ output.append(layout)
80
+
81
+ formatted_audio = []
82
+ for col in columns[1:]:
83
+ formatted_audio.append(format_fn(col, v[col], **kwargs))
84
+
85
+ row = f"| {k} | "
86
+ row += " | ".join(formatted_audio)
87
+ output.append(row)
88
+
89
+ output = "\n" + "\n".join(output)
90
+ return output
91
+
92
+
93
+ def in_notebook(): # pragma: no cover
94
+ """Determines if code is running in a notebook.
95
+
96
+ Returns
97
+ -------
98
+ bool
99
+ Whether or not this is running in a notebook.
100
+ """
101
+ try:
102
+ from IPython import get_ipython
103
+
104
+ if "IPKernelApp" not in get_ipython().config: # pragma: no cover
105
+ return False
106
+ except ImportError:
107
+ return False
108
+ except AttributeError:
109
+ return False
110
+ return True
111
+
112
+
113
+ def disp(obj, **kwargs): # pragma: no cover
114
+ """Displays an object, depending on if its in a notebook
115
+ or not.
116
+
117
+ Parameters
118
+ ----------
119
+ obj : typing.Any
120
+ Any object to display.
121
+
122
+ """
123
+ from audiotools import AudioSignal
124
+
125
+ IN_NOTEBOOK = in_notebook()
126
+
127
+ if isinstance(obj, AudioSignal):
128
+ audio_elem = obj.embed(display=False, return_html=True)
129
+ if IN_NOTEBOOK:
130
+ return HTML(audio_elem)
131
+ else:
132
+ print(audio_elem)
133
+ if isinstance(obj, dict):
134
+ table = audio_table(obj, **kwargs)
135
+ if IN_NOTEBOOK:
136
+ return HTML(md.markdown(table, extras=["tables"]))
137
+ else:
138
+ print(table)
139
+ if isinstance(obj, plt.Figure):
140
+ plt.show()
audiotools/preference.py ADDED
@@ -0,0 +1,600 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##############################################################
2
+ ### Tools for creating preference tests (MUSHRA, ABX, etc) ###
3
+ ##############################################################
4
+ import copy
5
+ import csv
6
+ import random
7
+ import sys
8
+ import traceback
9
+ from collections import defaultdict
10
+ from pathlib import Path
11
+ from typing import List
12
+
13
+ import gradio as gr
14
+
15
+ from audiotools.core.util import find_audio
16
+
17
+ ################################################################
18
+ ### Logic for audio player, and adding audio / play buttons. ###
19
+ ################################################################
20
+
21
+ WAVESURFER = """<div id="waveform"></div><div id="wave-timeline"></div>"""
22
+
23
+ CUSTOM_CSS = """
24
+ .gradio-container {
25
+ max-width: 840px !important;
26
+ }
27
+ region.wavesurfer-region:before {
28
+ content: attr(data-region-label);
29
+ }
30
+
31
+ block {
32
+ min-width: 0 !important;
33
+ }
34
+
35
+ #wave-timeline {
36
+ background-color: rgba(0, 0, 0, 0.8);
37
+ }
38
+
39
+ .head.svelte-1cl284s {
40
+ display: none;
41
+ }
42
+ """
43
+
44
+ load_wavesurfer_js = """
45
+ function load_wavesurfer() {
46
+ function load_script(url) {
47
+ const script = document.createElement('script');
48
+ script.src = url;
49
+ document.body.appendChild(script);
50
+
51
+ return new Promise((res, rej) => {
52
+ script.onload = function() {
53
+ res();
54
+ }
55
+ script.onerror = function () {
56
+ rej();
57
+ }
58
+ });
59
+ }
60
+
61
+ function create_wavesurfer() {
62
+ var options = {
63
+ container: '#waveform',
64
+ waveColor: '#F2F2F2', // Set a darker wave color
65
+ progressColor: 'white', // Set a slightly lighter progress color
66
+ loaderColor: 'white', // Set a slightly lighter loader color
67
+ cursorColor: 'black', // Set a slightly lighter cursor color
68
+ backgroundColor: '#00AAFF', // Set a black background color
69
+ barWidth: 4,
70
+ barRadius: 3,
71
+ barHeight: 1, // the height of the wave
72
+ plugins: [
73
+ WaveSurfer.regions.create({
74
+ regionsMinLength: 0.0,
75
+ dragSelection: {
76
+ slop: 5
77
+ },
78
+ color: 'hsla(200, 50%, 70%, 0.4)',
79
+ }),
80
+ WaveSurfer.timeline.create({
81
+ container: "#wave-timeline",
82
+ primaryLabelInterval: 5.0,
83
+ secondaryLabelInterval: 1.0,
84
+ primaryFontColor: '#F2F2F2',
85
+ secondaryFontColor: '#F2F2F2',
86
+ }),
87
+ ]
88
+ };
89
+ wavesurfer = WaveSurfer.create(options);
90
+ wavesurfer.on('region-created', region => {
91
+ wavesurfer.regions.clear();
92
+ });
93
+ wavesurfer.on('finish', function () {
94
+ var loop = document.getElementById("loop-button").textContent.includes("ON");
95
+ if (loop) {
96
+ wavesurfer.play();
97
+ }
98
+ else {
99
+ var button_elements = document.getElementsByClassName('playpause')
100
+ var buttons = Array.from(button_elements);
101
+
102
+ for (let j = 0; j < buttons.length; j++) {
103
+ buttons[j].classList.remove("primary");
104
+ buttons[j].classList.add("secondary");
105
+ buttons[j].textContent = buttons[j].textContent.replace("Stop", "Play")
106
+ }
107
+ }
108
+ });
109
+
110
+ wavesurfer.on('region-out', function () {
111
+ var loop = document.getElementById("loop-button").textContent.includes("ON");
112
+ if (!loop) {
113
+ var button_elements = document.getElementsByClassName('playpause')
114
+ var buttons = Array.from(button_elements);
115
+
116
+ for (let j = 0; j < buttons.length; j++) {
117
+ buttons[j].classList.remove("primary");
118
+ buttons[j].classList.add("secondary");
119
+ buttons[j].textContent = buttons[j].textContent.replace("Stop", "Play")
120
+ }
121
+ wavesurfer.pause();
122
+ }
123
+ });
124
+
125
+ console.log("Created WaveSurfer object.")
126
+ }
127
+
128
+ load_script('https://unpkg.com/[email protected]')
129
+ .then(() => {
130
+ load_script("https://unpkg.com/[email protected]/dist/plugin/wavesurfer.timeline.min.js")
131
+ .then(() => {
132
+ load_script('https://unpkg.com/[email protected]/dist/plugin/wavesurfer.regions.min.js')
133
+ .then(() => {
134
+ console.log("Loaded regions");
135
+ create_wavesurfer();
136
+ document.getElementById("start-survey").click();
137
+ })
138
+ })
139
+ });
140
+ }
141
+ """
142
+
143
+ play = lambda i: """
144
+ function play() {
145
+ var audio_elements = document.getElementsByTagName('audio');
146
+ var button_elements = document.getElementsByClassName('playpause')
147
+
148
+ var audio_array = Array.from(audio_elements);
149
+ var buttons = Array.from(button_elements);
150
+
151
+ var src_link = audio_array[{i}].getAttribute("src");
152
+ console.log(src_link);
153
+
154
+ var loop = document.getElementById("loop-button").textContent.includes("ON");
155
+ var playing = buttons[{i}].textContent.includes("Stop");
156
+
157
+ for (let j = 0; j < buttons.length; j++) {
158
+ if (j != {i} || playing) {
159
+ buttons[j].classList.remove("primary");
160
+ buttons[j].classList.add("secondary");
161
+ buttons[j].textContent = buttons[j].textContent.replace("Stop", "Play")
162
+ }
163
+ else {
164
+ buttons[j].classList.remove("secondary");
165
+ buttons[j].classList.add("primary");
166
+ buttons[j].textContent = buttons[j].textContent.replace("Play", "Stop")
167
+ }
168
+ }
169
+
170
+ if (playing) {
171
+ wavesurfer.pause();
172
+ wavesurfer.seekTo(0.0);
173
+ }
174
+ else {
175
+ wavesurfer.load(src_link);
176
+ wavesurfer.on('ready', function () {
177
+ var region = Object.values(wavesurfer.regions.list)[0];
178
+
179
+ if (region != null) {
180
+ region.loop = loop;
181
+ region.play();
182
+ } else {
183
+ wavesurfer.play();
184
+ }
185
+ });
186
+ }
187
+ }
188
+ """.replace(
189
+ "{i}", str(i)
190
+ )
191
+
192
+ clear_regions = """
193
+ function clear_regions() {
194
+ wavesurfer.clearRegions();
195
+ }
196
+ """
197
+
198
+ reset_player = """
199
+ function reset_player() {
200
+ wavesurfer.clearRegions();
201
+ wavesurfer.pause();
202
+ wavesurfer.seekTo(0.0);
203
+
204
+ var button_elements = document.getElementsByClassName('playpause')
205
+ var buttons = Array.from(button_elements);
206
+
207
+ for (let j = 0; j < buttons.length; j++) {
208
+ buttons[j].classList.remove("primary");
209
+ buttons[j].classList.add("secondary");
210
+ buttons[j].textContent = buttons[j].textContent.replace("Stop", "Play")
211
+ }
212
+ }
213
+ """
214
+
215
+ loop_region = """
216
+ function loop_region() {
217
+ var element = document.getElementById("loop-button");
218
+ var loop = element.textContent.includes("OFF");
219
+ console.log(loop);
220
+
221
+ try {
222
+ var region = Object.values(wavesurfer.regions.list)[0];
223
+ region.loop = loop;
224
+ } catch {}
225
+
226
+ if (loop) {
227
+ element.classList.remove("secondary");
228
+ element.classList.add("primary");
229
+ element.textContent = "Looping ON";
230
+ } else {
231
+ element.classList.remove("primary");
232
+ element.classList.add("secondary");
233
+ element.textContent = "Looping OFF";
234
+ }
235
+ }
236
+ """
237
+
238
+
239
+ class Player:
240
+ def __init__(self, app):
241
+ self.app = app
242
+
243
+ self.app.load(_js=load_wavesurfer_js)
244
+ self.app.css = CUSTOM_CSS
245
+
246
+ self.wavs = []
247
+ self.position = 0
248
+
249
+ def create(self):
250
+ gr.HTML(WAVESURFER)
251
+ gr.Markdown(
252
+ "Click and drag on the waveform above to select a region for playback. "
253
+ "Once created, the region can be moved around and resized. "
254
+ "Clear the regions using the button below. Hit play on one of the buttons below to start!"
255
+ )
256
+
257
+ with gr.Row():
258
+ clear = gr.Button("Clear region")
259
+ loop = gr.Button("Looping OFF", elem_id="loop-button")
260
+
261
+ loop.click(None, _js=loop_region)
262
+ clear.click(None, _js=clear_regions)
263
+
264
+ gr.HTML("<hr>")
265
+
266
+ def add(self, name: str = "Play"):
267
+ i = self.position
268
+ self.wavs.append(
269
+ {
270
+ "audio": gr.Audio(visible=False),
271
+ "button": gr.Button(name, elem_classes=["playpause"]),
272
+ "position": i,
273
+ }
274
+ )
275
+ self.wavs[-1]["button"].click(None, _js=play(i))
276
+ self.position += 1
277
+ return self.wavs[-1]
278
+
279
+ def to_list(self):
280
+ return [x["audio"] for x in self.wavs]
281
+
282
+
283
+ ############################################################
284
+ ### Keeping track of users, and CSS for the progress bar ###
285
+ ############################################################
286
+
287
+ load_tracker = lambda name: """
288
+ function load_name() {
289
+ function setCookie(name, value, exp_days) {
290
+ var d = new Date();
291
+ d.setTime(d.getTime() + (exp_days*24*60*60*1000));
292
+ var expires = "expires=" + d.toGMTString();
293
+ document.cookie = name + "=" + value + ";" + expires + ";path=/";
294
+ }
295
+
296
+ function getCookie(name) {
297
+ var cname = name + "=";
298
+ var decodedCookie = decodeURIComponent(document.cookie);
299
+ var ca = decodedCookie.split(';');
300
+ for(var i = 0; i < ca.length; i++){
301
+ var c = ca[i];
302
+ while(c.charAt(0) == ' '){
303
+ c = c.substring(1);
304
+ }
305
+ if(c.indexOf(cname) == 0){
306
+ return c.substring(cname.length, c.length);
307
+ }
308
+ }
309
+ return "";
310
+ }
311
+
312
+ name = getCookie("{name}");
313
+ if (name == "") {
314
+ name = Math.random().toString(36).slice(2);
315
+ console.log(name);
316
+ setCookie("name", name, 30);
317
+ }
318
+ name = getCookie("{name}");
319
+ return name;
320
+ }
321
+ """.replace(
322
+ "{name}", name
323
+ )
324
+
325
+ # Progress bar
326
+
327
+ progress_template = """
328
+ <!DOCTYPE html>
329
+ <html>
330
+ <head>
331
+ <title>Progress Bar</title>
332
+ <style>
333
+ .progress-bar {
334
+ background-color: #ddd;
335
+ border-radius: 4px;
336
+ height: 30px;
337
+ width: 100%;
338
+ position: relative;
339
+ }
340
+
341
+ .progress {
342
+ background-color: #00AAFF;
343
+ border-radius: 4px;
344
+ height: 100%;
345
+ width: {PROGRESS}%; /* Change this value to control the progress */
346
+ }
347
+
348
+ .progress-text {
349
+ position: absolute;
350
+ top: 50%;
351
+ left: 50%;
352
+ transform: translate(-50%, -50%);
353
+ font-size: 18px;
354
+ font-family: Arial, sans-serif;
355
+ font-weight: bold;
356
+ color: #333 !important;
357
+ text-shadow: 1px 1px #fff;
358
+ }
359
+ </style>
360
+ </head>
361
+ <body>
362
+ <div class="progress-bar">
363
+ <div class="progress"></div>
364
+ <div class="progress-text">{TEXT}</div>
365
+ </div>
366
+ </body>
367
+ </html>
368
+ """
369
+
370
+
371
+ def create_tracker(app, cookie_name="name"):
372
+ user = gr.Text(label="user", interactive=True, visible=False, elem_id="user")
373
+ app.load(_js=load_tracker(cookie_name), outputs=user)
374
+ return user
375
+
376
+
377
+ #################################################################
378
+ ### CSS and HTML for labeling sliders for both ABX and MUSHRA ###
379
+ #################################################################
380
+
381
+ slider_abx = """
382
+ <!DOCTYPE html>
383
+ <html>
384
+ <head>
385
+ <meta charset="UTF-8">
386
+ <title>Labels Example</title>
387
+ <style>
388
+ body {
389
+ margin: 0;
390
+ padding: 0;
391
+ }
392
+
393
+ .labels-container {
394
+ display: flex;
395
+ justify-content: space-between;
396
+ align-items: center;
397
+ width: 100%;
398
+ height: 40px;
399
+ padding: 0px 12px 0px;
400
+ }
401
+
402
+ .label {
403
+ display: flex;
404
+ justify-content: center;
405
+ align-items: center;
406
+ width: 33%;
407
+ height: 100%;
408
+ font-weight: bold;
409
+ text-transform: uppercase;
410
+ padding: 10px;
411
+ font-family: Arial, sans-serif;
412
+ font-size: 16px;
413
+ font-weight: 700;
414
+ letter-spacing: 1px;
415
+ line-height: 1.5;
416
+ }
417
+
418
+ .label-a {
419
+ background-color: #00AAFF;
420
+ color: #333 !important;
421
+ }
422
+
423
+ .label-tie {
424
+ background-color: #f97316;
425
+ color: #333 !important;
426
+ }
427
+
428
+ .label-b {
429
+ background-color: #00AAFF;
430
+ color: #333 !important;
431
+ }
432
+ </style>
433
+ </head>
434
+ <body>
435
+ <div class="labels-container">
436
+ <div class="label label-a">Prefer A</div>
437
+ <div class="label label-tie">Toss-up</div>
438
+ <div class="label label-b">Prefer B</div>
439
+ </div>
440
+ </body>
441
+ </html>
442
+ """
443
+
444
+ slider_mushra = """
445
+ <!DOCTYPE html>
446
+ <html>
447
+ <head>
448
+ <meta charset="UTF-8">
449
+ <title>Labels Example</title>
450
+ <style>
451
+ body {
452
+ margin: 0;
453
+ padding: 0;
454
+ }
455
+
456
+ .labels-container {
457
+ display: flex;
458
+ justify-content: space-between;
459
+ align-items: center;
460
+ width: 100%;
461
+ height: 30px;
462
+ padding: 10px;
463
+ }
464
+
465
+ .label {
466
+ display: flex;
467
+ justify-content: center;
468
+ align-items: center;
469
+ width: 20%;
470
+ height: 100%;
471
+ font-weight: bold;
472
+ text-transform: uppercase;
473
+ padding: 10px;
474
+ font-family: Arial, sans-serif;
475
+ font-size: 13.5px;
476
+ font-weight: 700;
477
+ line-height: 1.5;
478
+ }
479
+
480
+ .label-bad {
481
+ background-color: #ff5555;
482
+ color: #333 !important;
483
+ }
484
+
485
+ .label-poor {
486
+ background-color: #ffa500;
487
+ color: #333 !important;
488
+ }
489
+
490
+ .label-fair {
491
+ background-color: #ffd700;
492
+ color: #333 !important;
493
+ }
494
+
495
+ .label-good {
496
+ background-color: #97d997;
497
+ color: #333 !important;
498
+ }
499
+
500
+ .label-excellent {
501
+ background-color: #04c822;
502
+ color: #333 !important;
503
+ }
504
+ </style>
505
+ </head>
506
+ <body>
507
+ <div class="labels-container">
508
+ <div class="label label-bad">bad</div>
509
+ <div class="label label-poor">poor</div>
510
+ <div class="label label-fair">fair</div>
511
+ <div class="label label-good">good</div>
512
+ <div class="label label-excellent">excellent</div>
513
+ </div>
514
+ </body>
515
+ </html>
516
+ """
517
+
518
+ #########################################################
519
+ ### Handling loading audio and tracking session state ###
520
+ #########################################################
521
+
522
+
523
+ class Samples:
524
+ def __init__(self, folder: str, shuffle: bool = True, n_samples: int = None):
525
+ files = find_audio(folder)
526
+ samples = defaultdict(lambda: defaultdict())
527
+
528
+ for f in files:
529
+ condition = f.parent.stem
530
+ samples[f.name][condition] = f
531
+
532
+ self.samples = samples
533
+ self.names = list(samples.keys())
534
+ self.filtered = False
535
+ self.current = 0
536
+
537
+ if shuffle:
538
+ random.shuffle(self.names)
539
+
540
+ self.n_samples = len(self.names) if n_samples is None else n_samples
541
+
542
+ def get_updates(self, idx, order):
543
+ key = self.names[idx]
544
+ return [gr.update(value=str(self.samples[key][o])) for o in order]
545
+
546
+ def progress(self):
547
+ try:
548
+ pct = self.current / len(self) * 100
549
+ except: # pragma: no cover
550
+ pct = 100
551
+ text = f"On {self.current} / {len(self)} samples"
552
+ pbar = (
553
+ copy.copy(progress_template)
554
+ .replace("{PROGRESS}", str(pct))
555
+ .replace("{TEXT}", str(text))
556
+ )
557
+ return gr.update(value=pbar)
558
+
559
+ def __len__(self):
560
+ return self.n_samples
561
+
562
+ def filter_completed(self, user, save_path):
563
+ if not self.filtered:
564
+ done = []
565
+ if Path(save_path).exists():
566
+ with open(save_path, "r") as f:
567
+ reader = csv.DictReader(f)
568
+ done = [r["sample"] for r in reader if r["user"] == user]
569
+ self.names = [k for k in self.names if k not in done]
570
+ self.names = self.names[: self.n_samples]
571
+ self.filtered = True # Avoid filtering more than once per session.
572
+
573
+ def get_next_sample(self, reference, conditions):
574
+ random.shuffle(conditions)
575
+ if reference is not None:
576
+ self.order = [reference] + conditions
577
+ else:
578
+ self.order = conditions
579
+
580
+ try:
581
+ updates = self.get_updates(self.current, self.order)
582
+ self.current += 1
583
+ done = gr.update(interactive=True)
584
+ pbar = self.progress()
585
+ except:
586
+ traceback.print_exc()
587
+ updates = [gr.update() for _ in range(len(self.order))]
588
+ done = gr.update(value="No more samples!", interactive=False)
589
+ self.current = len(self)
590
+ pbar = self.progress()
591
+
592
+ return updates, done, pbar
593
+
594
+
595
+ def save_result(result, save_path):
596
+ with open(save_path, mode="a", newline="") as file:
597
+ writer = csv.DictWriter(file, fieldnames=sorted(list(result.keys())))
598
+ if file.tell() == 0:
599
+ writer.writeheader()
600
+ writer.writerow(result)
requirements.txt CHANGED
@@ -1,12 +1,10 @@
1
- alias_free_torch
2
- descript_audiotools==0.7.3
3
- diffusers
4
- einops
5
- librosa
6
- numpy
7
- pandas
8
- PyYAML
9
- soundfile
10
- tqdm
11
- transformers
12
  vector_quantize_pytorch
 
1
+ alias_free_torch
2
+ diffusers
3
+ einops
4
+ librosa
5
+ pandas
6
+ PyYAML
7
+ soundfile
8
+ tqdm
9
+ transformers
 
 
10
  vector_quantize_pytorch