renator commited on
Commit
aaa69e0
·
1 Parent(s): 4a367ac

fix build issue and env

Browse files
Files changed (4) hide show
  1. Dockerfile +3 -1
  2. matching.py +385 -0
  3. spectrum.py +0 -0
  4. utils/utils.py +2609 -0
Dockerfile CHANGED
@@ -30,7 +30,9 @@ COPY constantq.py /usr/local/lib/python3.10/site-packages/librosa/core/constantq
30
  COPY filters.py /usr/local/lib/python3.10/site-packages/librosa/filters.py
31
  COPY sequence.py /usr/local/lib/python3.10/site-packages/librosa/sequence.py
32
  COPY utils.py /usr/local/lib/python3.10/site-packages/librosa/feature/utils.py
33
-
 
 
34
  # RUN cd /tmp && mkdir cache1
35
 
36
  ENV NUMBA_CACHE_DIR=/tmp
 
30
  COPY filters.py /usr/local/lib/python3.10/site-packages/librosa/filters.py
31
  COPY sequence.py /usr/local/lib/python3.10/site-packages/librosa/sequence.py
32
  COPY utils.py /usr/local/lib/python3.10/site-packages/librosa/feature/utils.py
33
+ COPY utils/utils.py /usr/local/lib/python3.10/site-packages/librosa/util/utils.py
34
+ COPY matching.py /usr/local/lib/python3.10/site-packages/librosa/util/matching.py
35
+ COPY spectrum.py /usr/local/lib/python3.10/site-packages/librosa/core/spectrum.py
36
  # RUN cd /tmp && mkdir cache1
37
 
38
  ENV NUMBA_CACHE_DIR=/tmp
matching.py ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ #!/usr/bin/env python
4
+ # -*- coding: utf-8 -*-
5
+ """Matching functions"""
6
+
7
+ import numpy as np
8
+ import numba
9
+
10
+ from .exceptions import ParameterError
11
+ from .utils import valid_intervals
12
+ from .._typing import _SequenceLike
13
+
14
+ __all__ = ["match_intervals", "match_events"]
15
+
16
+
17
+ @numba.jit(nopython=True, cache=False) # type: ignore
18
+ def __jaccard(int_a: np.ndarray, int_b: np.ndarray): # pragma: no cover
19
+ """Jaccard similarity between two intervals
20
+
21
+ Parameters
22
+ ----------
23
+ int_a, int_b : np.ndarrays, shape=(2,)
24
+
25
+ Returns
26
+ -------
27
+ Jaccard similarity between intervals
28
+ """
29
+ ends = [int_a[1], int_b[1]]
30
+ if ends[1] < ends[0]:
31
+ ends.reverse()
32
+
33
+ starts = [int_a[0], int_b[0]]
34
+ if starts[1] < starts[0]:
35
+ starts.reverse()
36
+
37
+ intersection = ends[0] - starts[1]
38
+ if intersection < 0:
39
+ intersection = 0.0
40
+
41
+ union = ends[1] - starts[0]
42
+
43
+ if union > 0:
44
+ return intersection / union
45
+
46
+ return 0.0
47
+
48
+
49
+ @numba.jit(nopython=True, cache=False)
50
+ def __match_interval_overlaps(query, intervals_to, candidates): # pragma: no cover
51
+ """Find the best Jaccard match from query to candidates"""
52
+
53
+ best_score = -1
54
+ best_idx = -1
55
+ for idx in candidates:
56
+ score = __jaccard(query, intervals_to[idx])
57
+
58
+ if score > best_score:
59
+ best_score, best_idx = score, idx
60
+ return best_idx
61
+
62
+
63
+ @numba.jit(nopython=True, cache=False) # type: ignore
64
+ def __match_intervals(
65
+ intervals_from: np.ndarray, intervals_to: np.ndarray, strict: bool = True
66
+ ) -> np.ndarray: # pragma: no cover
67
+ """Numba-accelerated interval matching algorithm."""
68
+ # sort index of the interval starts
69
+ start_index = np.argsort(intervals_to[:, 0])
70
+
71
+ # sort index of the interval ends
72
+ end_index = np.argsort(intervals_to[:, 1])
73
+
74
+ # and sorted values of starts
75
+ start_sorted = intervals_to[start_index, 0]
76
+ # and ends
77
+ end_sorted = intervals_to[end_index, 1]
78
+
79
+ search_ends = np.searchsorted(start_sorted, intervals_from[:, 1], side="right")
80
+ search_starts = np.searchsorted(end_sorted, intervals_from[:, 0], side="left")
81
+
82
+ output = np.empty(len(intervals_from), dtype=numba.uint32)
83
+ for i in range(len(intervals_from)):
84
+ query = intervals_from[i]
85
+
86
+ # Find the intervals that start after our query ends
87
+ after_query = search_ends[i]
88
+ # And the intervals that end after our query begins
89
+ before_query = search_starts[i]
90
+
91
+ # Candidates for overlapping have to (end after we start) and (begin before we end)
92
+ candidates = set(start_index[:after_query]) & set(end_index[before_query:])
93
+
94
+ # Proceed as before
95
+ if len(candidates) > 0:
96
+ output[i] = __match_interval_overlaps(query, intervals_to, candidates)
97
+ elif strict:
98
+ # Numba only lets us use compile-time constants in exception messages
99
+ raise ParameterError
100
+ else:
101
+ # Find the closest interval
102
+ # (start_index[after_query] - query[1]) is the distance to the next interval
103
+ # (query[0] - end_index[before_query])
104
+ dist_before = np.inf
105
+ dist_after = np.inf
106
+ if search_starts[i] > 0:
107
+ dist_before = query[0] - end_sorted[search_starts[i] - 1]
108
+ if search_ends[i] + 1 < len(intervals_to):
109
+ dist_after = start_sorted[search_ends[i] + 1] - query[1]
110
+ if dist_before < dist_after:
111
+ output[i] = end_index[search_starts[i] - 1]
112
+ else:
113
+ output[i] = start_index[search_ends[i] + 1]
114
+ return output
115
+
116
+
117
+ def match_intervals(
118
+ intervals_from: np.ndarray, intervals_to: np.ndarray, strict: bool = True
119
+ ) -> np.ndarray:
120
+ """Match one set of time intervals to another.
121
+
122
+ This can be useful for tasks such as mapping beat timings
123
+ to segments.
124
+
125
+ Each element ``[a, b]`` of ``intervals_from`` is matched to the
126
+ element ``[c, d]`` of ``intervals_to`` which maximizes the
127
+ Jaccard similarity between the intervals::
128
+
129
+ max(0, |min(b, d) - max(a, c)|) / |max(d, b) - min(a, c)|
130
+
131
+ In ``strict=True`` mode, if there is no interval with positive
132
+ intersection with ``[a,b]``, an exception is thrown.
133
+
134
+ In ``strict=False`` mode, any interval ``[a, b]`` that has no
135
+ intersection with any element of ``intervals_to`` is instead
136
+ matched to the interval ``[c, d]`` which minimizes::
137
+
138
+ min(|b - c|, |a - d|)
139
+
140
+ that is, the disjoint interval [c, d] with a boundary closest
141
+ to [a, b].
142
+
143
+ .. note:: An element of ``intervals_to`` may be matched to multiple
144
+ entries of ``intervals_from``.
145
+
146
+ Parameters
147
+ ----------
148
+ intervals_from : np.ndarray [shape=(n, 2)]
149
+ The time range for source intervals.
150
+ The ``i`` th interval spans time ``intervals_from[i, 0]``
151
+ to ``intervals_from[i, 1]``.
152
+ ``intervals_from[0, 0]`` should be 0, ``intervals_from[-1, 1]``
153
+ should be the track duration.
154
+ intervals_to : np.ndarray [shape=(m, 2)]
155
+ Analogous to ``intervals_from``.
156
+ strict : bool
157
+ If ``True``, intervals can only match if they intersect.
158
+ If ``False``, disjoint intervals can match.
159
+
160
+ Returns
161
+ -------
162
+ interval_mapping : np.ndarray [shape=(n,)]
163
+ For each interval in ``intervals_from``, the
164
+ corresponding interval in ``intervals_to``.
165
+
166
+ See Also
167
+ --------
168
+ match_events
169
+
170
+ Raises
171
+ ------
172
+ ParameterError
173
+ If either array of input intervals is not the correct shape
174
+
175
+ If ``strict=True`` and some element of ``intervals_from`` is disjoint from
176
+ every element of ``intervals_to``.
177
+
178
+ Examples
179
+ --------
180
+ >>> ints_from = np.array([[3, 5], [1, 4], [4, 5]])
181
+ >>> ints_to = np.array([[0, 2], [1, 3], [4, 5], [6, 7]])
182
+ >>> librosa.util.match_intervals(ints_from, ints_to)
183
+ array([2, 1, 2], dtype=uint32)
184
+ >>> # [3, 5] => [4, 5] (ints_to[2])
185
+ >>> # [1, 4] => [1, 3] (ints_to[1])
186
+ >>> # [4, 5] => [4, 5] (ints_to[2])
187
+
188
+ The reverse matching of the above is not possible in ``strict`` mode
189
+ because ``[6, 7]`` is disjoint from all intervals in ``ints_from``.
190
+ With ``strict=False``, we get the following:
191
+
192
+ >>> librosa.util.match_intervals(ints_to, ints_from, strict=False)
193
+ array([1, 1, 2, 2], dtype=uint32)
194
+ >>> # [0, 2] => [1, 4] (ints_from[1])
195
+ >>> # [1, 3] => [1, 4] (ints_from[1])
196
+ >>> # [4, 5] => [4, 5] (ints_from[2])
197
+ >>> # [6, 7] => [4, 5] (ints_from[2])
198
+ """
199
+
200
+ if len(intervals_from) == 0 or len(intervals_to) == 0:
201
+ raise ParameterError("Attempting to match empty interval list")
202
+
203
+ # Verify that the input intervals has correct shape and size
204
+ valid_intervals(intervals_from)
205
+ valid_intervals(intervals_to)
206
+
207
+ try:
208
+ # Suppress type check because of numba wrapper
209
+ return __match_intervals(intervals_from, intervals_to, strict=strict) # type: ignore
210
+ except ParameterError as exc:
211
+ raise ParameterError(f"Unable to match intervals with strict={strict}") from exc
212
+
213
+
214
+ def match_events(
215
+ events_from: _SequenceLike,
216
+ events_to: _SequenceLike,
217
+ left: bool = True,
218
+ right: bool = True,
219
+ ) -> np.ndarray:
220
+ """Match one set of events to another.
221
+
222
+ This is useful for tasks such as matching beats to the nearest
223
+ detected onsets, or frame-aligned events to the nearest zero-crossing.
224
+
225
+ .. note:: A target event may be matched to multiple source events.
226
+
227
+ Examples
228
+ --------
229
+ >>> # Sources are multiples of 7
230
+ >>> s_from = np.arange(0, 100, 7)
231
+ >>> s_from
232
+ array([ 0, 7, 14, 21, 28, 35, 42, 49, 56, 63, 70, 77, 84, 91,
233
+ 98])
234
+ >>> # Targets are multiples of 10
235
+ >>> s_to = np.arange(0, 100, 10)
236
+ >>> s_to
237
+ array([ 0, 10, 20, 30, 40, 50, 60, 70, 80, 90])
238
+ >>> # Find the matching
239
+ >>> idx = librosa.util.match_events(s_from, s_to)
240
+ >>> idx
241
+ array([0, 1, 1, 2, 3, 3, 4, 5, 6, 6, 7, 8, 8, 9, 9])
242
+ >>> # Print each source value to its matching target
243
+ >>> zip(s_from, s_to[idx])
244
+ [(0, 0), (7, 10), (14, 10), (21, 20), (28, 30), (35, 30),
245
+ (42, 40), (49, 50), (56, 60), (63, 60), (70, 70), (77, 80),
246
+ (84, 80), (91, 90), (98, 90)]
247
+
248
+ Parameters
249
+ ----------
250
+ events_from : ndarray [shape=(n,)]
251
+ Array of events (eg, times, sample or frame indices) to match from.
252
+ events_to : ndarray [shape=(m,)]
253
+ Array of events (eg, times, sample or frame indices) to
254
+ match against.
255
+ left : bool
256
+ right : bool
257
+ If ``False``, then matched events cannot be to the left (or right)
258
+ of source events.
259
+
260
+ Returns
261
+ -------
262
+ event_mapping : np.ndarray [shape=(n,)]
263
+ For each event in ``events_from``, the corresponding event
264
+ index in ``events_to``::
265
+
266
+ event_mapping[i] == arg min |events_from[i] - events_to[:]|
267
+
268
+ See Also
269
+ --------
270
+ match_intervals
271
+
272
+ Raises
273
+ ------
274
+ ParameterError
275
+ If either array of input events is not the correct shape
276
+ """
277
+ if len(events_from) == 0 or len(events_to) == 0:
278
+ raise ParameterError("Attempting to match empty event list")
279
+
280
+ # If we can't match left or right, then only strict equivalence
281
+ # counts as a match.
282
+ if not (left or right) and not np.all(np.in1d(events_from, events_to)):
283
+ raise ParameterError(
284
+ "Cannot match events with left=right=False "
285
+ "and events_from is not contained "
286
+ "in events_to"
287
+ )
288
+
289
+ # If we can't match to the left, then there should be at least one
290
+ # target event greater-equal to every source event
291
+ if (not left) and max(events_to) < max(events_from):
292
+ raise ParameterError(
293
+ "Cannot match events with left=False "
294
+ "and max(events_to) < max(events_from)"
295
+ )
296
+
297
+ # If we can't match to the right, then there should be at least one
298
+ # target event less-equal to every source event
299
+ if (not right) and min(events_to) > min(events_from):
300
+ raise ParameterError(
301
+ "Cannot match events with right=False "
302
+ "and min(events_to) > min(events_from)"
303
+ )
304
+
305
+ # array of matched items
306
+ output = np.empty_like(events_from, dtype=np.int32)
307
+
308
+ # Suppress type check because of numba
309
+ return __match_events_helper(output, events_from, events_to, left, right) # type: ignore
310
+
311
+
312
+ @numba.jit(nopython=True, cache=False) # type: ignore
313
+ def __match_events_helper(
314
+ output: np.ndarray,
315
+ events_from: np.ndarray,
316
+ events_to: np.ndarray,
317
+ left: bool = True,
318
+ right: bool = True,
319
+ ): # pragma: no cover
320
+ # mock dictionary for events
321
+ from_idx = np.argsort(events_from)
322
+ sorted_from = events_from[from_idx]
323
+
324
+ to_idx = np.argsort(events_to)
325
+ sorted_to = events_to[to_idx]
326
+
327
+ # find the matching indices
328
+ matching_indices = np.searchsorted(sorted_to, sorted_from)
329
+
330
+ # iterate over indices in matching_indices
331
+ for ind, middle_ind in enumerate(matching_indices):
332
+ left_flag = False
333
+ right_flag = False
334
+
335
+ left_ind = -1
336
+ right_ind = len(matching_indices)
337
+
338
+ left_diff = 0
339
+ right_diff = 0
340
+ mid_diff = 0
341
+
342
+ middle_ind = matching_indices[ind]
343
+ sorted_from_num = sorted_from[ind]
344
+
345
+ # Prevent oob from chosen index
346
+ if middle_ind == len(sorted_to):
347
+ middle_ind -= 1
348
+
349
+ # Permitted to look to the left
350
+ if left and middle_ind > 0:
351
+ left_ind = middle_ind - 1
352
+ left_flag = True
353
+
354
+ # Permitted to look to right
355
+ if right and middle_ind < len(sorted_to) - 1:
356
+ right_ind = middle_ind + 1
357
+ right_flag = True
358
+
359
+ mid_diff = abs(sorted_to[middle_ind] - sorted_from_num)
360
+ if left and left_flag:
361
+ left_diff = abs(sorted_to[left_ind] - sorted_from_num)
362
+ if right and right_flag:
363
+ right_diff = abs(sorted_to[right_ind] - sorted_from_num)
364
+
365
+ if left_flag and (
366
+ not right
367
+ and (sorted_to[middle_ind] > sorted_from_num)
368
+ or (not right_flag and left_diff < mid_diff)
369
+ or (left_diff < right_diff and left_diff < mid_diff)
370
+ ):
371
+ output[ind] = to_idx[left_ind]
372
+
373
+ # Check if right should be chosen
374
+ elif right_flag and (right_diff < mid_diff):
375
+ output[ind] = to_idx[right_ind]
376
+
377
+ # Selected index wins
378
+ else:
379
+ output[ind] = to_idx[middle_ind]
380
+
381
+ # Undo sorting
382
+ solutions = np.empty_like(output)
383
+ solutions[from_idx] = output
384
+
385
+ return solutions
spectrum.py ADDED
The diff for this file is too large to render. See raw diff
 
utils/utils.py ADDED
@@ -0,0 +1,2609 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ #!/usr/bin/env python
4
+ # -*- coding: utf-8 -*-
5
+ """Utility functions"""
6
+
7
+ from __future__ import annotations
8
+
9
+ import scipy.ndimage
10
+ import scipy.sparse
11
+
12
+ import numpy as np
13
+ import numba
14
+ from numpy.lib.stride_tricks import as_strided
15
+
16
+ from .._cache import cache
17
+ from .exceptions import ParameterError
18
+ from .deprecation import Deprecated
19
+ from numpy.typing import ArrayLike, DTypeLike
20
+ from typing import (
21
+ Any,
22
+ Callable,
23
+ Iterable,
24
+ List,
25
+ Dict,
26
+ Optional,
27
+ Sequence,
28
+ Tuple,
29
+ TypeVar,
30
+ Union,
31
+ overload,
32
+ )
33
+ from typing_extensions import Literal
34
+ from .._typing import _SequenceLike, _FloatLike_co, _ComplexLike_co
35
+
36
+ # Constrain STFT block sizes to 256 KB
37
+ MAX_MEM_BLOCK = 2**8 * 2**10
38
+
39
+ __all__ = [
40
+ "MAX_MEM_BLOCK",
41
+ "frame",
42
+ "pad_center",
43
+ "expand_to",
44
+ "fix_length",
45
+ "valid_audio",
46
+ "valid_int",
47
+ "is_positive_int",
48
+ "valid_intervals",
49
+ "fix_frames",
50
+ "axis_sort",
51
+ "localmax",
52
+ "localmin",
53
+ "normalize",
54
+ "peak_pick",
55
+ "sparsify_rows",
56
+ "shear",
57
+ "stack",
58
+ "fill_off_diagonal",
59
+ "index_to_slice",
60
+ "sync",
61
+ "softmask",
62
+ "buf_to_float",
63
+ "tiny",
64
+ "cyclic_gradient",
65
+ "dtype_r2c",
66
+ "dtype_c2r",
67
+ "count_unique",
68
+ "is_unique",
69
+ "abs2",
70
+ "phasor",
71
+ ]
72
+
73
+
74
+ def frame(
75
+ x: np.ndarray,
76
+ *,
77
+ frame_length: int,
78
+ hop_length: int,
79
+ axis: int = -1,
80
+ writeable: bool = False,
81
+ subok: bool = False,
82
+ ) -> np.ndarray:
83
+ """Slice a data array into (overlapping) frames.
84
+
85
+ This implementation uses low-level stride manipulation to avoid
86
+ making a copy of the data. The resulting frame representation
87
+ is a new view of the same input data.
88
+
89
+ For example, a one-dimensional input ``x = [0, 1, 2, 3, 4, 5, 6]``
90
+ can be framed with frame length 3 and hop length 2 in two ways.
91
+ The first (``axis=-1``), results in the array ``x_frames``::
92
+
93
+ [[0, 2, 4],
94
+ [1, 3, 5],
95
+ [2, 4, 6]]
96
+
97
+ where each column ``x_frames[:, i]`` contains a contiguous slice of
98
+ the input ``x[i * hop_length : i * hop_length + frame_length]``.
99
+
100
+ The second way (``axis=0``) results in the array ``x_frames``::
101
+
102
+ [[0, 1, 2],
103
+ [2, 3, 4],
104
+ [4, 5, 6]]
105
+
106
+ where each row ``x_frames[i]`` contains a contiguous slice of the input.
107
+
108
+ This generalizes to higher dimensional inputs, as shown in the examples below.
109
+ In general, the framing operation increments by 1 the number of dimensions,
110
+ adding a new "frame axis" either before the framing axis (if ``axis < 0``)
111
+ or after the framing axis (if ``axis >= 0``).
112
+
113
+ Parameters
114
+ ----------
115
+ x : np.ndarray
116
+ Array to frame
117
+ frame_length : int > 0 [scalar]
118
+ Length of the frame
119
+ hop_length : int > 0 [scalar]
120
+ Number of steps to advance between frames
121
+ axis : int
122
+ The axis along which to frame.
123
+ writeable : bool
124
+ If ``True``, then the framed view of ``x`` is read-only.
125
+ If ``False``, then the framed view is read-write. Note that writing to the framed view
126
+ will also write to the input array ``x`` in this case.
127
+ subok : bool
128
+ If True, sub-classes will be passed-through, otherwise the returned array will be
129
+ forced to be a base-class array (default).
130
+
131
+ Returns
132
+ -------
133
+ x_frames : np.ndarray [shape=(..., frame_length, N_FRAMES, ...)]
134
+ A framed view of ``x``, for example with ``axis=-1`` (framing on the last dimension)::
135
+
136
+ x_frames[..., j] == x[..., j * hop_length : j * hop_length + frame_length]
137
+
138
+ If ``axis=0`` (framing on the first dimension), then::
139
+
140
+ x_frames[j] = x[j * hop_length : j * hop_length + frame_length]
141
+
142
+ Raises
143
+ ------
144
+ ParameterError
145
+ If ``x.shape[axis] < frame_length``, there is not enough data to fill one frame.
146
+
147
+ If ``hop_length < 1``, frames cannot advance.
148
+
149
+ See Also
150
+ --------
151
+ numpy.lib.stride_tricks.as_strided
152
+
153
+ Examples
154
+ --------
155
+ Extract 2048-sample frames from monophonic signal with a hop of 64 samples per frame
156
+
157
+ >>> y, sr = librosa.load(librosa.ex('trumpet'))
158
+ >>> frames = librosa.util.frame(y, frame_length=2048, hop_length=64)
159
+ >>> frames
160
+ array([[-1.407e-03, -2.604e-02, ..., -1.795e-05, -8.108e-06],
161
+ [-4.461e-04, -3.721e-02, ..., -1.573e-05, -1.652e-05],
162
+ ...,
163
+ [ 7.960e-02, -2.335e-01, ..., -6.815e-06, 1.266e-05],
164
+ [ 9.568e-02, -1.252e-01, ..., 7.397e-06, -1.921e-05]],
165
+ dtype=float32)
166
+ >>> y.shape
167
+ (117601,)
168
+
169
+ >>> frames.shape
170
+ (2048, 1806)
171
+
172
+ Or frame along the first axis instead of the last:
173
+
174
+ >>> frames = librosa.util.frame(y, frame_length=2048, hop_length=64, axis=0)
175
+ >>> frames.shape
176
+ (1806, 2048)
177
+
178
+ Frame a stereo signal:
179
+
180
+ >>> y, sr = librosa.load(librosa.ex('trumpet', hq=True), mono=False)
181
+ >>> y.shape
182
+ (2, 117601)
183
+ >>> frames = librosa.util.frame(y, frame_length=2048, hop_length=64)
184
+ (2, 2048, 1806)
185
+
186
+ Carve an STFT into fixed-length patches of 32 frames with 50% overlap
187
+
188
+ >>> y, sr = librosa.load(librosa.ex('trumpet'))
189
+ >>> S = np.abs(librosa.stft(y))
190
+ >>> S.shape
191
+ (1025, 230)
192
+ >>> S_patch = librosa.util.frame(S, frame_length=32, hop_length=16)
193
+ >>> S_patch.shape
194
+ (1025, 32, 13)
195
+ >>> # The first patch contains the first 32 frames of S
196
+ >>> np.allclose(S_patch[:, :, 0], S[:, :32])
197
+ True
198
+ >>> # The second patch contains frames 16 to 16+32=48, and so on
199
+ >>> np.allclose(S_patch[:, :, 1], S[:, 16:48])
200
+ True
201
+ """
202
+
203
+ # This implementation is derived from numpy.lib.stride_tricks.sliding_window_view (1.20.0)
204
+ # https://numpy.org/doc/stable/reference/generated/numpy.lib.stride_tricks.sliding_window_view.html
205
+
206
+ x = np.array(x, copy=False, subok=subok)
207
+
208
+ if x.shape[axis] < frame_length:
209
+ raise ParameterError(
210
+ f"Input is too short (n={x.shape[axis]:d}) for frame_length={frame_length:d}"
211
+ )
212
+
213
+ if hop_length < 1:
214
+ raise ParameterError(f"Invalid hop_length: {hop_length:d}")
215
+
216
+ # put our new within-frame axis at the end for now
217
+ out_strides = x.strides + tuple([x.strides[axis]])
218
+
219
+ # Reduce the shape on the framing axis
220
+ x_shape_trimmed = list(x.shape)
221
+ x_shape_trimmed[axis] -= frame_length - 1
222
+
223
+ out_shape = tuple(x_shape_trimmed) + tuple([frame_length])
224
+ xw = as_strided(
225
+ x, strides=out_strides, shape=out_shape, subok=subok, writeable=writeable
226
+ )
227
+
228
+ if axis < 0:
229
+ target_axis = axis - 1
230
+ else:
231
+ target_axis = axis + 1
232
+
233
+ xw = np.moveaxis(xw, -1, target_axis)
234
+
235
+ # Downsample along the target axis
236
+ slices = [slice(None)] * xw.ndim
237
+ slices[axis] = slice(0, None, hop_length)
238
+ return xw[tuple(slices)]
239
+
240
+
241
+ @cache(level=20)
242
+ def valid_audio(y: np.ndarray, *, mono: Union[bool, Deprecated] = Deprecated()) -> bool:
243
+ """Determine whether a variable contains valid audio data.
244
+
245
+ The following conditions must be satisfied:
246
+
247
+ - ``type(y)`` is ``np.ndarray``
248
+ - ``y.dtype`` is floating-point
249
+ - ``y.ndim != 0`` (must have at least one dimension)
250
+ - ``np.isfinite(y).all()`` samples must be all finite values
251
+
252
+ If ``mono`` is specified, then we additionally require
253
+ - ``y.ndim == 1``
254
+
255
+ Parameters
256
+ ----------
257
+ y : np.ndarray
258
+ The input data to validate
259
+
260
+ mono : bool
261
+ Whether or not to require monophonic audio
262
+
263
+ .. warning:: The ``mono`` parameter is deprecated in version 0.9 and will be
264
+ removed in 0.10.
265
+
266
+ Returns
267
+ -------
268
+ valid : bool
269
+ True if all tests pass
270
+
271
+ Raises
272
+ ------
273
+ ParameterError
274
+ In any of the conditions specified above fails
275
+
276
+ Notes
277
+ -----
278
+ This function caches at level 20.
279
+
280
+ Examples
281
+ --------
282
+ >>> # By default, valid_audio allows only mono signals
283
+ >>> filepath = librosa.ex('trumpet', hq=True)
284
+ >>> y_mono, sr = librosa.load(filepath, mono=True)
285
+ >>> y_stereo, _ = librosa.load(filepath, mono=False)
286
+ >>> librosa.util.valid_audio(y_mono), librosa.util.valid_audio(y_stereo)
287
+ True, False
288
+
289
+ >>> # To allow stereo signals, set mono=False
290
+ >>> librosa.util.valid_audio(y_stereo, mono=False)
291
+ True
292
+
293
+ See Also
294
+ --------
295
+ numpy.float32
296
+ """
297
+
298
+ if not isinstance(y, np.ndarray):
299
+ raise ParameterError("Audio data must be of type numpy.ndarray")
300
+
301
+ if not np.issubdtype(y.dtype, np.floating):
302
+ raise ParameterError("Audio data must be floating-point")
303
+
304
+ if y.ndim == 0:
305
+ raise ParameterError(
306
+ f"Audio data must be at least one-dimensional, given y.shape={y.shape}"
307
+ )
308
+
309
+ if isinstance(mono, Deprecated):
310
+ mono = False
311
+
312
+ if mono and y.ndim != 1:
313
+ raise ParameterError(
314
+ f"Invalid shape for monophonic audio: ndim={y.ndim:d}, shape={y.shape}"
315
+ )
316
+
317
+ if not np.isfinite(y).all():
318
+ raise ParameterError("Audio buffer is not finite everywhere")
319
+
320
+ return True
321
+
322
+
323
+ def valid_int(x: float, *, cast: Optional[Callable[[float], float]] = None) -> int:
324
+ """Ensure that an input value is integer-typed.
325
+ This is primarily useful for ensuring integrable-valued
326
+ array indices.
327
+
328
+ Parameters
329
+ ----------
330
+ x : number
331
+ A scalar value to be cast to int
332
+ cast : function [optional]
333
+ A function to modify ``x`` before casting.
334
+ Default: `np.floor`
335
+
336
+ Returns
337
+ -------
338
+ x_int : int
339
+ ``x_int = int(cast(x))``
340
+
341
+ Raises
342
+ ------
343
+ ParameterError
344
+ If ``cast`` is provided and is not callable.
345
+ """
346
+
347
+ if cast is None:
348
+ cast = np.floor
349
+
350
+ if not callable(cast):
351
+ raise ParameterError("cast parameter must be callable")
352
+
353
+ return int(cast(x))
354
+
355
+
356
+ def is_positive_int(x: float) -> bool:
357
+ """Checks that x is a positive integer, i.e. 1 or greater.
358
+
359
+ Parameters
360
+ ----------
361
+ x : number
362
+
363
+ Returns
364
+ -------
365
+ positive : bool
366
+
367
+ """
368
+
369
+ # Check type first to catch None values.
370
+ return isinstance(x, (int, np.integer)) and (x > 0)
371
+
372
+
373
+ def valid_intervals(intervals: np.ndarray) -> bool:
374
+ """Ensure that an array is a valid representation of time intervals:
375
+
376
+ - intervals.ndim == 2
377
+ - intervals.shape[1] == 2
378
+ - intervals[i, 0] <= intervals[i, 1] for all i
379
+
380
+ Parameters
381
+ ----------
382
+ intervals : np.ndarray [shape=(n, 2)]
383
+ set of time intervals
384
+
385
+ Returns
386
+ -------
387
+ valid : bool
388
+ True if ``intervals`` passes validation.
389
+ """
390
+
391
+ if intervals.ndim != 2 or intervals.shape[-1] != 2:
392
+ raise ParameterError("intervals must have shape (n, 2)")
393
+
394
+ if np.any(intervals[:, 0] > intervals[:, 1]):
395
+ raise ParameterError(f"intervals={intervals} must have non-negative durations")
396
+
397
+ return True
398
+
399
+
400
+ def pad_center(
401
+ data: np.ndarray, *, size: int, axis: int = -1, **kwargs: Any
402
+ ) -> np.ndarray:
403
+ """Pad an array to a target length along a target axis.
404
+
405
+ This differs from `np.pad` by centering the data prior to padding,
406
+ analogous to `str.center`
407
+
408
+ Examples
409
+ --------
410
+ >>> # Generate a vector
411
+ >>> data = np.ones(5)
412
+ >>> librosa.util.pad_center(data, size=10, mode='constant')
413
+ array([ 0., 0., 1., 1., 1., 1., 1., 0., 0., 0.])
414
+
415
+ >>> # Pad a matrix along its first dimension
416
+ >>> data = np.ones((3, 5))
417
+ >>> librosa.util.pad_center(data, size=7, axis=0)
418
+ array([[ 0., 0., 0., 0., 0.],
419
+ [ 0., 0., 0., 0., 0.],
420
+ [ 1., 1., 1., 1., 1.],
421
+ [ 1., 1., 1., 1., 1.],
422
+ [ 1., 1., 1., 1., 1.],
423
+ [ 0., 0., 0., 0., 0.],
424
+ [ 0., 0., 0., 0., 0.]])
425
+ >>> # Or its second dimension
426
+ >>> librosa.util.pad_center(data, size=7, axis=1)
427
+ array([[ 0., 1., 1., 1., 1., 1., 0.],
428
+ [ 0., 1., 1., 1., 1., 1., 0.],
429
+ [ 0., 1., 1., 1., 1., 1., 0.]])
430
+
431
+ Parameters
432
+ ----------
433
+ data : np.ndarray
434
+ Vector to be padded and centered
435
+ size : int >= len(data) [scalar]
436
+ Length to pad ``data``
437
+ axis : int
438
+ Axis along which to pad and center the data
439
+ **kwargs : additional keyword arguments
440
+ arguments passed to `np.pad`
441
+
442
+ Returns
443
+ -------
444
+ data_padded : np.ndarray
445
+ ``data`` centered and padded to length ``size`` along the
446
+ specified axis
447
+
448
+ Raises
449
+ ------
450
+ ParameterError
451
+ If ``size < data.shape[axis]``
452
+
453
+ See Also
454
+ --------
455
+ numpy.pad
456
+ """
457
+
458
+ kwargs.setdefault("mode", "constant")
459
+
460
+ n = data.shape[axis]
461
+
462
+ lpad = int((size - n) // 2)
463
+
464
+ lengths = [(0, 0)] * data.ndim
465
+ lengths[axis] = (lpad, int(size - n - lpad))
466
+
467
+ if lpad < 0:
468
+ raise ParameterError(
469
+ f"Target size ({size:d}) must be at least input size ({n:d})"
470
+ )
471
+
472
+ return np.pad(data, lengths, **kwargs)
473
+
474
+
475
+ def expand_to(
476
+ x: np.ndarray, *, ndim: int, axes: Union[int, slice, Sequence[int], Sequence[slice]]
477
+ ) -> np.ndarray:
478
+ """Expand the dimensions of an input array with
479
+
480
+ Parameters
481
+ ----------
482
+ x : np.ndarray
483
+ The input array
484
+ ndim : int
485
+ The number of dimensions to expand to. Must be at least ``x.ndim``
486
+ axes : int or slice
487
+ The target axis or axes to preserve from x.
488
+ All other axes will have length 1.
489
+
490
+ Returns
491
+ -------
492
+ x_exp : np.ndarray
493
+ The expanded version of ``x``, satisfying the following:
494
+ ``x_exp[axes] == x``
495
+ ``x_exp.ndim == ndim``
496
+
497
+ See Also
498
+ --------
499
+ np.expand_dims
500
+
501
+ Examples
502
+ --------
503
+ Expand a 1d array into an (n, 1) shape
504
+
505
+ >>> x = np.arange(3)
506
+ >>> librosa.util.expand_to(x, ndim=2, axes=0)
507
+ array([[0],
508
+ [1],
509
+ [2]])
510
+
511
+ Expand a 1d array into a (1, n) shape
512
+
513
+ >>> librosa.util.expand_to(x, ndim=2, axes=1)
514
+ array([[0, 1, 2]])
515
+
516
+ Expand a 2d array into (1, n, m, 1) shape
517
+
518
+ >>> x = np.vander(np.arange(3))
519
+ >>> librosa.util.expand_to(x, ndim=4, axes=[1,2]).shape
520
+ (1, 3, 3, 1)
521
+ """
522
+
523
+ # Force axes into a tuple
524
+ axes_tup: Tuple[int]
525
+ try:
526
+ axes_tup = tuple(axes) # type: ignore
527
+ except TypeError:
528
+ axes_tup = tuple([axes]) # type: ignore
529
+
530
+ if len(axes_tup) != x.ndim:
531
+ raise ParameterError(
532
+ f"Shape mismatch between axes={axes_tup} and input x.shape={x.shape}"
533
+ )
534
+
535
+ if ndim < x.ndim:
536
+ raise ParameterError(
537
+ f"Cannot expand x.shape={x.shape} to fewer dimensions ndim={ndim}"
538
+ )
539
+
540
+ shape: List[int] = [1] * ndim
541
+ for i, axi in enumerate(axes_tup):
542
+ shape[axi] = x.shape[i]
543
+
544
+ return x.reshape(shape)
545
+
546
+
547
+ def fix_length(
548
+ data: np.ndarray, *, size: int, axis: int = -1, **kwargs: Any
549
+ ) -> np.ndarray:
550
+ """Fix the length an array ``data`` to exactly ``size`` along a target axis.
551
+
552
+ If ``data.shape[axis] < n``, pad according to the provided kwargs.
553
+ By default, ``data`` is padded with trailing zeros.
554
+
555
+ Examples
556
+ --------
557
+ >>> y = np.arange(7)
558
+ >>> # Default: pad with zeros
559
+ >>> librosa.util.fix_length(y, size=10)
560
+ array([0, 1, 2, 3, 4, 5, 6, 0, 0, 0])
561
+ >>> # Trim to a desired length
562
+ >>> librosa.util.fix_length(y, size=5)
563
+ array([0, 1, 2, 3, 4])
564
+ >>> # Use edge-padding instead of zeros
565
+ >>> librosa.util.fix_length(y, size=10, mode='edge')
566
+ array([0, 1, 2, 3, 4, 5, 6, 6, 6, 6])
567
+
568
+ Parameters
569
+ ----------
570
+ data : np.ndarray
571
+ array to be length-adjusted
572
+ size : int >= 0 [scalar]
573
+ desired length of the array
574
+ axis : int, <= data.ndim
575
+ axis along which to fix length
576
+ **kwargs : additional keyword arguments
577
+ Parameters to ``np.pad``
578
+
579
+ Returns
580
+ -------
581
+ data_fixed : np.ndarray [shape=data.shape]
582
+ ``data`` either trimmed or padded to length ``size``
583
+ along the specified axis.
584
+
585
+ See Also
586
+ --------
587
+ numpy.pad
588
+ """
589
+
590
+ kwargs.setdefault("mode", "constant")
591
+
592
+ n = data.shape[axis]
593
+
594
+ if n > size:
595
+ slices = [slice(None)] * data.ndim
596
+ slices[axis] = slice(0, size)
597
+ return data[tuple(slices)]
598
+
599
+ elif n < size:
600
+ lengths = [(0, 0)] * data.ndim
601
+ lengths[axis] = (0, size - n)
602
+ return np.pad(data, lengths, **kwargs)
603
+
604
+ return data
605
+
606
+
607
+ def fix_frames(
608
+ frames: _SequenceLike[int],
609
+ *,
610
+ x_min: Optional[int] = 0,
611
+ x_max: Optional[int] = None,
612
+ pad: bool = True,
613
+ ) -> np.ndarray:
614
+ """Fix a list of frames to lie within [x_min, x_max]
615
+
616
+ Examples
617
+ --------
618
+ >>> # Generate a list of frame indices
619
+ >>> frames = np.arange(0, 1000.0, 50)
620
+ >>> frames
621
+ array([ 0., 50., 100., 150., 200., 250., 300., 350.,
622
+ 400., 450., 500., 550., 600., 650., 700., 750.,
623
+ 800., 850., 900., 950.])
624
+ >>> # Clip to span at most 250
625
+ >>> librosa.util.fix_frames(frames, x_max=250)
626
+ array([ 0, 50, 100, 150, 200, 250])
627
+ >>> # Or pad to span up to 2500
628
+ >>> librosa.util.fix_frames(frames, x_max=2500)
629
+ array([ 0, 50, 100, 150, 200, 250, 300, 350, 400,
630
+ 450, 500, 550, 600, 650, 700, 750, 800, 850,
631
+ 900, 950, 2500])
632
+ >>> librosa.util.fix_frames(frames, x_max=2500, pad=False)
633
+ array([ 0, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500,
634
+ 550, 600, 650, 700, 750, 800, 850, 900, 950])
635
+
636
+ >>> # Or starting away from zero
637
+ >>> frames = np.arange(200, 500, 33)
638
+ >>> frames
639
+ array([200, 233, 266, 299, 332, 365, 398, 431, 464, 497])
640
+ >>> librosa.util.fix_frames(frames)
641
+ array([ 0, 200, 233, 266, 299, 332, 365, 398, 431, 464, 497])
642
+ >>> librosa.util.fix_frames(frames, x_max=500)
643
+ array([ 0, 200, 233, 266, 299, 332, 365, 398, 431, 464, 497,
644
+ 500])
645
+
646
+ Parameters
647
+ ----------
648
+ frames : np.ndarray [shape=(n_frames,)]
649
+ List of non-negative frame indices
650
+ x_min : int >= 0 or None
651
+ Minimum allowed frame index
652
+ x_max : int >= 0 or None
653
+ Maximum allowed frame index
654
+ pad : boolean
655
+ If ``True``, then ``frames`` is expanded to span the full range
656
+ ``[x_min, x_max]``
657
+
658
+ Returns
659
+ -------
660
+ fixed_frames : np.ndarray [shape=(n_fixed_frames,), dtype=int]
661
+ Fixed frame indices, flattened and sorted
662
+
663
+ Raises
664
+ ------
665
+ ParameterError
666
+ If ``frames`` contains negative values
667
+ """
668
+
669
+ frames = np.asarray(frames)
670
+
671
+ if np.any(frames < 0):
672
+ raise ParameterError("Negative frame index detected")
673
+
674
+ # TODO: this whole function could be made more efficient
675
+
676
+ if pad and (x_min is not None or x_max is not None):
677
+ frames = np.clip(frames, x_min, x_max)
678
+
679
+ if pad:
680
+ pad_data = []
681
+ if x_min is not None:
682
+ pad_data.append(x_min)
683
+ if x_max is not None:
684
+ pad_data.append(x_max)
685
+ frames = np.concatenate((np.asarray(pad_data), frames))
686
+
687
+ if x_min is not None:
688
+ frames = frames[frames >= x_min]
689
+
690
+ if x_max is not None:
691
+ frames = frames[frames <= x_max]
692
+
693
+ unique: np.ndarray = np.unique(frames).astype(int)
694
+ return unique
695
+
696
+
697
+ @overload
698
+ def axis_sort(
699
+ S: np.ndarray,
700
+ *,
701
+ axis: int = ...,
702
+ index: Literal[False] = ...,
703
+ value: Optional[Callable[..., Any]] = ...,
704
+ ) -> np.ndarray:
705
+ ...
706
+
707
+
708
+ @overload
709
+ def axis_sort(
710
+ S: np.ndarray,
711
+ *,
712
+ axis: int = ...,
713
+ index: Literal[True],
714
+ value: Optional[Callable[..., Any]] = ...,
715
+ ) -> Tuple[np.ndarray, np.ndarray]:
716
+ ...
717
+
718
+
719
+ def axis_sort(
720
+ S: np.ndarray,
721
+ *,
722
+ axis: int = -1,
723
+ index: bool = False,
724
+ value: Optional[Callable[..., Any]] = None,
725
+ ) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]:
726
+ """Sort an array along its rows or columns.
727
+
728
+ Examples
729
+ --------
730
+ Visualize NMF output for a spectrogram S
731
+
732
+ >>> # Sort the columns of W by peak frequency bin
733
+ >>> y, sr = librosa.load(librosa.ex('trumpet'))
734
+ >>> S = np.abs(librosa.stft(y))
735
+ >>> W, H = librosa.decompose.decompose(S, n_components=64)
736
+ >>> W_sort = librosa.util.axis_sort(W)
737
+
738
+ Or sort by the lowest frequency bin
739
+
740
+ >>> W_sort = librosa.util.axis_sort(W, value=np.argmin)
741
+
742
+ Or sort the rows instead of the columns
743
+
744
+ >>> W_sort_rows = librosa.util.axis_sort(W, axis=0)
745
+
746
+ Get the sorting index also, and use it to permute the rows of H
747
+
748
+ >>> W_sort, idx = librosa.util.axis_sort(W, index=True)
749
+ >>> H_sort = H[idx, :]
750
+
751
+ >>> import matplotlib.pyplot as plt
752
+ >>> fig, ax = plt.subplots(nrows=2, ncols=2)
753
+ >>> img_w = librosa.display.specshow(librosa.amplitude_to_db(W, ref=np.max),
754
+ ... y_axis='log', ax=ax[0, 0])
755
+ >>> ax[0, 0].set(title='W')
756
+ >>> ax[0, 0].label_outer()
757
+ >>> img_act = librosa.display.specshow(H, x_axis='time', ax=ax[0, 1])
758
+ >>> ax[0, 1].set(title='H')
759
+ >>> ax[0, 1].label_outer()
760
+ >>> librosa.display.specshow(librosa.amplitude_to_db(W_sort,
761
+ ... ref=np.max),
762
+ ... y_axis='log', ax=ax[1, 0])
763
+ >>> ax[1, 0].set(title='W sorted')
764
+ >>> librosa.display.specshow(H_sort, x_axis='time', ax=ax[1, 1])
765
+ >>> ax[1, 1].set(title='H sorted')
766
+ >>> ax[1, 1].label_outer()
767
+ >>> fig.colorbar(img_w, ax=ax[:, 0], orientation='horizontal')
768
+ >>> fig.colorbar(img_act, ax=ax[:, 1], orientation='horizontal')
769
+
770
+ Parameters
771
+ ----------
772
+ S : np.ndarray [shape=(d, n)]
773
+ Array to be sorted
774
+
775
+ axis : int [scalar]
776
+ The axis along which to compute the sorting values
777
+
778
+ - ``axis=0`` to sort rows by peak column index
779
+ - ``axis=1`` to sort columns by peak row index
780
+
781
+ index : boolean [scalar]
782
+ If true, returns the index array as well as the permuted data.
783
+
784
+ value : function
785
+ function to return the index corresponding to the sort order.
786
+ Default: `np.argmax`.
787
+
788
+ Returns
789
+ -------
790
+ S_sort : np.ndarray [shape=(d, n)]
791
+ ``S`` with the columns or rows permuted in sorting order
792
+ idx : np.ndarray (optional) [shape=(d,) or (n,)]
793
+ If ``index == True``, the sorting index used to permute ``S``.
794
+ Length of ``idx`` corresponds to the selected ``axis``.
795
+
796
+ Raises
797
+ ------
798
+ ParameterError
799
+ If ``S`` does not have exactly 2 dimensions (``S.ndim != 2``)
800
+ """
801
+
802
+ if value is None:
803
+ value = np.argmax
804
+
805
+ if S.ndim != 2:
806
+ raise ParameterError("axis_sort is only defined for 2D arrays")
807
+
808
+ bin_idx = value(S, axis=np.mod(1 - axis, S.ndim))
809
+ idx = np.argsort(bin_idx)
810
+
811
+ sort_slice = [slice(None)] * S.ndim
812
+ sort_slice[axis] = idx # type: ignore
813
+
814
+ if index:
815
+ return S[tuple(sort_slice)], idx
816
+ else:
817
+ return S[tuple(sort_slice)]
818
+
819
+
820
+ @cache(level=40)
821
+ def normalize(
822
+ S: np.ndarray,
823
+ *,
824
+ norm: Optional[float] = np.inf,
825
+ axis: Optional[int] = 0,
826
+ threshold: Optional[_FloatLike_co] = None,
827
+ fill: Optional[bool] = None,
828
+ ) -> np.ndarray:
829
+ """Normalize an array along a chosen axis.
830
+
831
+ Given a norm (described below) and a target axis, the input
832
+ array is scaled so that::
833
+
834
+ norm(S, axis=axis) == 1
835
+
836
+ For example, ``axis=0`` normalizes each column of a 2-d array
837
+ by aggregating over the rows (0-axis).
838
+ Similarly, ``axis=1`` normalizes each row of a 2-d array.
839
+
840
+ This function also supports thresholding small-norm slices:
841
+ any slice (i.e., row or column) with norm below a specified
842
+ ``threshold`` can be left un-normalized, set to all-zeros, or
843
+ filled with uniform non-zero values that normalize to 1.
844
+
845
+ Note: the semantics of this function differ from
846
+ `scipy.linalg.norm` in two ways: multi-dimensional arrays
847
+ are supported, but matrix-norms are not.
848
+
849
+ Parameters
850
+ ----------
851
+ S : np.ndarray
852
+ The array to normalize
853
+
854
+ norm : {np.inf, -np.inf, 0, float > 0, None}
855
+ - `np.inf` : maximum absolute value
856
+ - `-np.inf` : minimum absolute value
857
+ - `0` : number of non-zeros (the support)
858
+ - float : corresponding l_p norm
859
+ See `scipy.linalg.norm` for details.
860
+ - None : no normalization is performed
861
+
862
+ axis : int [scalar]
863
+ Axis along which to compute the norm.
864
+
865
+ threshold : number > 0 [optional]
866
+ Only the columns (or rows) with norm at least ``threshold`` are
867
+ normalized.
868
+
869
+ By default, the threshold is determined from
870
+ the numerical precision of ``S.dtype``.
871
+
872
+ fill : None or bool
873
+ If None, then columns (or rows) with norm below ``threshold``
874
+ are left as is.
875
+
876
+ If False, then columns (rows) with norm below ``threshold``
877
+ are set to 0.
878
+
879
+ If True, then columns (rows) with norm below ``threshold``
880
+ are filled uniformly such that the corresponding norm is 1.
881
+
882
+ .. note:: ``fill=True`` is incompatible with ``norm=0`` because
883
+ no uniform vector exists with l0 "norm" equal to 1.
884
+
885
+ Returns
886
+ -------
887
+ S_norm : np.ndarray [shape=S.shape]
888
+ Normalized array
889
+
890
+ Raises
891
+ ------
892
+ ParameterError
893
+ If ``norm`` is not among the valid types defined above
894
+
895
+ If ``S`` is not finite
896
+
897
+ If ``fill=True`` and ``norm=0``
898
+
899
+ See Also
900
+ --------
901
+ scipy.linalg.norm
902
+
903
+ Notes
904
+ -----
905
+ This function caches at level 40.
906
+
907
+ Examples
908
+ --------
909
+ >>> # Construct an example matrix
910
+ >>> S = np.vander(np.arange(-2.0, 2.0))
911
+ >>> S
912
+ array([[-8., 4., -2., 1.],
913
+ [-1., 1., -1., 1.],
914
+ [ 0., 0., 0., 1.],
915
+ [ 1., 1., 1., 1.]])
916
+ >>> # Max (l-infinity)-normalize the columns
917
+ >>> librosa.util.normalize(S)
918
+ array([[-1. , 1. , -1. , 1. ],
919
+ [-0.125, 0.25 , -0.5 , 1. ],
920
+ [ 0. , 0. , 0. , 1. ],
921
+ [ 0.125, 0.25 , 0.5 , 1. ]])
922
+ >>> # Max (l-infinity)-normalize the rows
923
+ >>> librosa.util.normalize(S, axis=1)
924
+ array([[-1. , 0.5 , -0.25 , 0.125],
925
+ [-1. , 1. , -1. , 1. ],
926
+ [ 0. , 0. , 0. , 1. ],
927
+ [ 1. , 1. , 1. , 1. ]])
928
+ >>> # l1-normalize the columns
929
+ >>> librosa.util.normalize(S, norm=1)
930
+ array([[-0.8 , 0.667, -0.5 , 0.25 ],
931
+ [-0.1 , 0.167, -0.25 , 0.25 ],
932
+ [ 0. , 0. , 0. , 0.25 ],
933
+ [ 0.1 , 0.167, 0.25 , 0.25 ]])
934
+ >>> # l2-normalize the columns
935
+ >>> librosa.util.normalize(S, norm=2)
936
+ array([[-0.985, 0.943, -0.816, 0.5 ],
937
+ [-0.123, 0.236, -0.408, 0.5 ],
938
+ [ 0. , 0. , 0. , 0.5 ],
939
+ [ 0.123, 0.236, 0.408, 0.5 ]])
940
+
941
+ >>> # Thresholding and filling
942
+ >>> S[:, -1] = 1e-308
943
+ >>> S
944
+ array([[ -8.000e+000, 4.000e+000, -2.000e+000,
945
+ 1.000e-308],
946
+ [ -1.000e+000, 1.000e+000, -1.000e+000,
947
+ 1.000e-308],
948
+ [ 0.000e+000, 0.000e+000, 0.000e+000,
949
+ 1.000e-308],
950
+ [ 1.000e+000, 1.000e+000, 1.000e+000,
951
+ 1.000e-308]])
952
+
953
+ >>> # By default, small-norm columns are left untouched
954
+ >>> librosa.util.normalize(S)
955
+ array([[ -1.000e+000, 1.000e+000, -1.000e+000,
956
+ 1.000e-308],
957
+ [ -1.250e-001, 2.500e-001, -5.000e-001,
958
+ 1.000e-308],
959
+ [ 0.000e+000, 0.000e+000, 0.000e+000,
960
+ 1.000e-308],
961
+ [ 1.250e-001, 2.500e-001, 5.000e-001,
962
+ 1.000e-308]])
963
+ >>> # Small-norm columns can be zeroed out
964
+ >>> librosa.util.normalize(S, fill=False)
965
+ array([[-1. , 1. , -1. , 0. ],
966
+ [-0.125, 0.25 , -0.5 , 0. ],
967
+ [ 0. , 0. , 0. , 0. ],
968
+ [ 0.125, 0.25 , 0.5 , 0. ]])
969
+ >>> # Or set to constant with unit-norm
970
+ >>> librosa.util.normalize(S, fill=True)
971
+ array([[-1. , 1. , -1. , 1. ],
972
+ [-0.125, 0.25 , -0.5 , 1. ],
973
+ [ 0. , 0. , 0. , 1. ],
974
+ [ 0.125, 0.25 , 0.5 , 1. ]])
975
+ >>> # With an l1 norm instead of max-norm
976
+ >>> librosa.util.normalize(S, norm=1, fill=True)
977
+ array([[-0.8 , 0.667, -0.5 , 0.25 ],
978
+ [-0.1 , 0.167, -0.25 , 0.25 ],
979
+ [ 0. , 0. , 0. , 0.25 ],
980
+ [ 0.1 , 0.167, 0.25 , 0.25 ]])
981
+ """
982
+
983
+ # Avoid div-by-zero
984
+ if threshold is None:
985
+ threshold = tiny(S)
986
+
987
+ elif threshold <= 0:
988
+ raise ParameterError(f"threshold={threshold} must be strictly positive")
989
+
990
+ if fill not in [None, False, True]:
991
+ raise ParameterError(f"fill={fill} must be None or boolean")
992
+
993
+ if not np.all(np.isfinite(S)):
994
+ raise ParameterError("Input must be finite")
995
+
996
+ # All norms only depend on magnitude, let's do that first
997
+ mag = np.abs(S).astype(float)
998
+
999
+ # For max/min norms, filling with 1 works
1000
+ fill_norm = 1
1001
+
1002
+ if norm is None:
1003
+ return S
1004
+
1005
+ elif norm == np.inf:
1006
+ length = np.max(mag, axis=axis, keepdims=True)
1007
+
1008
+ elif norm == -np.inf:
1009
+ length = np.min(mag, axis=axis, keepdims=True)
1010
+
1011
+ elif norm == 0:
1012
+ if fill is True:
1013
+ raise ParameterError("Cannot normalize with norm=0 and fill=True")
1014
+
1015
+ length = np.sum(mag > 0, axis=axis, keepdims=True, dtype=mag.dtype)
1016
+
1017
+ elif np.issubdtype(type(norm), np.number) and norm > 0:
1018
+ length = np.sum(mag**norm, axis=axis, keepdims=True) ** (1.0 / norm)
1019
+
1020
+ if axis is None:
1021
+ fill_norm = mag.size ** (-1.0 / norm)
1022
+ else:
1023
+ fill_norm = mag.shape[axis] ** (-1.0 / norm)
1024
+
1025
+ else:
1026
+ raise ParameterError(f"Unsupported norm: {repr(norm)}")
1027
+
1028
+ # indices where norm is below the threshold
1029
+ small_idx = length < threshold
1030
+
1031
+ Snorm = np.empty_like(S)
1032
+ if fill is None:
1033
+ # Leave small indices un-normalized
1034
+ length[small_idx] = 1.0
1035
+ Snorm[:] = S / length
1036
+
1037
+ elif fill:
1038
+ # If we have a non-zero fill value, we locate those entries by
1039
+ # doing a nan-divide.
1040
+ # If S was finite, then length is finite (except for small positions)
1041
+ length[small_idx] = np.nan
1042
+ Snorm[:] = S / length
1043
+ Snorm[np.isnan(Snorm)] = fill_norm
1044
+ else:
1045
+ # Set small values to zero by doing an inf-divide.
1046
+ # This is safe (by IEEE-754) as long as S is finite.
1047
+ length[small_idx] = np.inf
1048
+ Snorm[:] = S / length
1049
+
1050
+ return Snorm
1051
+
1052
+
1053
+ @numba.stencil
1054
+ def _localmax_sten(x): # pragma: no cover
1055
+ """Numba stencil for local maxima computation"""
1056
+ return (x[0] > x[-1]) & (x[0] >= x[1])
1057
+
1058
+
1059
+ @numba.stencil
1060
+ def _localmin_sten(x): # pragma: no cover
1061
+ """Numba stencil for local minima computation"""
1062
+ return (x[0] < x[-1]) & (x[0] <= x[1])
1063
+
1064
+
1065
+ @numba.guvectorize(
1066
+ [
1067
+ "void(int16[:], bool_[:])",
1068
+ "void(int32[:], bool_[:])",
1069
+ "void(int64[:], bool_[:])",
1070
+ "void(float32[:], bool_[:])",
1071
+ "void(float64[:], bool_[:])",
1072
+ ],
1073
+ "(n)->(n)",
1074
+ cache=True,
1075
+ nopython=True,
1076
+ )
1077
+ def _localmax(x, y): # pragma: no cover
1078
+ """Vectorized wrapper for the localmax stencil"""
1079
+ y[:] = _localmax_sten(x)
1080
+
1081
+
1082
+ @numba.guvectorize(
1083
+ [
1084
+ "void(int16[:], bool_[:])",
1085
+ "void(int32[:], bool_[:])",
1086
+ "void(int64[:], bool_[:])",
1087
+ "void(float32[:], bool_[:])",
1088
+ "void(float64[:], bool_[:])",
1089
+ ],
1090
+ "(n)->(n)",
1091
+ cache=True,
1092
+ nopython=True,
1093
+ )
1094
+ def _localmin(x, y): # pragma: no cover
1095
+ """Vectorized wrapper for the localmin stencil"""
1096
+ y[:] = _localmin_sten(x)
1097
+
1098
+
1099
+ def localmax(x: np.ndarray, *, axis: int = 0) -> np.ndarray:
1100
+ """Find local maxima in an array
1101
+
1102
+ An element ``x[i]`` is considered a local maximum if the following
1103
+ conditions are met:
1104
+
1105
+ - ``x[i] > x[i-1]``
1106
+ - ``x[i] >= x[i+1]``
1107
+
1108
+ Note that the first condition is strict, and that the first element
1109
+ ``x[0]`` will never be considered as a local maximum.
1110
+
1111
+ Examples
1112
+ --------
1113
+ >>> x = np.array([1, 0, 1, 2, -1, 0, -2, 1])
1114
+ >>> librosa.util.localmax(x)
1115
+ array([False, False, False, True, False, True, False, True], dtype=bool)
1116
+
1117
+ >>> # Two-dimensional example
1118
+ >>> x = np.array([[1,0,1], [2, -1, 0], [2, 1, 3]])
1119
+ >>> librosa.util.localmax(x, axis=0)
1120
+ array([[False, False, False],
1121
+ [ True, False, False],
1122
+ [False, True, True]], dtype=bool)
1123
+ >>> librosa.util.localmax(x, axis=1)
1124
+ array([[False, False, True],
1125
+ [False, False, True],
1126
+ [False, False, True]], dtype=bool)
1127
+
1128
+ Parameters
1129
+ ----------
1130
+ x : np.ndarray [shape=(d1,d2,...)]
1131
+ input vector or array
1132
+ axis : int
1133
+ axis along which to compute local maximality
1134
+
1135
+ Returns
1136
+ -------
1137
+ m : np.ndarray [shape=x.shape, dtype=bool]
1138
+ indicator array of local maximality along ``axis``
1139
+
1140
+ See Also
1141
+ --------
1142
+ localmin
1143
+ """
1144
+ # Rotate the target axis to the end
1145
+ xi = x.swapaxes(-1, axis)
1146
+
1147
+ # Allocate the output array and rotate target axis
1148
+ lmax = np.empty_like(x, dtype=bool)
1149
+ lmaxi = lmax.swapaxes(-1, axis)
1150
+
1151
+ # Call the vectorized stencil
1152
+ _localmax(xi, lmaxi)
1153
+
1154
+ # Handle the edge condition not covered by the stencil
1155
+ lmaxi[..., -1] = xi[..., -1] > xi[..., -2]
1156
+
1157
+ return lmax
1158
+
1159
+
1160
+ def localmin(x: np.ndarray, *, axis: int = 0) -> np.ndarray:
1161
+ """Find local minima in an array
1162
+
1163
+ An element ``x[i]`` is considered a local minimum if the following
1164
+ conditions are met:
1165
+
1166
+ - ``x[i] < x[i-1]``
1167
+ - ``x[i] <= x[i+1]``
1168
+
1169
+ Note that the first condition is strict, and that the first element
1170
+ ``x[0]`` will never be considered as a local minimum.
1171
+
1172
+ Examples
1173
+ --------
1174
+ >>> x = np.array([1, 0, 1, 2, -1, 0, -2, 1])
1175
+ >>> librosa.util.localmin(x)
1176
+ array([False, True, False, False, True, False, True, False])
1177
+
1178
+ >>> # Two-dimensional example
1179
+ >>> x = np.array([[1,0,1], [2, -1, 0], [2, 1, 3]])
1180
+ >>> librosa.util.localmin(x, axis=0)
1181
+ array([[False, False, False],
1182
+ [False, True, True],
1183
+ [False, False, False]])
1184
+
1185
+ >>> librosa.util.localmin(x, axis=1)
1186
+ array([[False, True, False],
1187
+ [False, True, False],
1188
+ [False, True, False]])
1189
+
1190
+ Parameters
1191
+ ----------
1192
+ x : np.ndarray [shape=(d1,d2,...)]
1193
+ input vector or array
1194
+ axis : int
1195
+ axis along which to compute local minimality
1196
+
1197
+ Returns
1198
+ -------
1199
+ m : np.ndarray [shape=x.shape, dtype=bool]
1200
+ indicator array of local minimality along ``axis``
1201
+
1202
+ See Also
1203
+ --------
1204
+ localmax
1205
+ """
1206
+ # Rotate the target axis to the end
1207
+ xi = x.swapaxes(-1, axis)
1208
+
1209
+ # Allocate the output array and rotate target axis
1210
+ lmin = np.empty_like(x, dtype=bool)
1211
+ lmini = lmin.swapaxes(-1, axis)
1212
+
1213
+ # Call the vectorized stencil
1214
+ _localmin(xi, lmini)
1215
+
1216
+ # Handle the edge condition not covered by the stencil
1217
+ lmini[..., -1] = xi[..., -1] < xi[..., -2]
1218
+
1219
+ return lmin
1220
+
1221
+
1222
+ def peak_pick(
1223
+ x: np.ndarray,
1224
+ *,
1225
+ pre_max: int,
1226
+ post_max: int,
1227
+ pre_avg: int,
1228
+ post_avg: int,
1229
+ delta: float,
1230
+ wait: int,
1231
+ ) -> np.ndarray:
1232
+ """Uses a flexible heuristic to pick peaks in a signal.
1233
+
1234
+ A sample n is selected as an peak if the corresponding ``x[n]``
1235
+ fulfills the following three conditions:
1236
+
1237
+ 1. ``x[n] == max(x[n - pre_max:n + post_max])``
1238
+ 2. ``x[n] >= mean(x[n - pre_avg:n + post_avg]) + delta``
1239
+ 3. ``n - previous_n > wait``
1240
+
1241
+ where ``previous_n`` is the last sample picked as a peak (greedily).
1242
+
1243
+ This implementation is based on [#]_ and [#]_.
1244
+
1245
+ .. [#] Boeck, Sebastian, Florian Krebs, and Markus Schedl.
1246
+ "Evaluating the Online Capabilities of Onset Detection Methods." ISMIR.
1247
+ 2012.
1248
+
1249
+ .. [#] https://github.com/CPJKU/onset_detection/blob/master/onset_program.py
1250
+
1251
+ Parameters
1252
+ ----------
1253
+ x : np.ndarray [shape=(n,)]
1254
+ input signal to peak picks from
1255
+ pre_max : int >= 0 [scalar]
1256
+ number of samples before ``n`` over which max is computed
1257
+ post_max : int >= 1 [scalar]
1258
+ number of samples after ``n`` over which max is computed
1259
+ pre_avg : int >= 0 [scalar]
1260
+ number of samples before ``n`` over which mean is computed
1261
+ post_avg : int >= 1 [scalar]
1262
+ number of samples after ``n`` over which mean is computed
1263
+ delta : float >= 0 [scalar]
1264
+ threshold offset for mean
1265
+ wait : int >= 0 [scalar]
1266
+ number of samples to wait after picking a peak
1267
+
1268
+ Returns
1269
+ -------
1270
+ peaks : np.ndarray [shape=(n_peaks,), dtype=int]
1271
+ indices of peaks in ``x``
1272
+
1273
+ Raises
1274
+ ------
1275
+ ParameterError
1276
+ If any input lies outside its defined range
1277
+
1278
+ Examples
1279
+ --------
1280
+ >>> y, sr = librosa.load(librosa.ex('trumpet'))
1281
+ >>> onset_env = librosa.onset.onset_strength(y=y, sr=sr,
1282
+ ... hop_length=512,
1283
+ ... aggregate=np.median)
1284
+ >>> peaks = librosa.util.peak_pick(onset_env, pre_max=3, post_max=3, pre_avg=3, post_avg=5, delta=0.5, wait=10)
1285
+ >>> peaks
1286
+ array([ 3, 27, 40, 61, 72, 88, 103])
1287
+
1288
+ >>> import matplotlib.pyplot as plt
1289
+ >>> times = librosa.times_like(onset_env, sr=sr, hop_length=512)
1290
+ >>> fig, ax = plt.subplots(nrows=2, sharex=True)
1291
+ >>> D = np.abs(librosa.stft(y))
1292
+ >>> librosa.display.specshow(librosa.amplitude_to_db(D, ref=np.max),
1293
+ ... y_axis='log', x_axis='time', ax=ax[1])
1294
+ >>> ax[0].plot(times, onset_env, alpha=0.8, label='Onset strength')
1295
+ >>> ax[0].vlines(times[peaks], 0,
1296
+ ... onset_env.max(), color='r', alpha=0.8,
1297
+ ... label='Selected peaks')
1298
+ >>> ax[0].legend(frameon=True, framealpha=0.8)
1299
+ >>> ax[0].label_outer()
1300
+ """
1301
+
1302
+ if pre_max < 0:
1303
+ raise ParameterError("pre_max must be non-negative")
1304
+ if pre_avg < 0:
1305
+ raise ParameterError("pre_avg must be non-negative")
1306
+ if delta < 0:
1307
+ raise ParameterError("delta must be non-negative")
1308
+ if wait < 0:
1309
+ raise ParameterError("wait must be non-negative")
1310
+
1311
+ if post_max <= 0:
1312
+ raise ParameterError("post_max must be positive")
1313
+
1314
+ if post_avg <= 0:
1315
+ raise ParameterError("post_avg must be positive")
1316
+
1317
+ if x.ndim != 1:
1318
+ raise ParameterError("input array must be one-dimensional")
1319
+
1320
+ # Ensure valid index types
1321
+ pre_max = valid_int(pre_max, cast=np.ceil)
1322
+ post_max = valid_int(post_max, cast=np.ceil)
1323
+ pre_avg = valid_int(pre_avg, cast=np.ceil)
1324
+ post_avg = valid_int(post_avg, cast=np.ceil)
1325
+ wait = valid_int(wait, cast=np.ceil)
1326
+
1327
+ # Get the maximum of the signal over a sliding window
1328
+ max_length = pre_max + post_max
1329
+ max_origin = np.ceil(0.5 * (pre_max - post_max))
1330
+ # Using mode='constant' and cval=x.min() effectively truncates
1331
+ # the sliding window at the boundaries
1332
+ mov_max = scipy.ndimage.filters.maximum_filter1d(
1333
+ x, int(max_length), mode="constant", origin=int(max_origin), cval=x.min()
1334
+ )
1335
+
1336
+ # Get the mean of the signal over a sliding window
1337
+ avg_length = pre_avg + post_avg
1338
+ avg_origin = np.ceil(0.5 * (pre_avg - post_avg))
1339
+ # Here, there is no mode which results in the behavior we want,
1340
+ # so we'll correct below.
1341
+ mov_avg = scipy.ndimage.filters.uniform_filter1d(
1342
+ x, int(avg_length), mode="nearest", origin=int(avg_origin)
1343
+ )
1344
+
1345
+ # Correct sliding average at the beginning
1346
+ n = 0
1347
+ # Only need to correct in the range where the window needs to be truncated
1348
+ while n - pre_avg < 0 and n < x.shape[0]:
1349
+ # This just explicitly does mean(x[n - pre_avg:n + post_avg])
1350
+ # with truncation
1351
+ start = n - pre_avg
1352
+ start = start if start > 0 else 0
1353
+ mov_avg[n] = np.mean(x[start : n + post_avg])
1354
+ n += 1
1355
+ # Correct sliding average at the end
1356
+ n = x.shape[0] - post_avg
1357
+ # When post_avg > x.shape[0] (weird case), reset to 0
1358
+ n = n if n > 0 else 0
1359
+ while n < x.shape[0]:
1360
+ start = n - pre_avg
1361
+ start = start if start > 0 else 0
1362
+ mov_avg[n] = np.mean(x[start : n + post_avg])
1363
+ n += 1
1364
+
1365
+ # First mask out all entries not equal to the local max
1366
+ detections = x * (x == mov_max)
1367
+
1368
+ # Then mask out all entries less than the thresholded average
1369
+ detections = detections * (detections >= (mov_avg + delta))
1370
+
1371
+ # Initialize peaks array, to be filled greedily
1372
+ peaks = []
1373
+
1374
+ # Remove onsets which are close together in time
1375
+ last_onset = -np.inf
1376
+
1377
+ for i in np.nonzero(detections)[0]:
1378
+ # Only report an onset if the "wait" samples was reported
1379
+ if i > last_onset + wait:
1380
+ peaks.append(i)
1381
+ # Save last reported onset
1382
+ last_onset = i
1383
+
1384
+ return np.array(peaks)
1385
+
1386
+
1387
+ @cache(level=40)
1388
+ def sparsify_rows(
1389
+ x: np.ndarray, *, quantile: float = 0.01, dtype: Optional[DTypeLike] = None
1390
+ ) -> scipy.sparse.csr_matrix:
1391
+ """Return a row-sparse matrix approximating the input
1392
+
1393
+ Parameters
1394
+ ----------
1395
+ x : np.ndarray [ndim <= 2]
1396
+ The input matrix to sparsify.
1397
+ quantile : float in [0, 1.0)
1398
+ Percentage of magnitude to discard in each row of ``x``
1399
+ dtype : np.dtype, optional
1400
+ The dtype of the output array.
1401
+ If not provided, then ``x.dtype`` will be used.
1402
+
1403
+ Returns
1404
+ -------
1405
+ x_sparse : ``scipy.sparse.csr_matrix`` [shape=x.shape]
1406
+ Row-sparsified approximation of ``x``
1407
+
1408
+ If ``x.ndim == 1``, then ``x`` is interpreted as a row vector,
1409
+ and ``x_sparse.shape == (1, len(x))``.
1410
+
1411
+ Raises
1412
+ ------
1413
+ ParameterError
1414
+ If ``x.ndim > 2``
1415
+
1416
+ If ``quantile`` lies outside ``[0, 1.0)``
1417
+
1418
+ Notes
1419
+ -----
1420
+ This function caches at level 40.
1421
+
1422
+ Examples
1423
+ --------
1424
+ >>> # Construct a Hann window to sparsify
1425
+ >>> x = scipy.signal.hann(32)
1426
+ >>> x
1427
+ array([ 0. , 0.01 , 0.041, 0.09 , 0.156, 0.236, 0.326,
1428
+ 0.424, 0.525, 0.625, 0.72 , 0.806, 0.879, 0.937,
1429
+ 0.977, 0.997, 0.997, 0.977, 0.937, 0.879, 0.806,
1430
+ 0.72 , 0.625, 0.525, 0.424, 0.326, 0.236, 0.156,
1431
+ 0.09 , 0.041, 0.01 , 0. ])
1432
+ >>> # Discard the bottom percentile
1433
+ >>> x_sparse = librosa.util.sparsify_rows(x, quantile=0.01)
1434
+ >>> x_sparse
1435
+ <1x32 sparse matrix of type '<type 'numpy.float64'>'
1436
+ with 26 stored elements in Compressed Sparse Row format>
1437
+ >>> x_sparse.todense()
1438
+ matrix([[ 0. , 0. , 0. , 0.09 , 0.156, 0.236, 0.326,
1439
+ 0.424, 0.525, 0.625, 0.72 , 0.806, 0.879, 0.937,
1440
+ 0.977, 0.997, 0.997, 0.977, 0.937, 0.879, 0.806,
1441
+ 0.72 , 0.625, 0.525, 0.424, 0.326, 0.236, 0.156,
1442
+ 0.09 , 0. , 0. , 0. ]])
1443
+ >>> # Discard up to the bottom 10th percentile
1444
+ >>> x_sparse = librosa.util.sparsify_rows(x, quantile=0.1)
1445
+ >>> x_sparse
1446
+ <1x32 sparse matrix of type '<type 'numpy.float64'>'
1447
+ with 20 stored elements in Compressed Sparse Row format>
1448
+ >>> x_sparse.todense()
1449
+ matrix([[ 0. , 0. , 0. , 0. , 0. , 0. , 0.326,
1450
+ 0.424, 0.525, 0.625, 0.72 , 0.806, 0.879, 0.937,
1451
+ 0.977, 0.997, 0.997, 0.977, 0.937, 0.879, 0.806,
1452
+ 0.72 , 0.625, 0.525, 0.424, 0.326, 0. , 0. ,
1453
+ 0. , 0. , 0. , 0. ]])
1454
+ """
1455
+
1456
+ if x.ndim == 1:
1457
+ x = x.reshape((1, -1))
1458
+
1459
+ elif x.ndim > 2:
1460
+ raise ParameterError(
1461
+ f"Input must have 2 or fewer dimensions. Provided x.shape={x.shape}."
1462
+ )
1463
+
1464
+ if not 0.0 <= quantile < 1:
1465
+ raise ParameterError(f"Invalid quantile {quantile:.2f}")
1466
+
1467
+ if dtype is None:
1468
+ dtype = x.dtype
1469
+
1470
+ x_sparse = scipy.sparse.lil_matrix(x.shape, dtype=dtype)
1471
+
1472
+ mags = np.abs(x)
1473
+ norms = np.sum(mags, axis=1, keepdims=True)
1474
+
1475
+ mag_sort = np.sort(mags, axis=1)
1476
+ cumulative_mag = np.cumsum(mag_sort / norms, axis=1)
1477
+
1478
+ threshold_idx = np.argmin(cumulative_mag < quantile, axis=1)
1479
+
1480
+ for i, j in enumerate(threshold_idx):
1481
+ idx = np.where(mags[i] >= mag_sort[i, j])
1482
+ x_sparse[i, idx] = x[i, idx]
1483
+
1484
+ return x_sparse.tocsr()
1485
+
1486
+
1487
+ def buf_to_float(
1488
+ x: np.ndarray, *, n_bytes: int = 2, dtype: DTypeLike = np.float32
1489
+ ) -> np.ndarray:
1490
+ """Convert an integer buffer to floating point values.
1491
+ This is primarily useful when loading integer-valued wav data
1492
+ into numpy arrays.
1493
+
1494
+ Parameters
1495
+ ----------
1496
+ x : np.ndarray [dtype=int]
1497
+ The integer-valued data buffer
1498
+ n_bytes : int [1, 2, 4]
1499
+ The number of bytes per sample in ``x``
1500
+ dtype : numeric type
1501
+ The target output type (default: 32-bit float)
1502
+
1503
+ Returns
1504
+ -------
1505
+ x_float : np.ndarray [dtype=float]
1506
+ The input data buffer cast to floating point
1507
+ """
1508
+
1509
+ # Invert the scale of the data
1510
+ scale = 1.0 / float(1 << ((8 * n_bytes) - 1))
1511
+
1512
+ # Construct the format string
1513
+ fmt = f"<i{n_bytes:d}"
1514
+
1515
+ # Rescale and format the data buffer
1516
+ return scale * np.frombuffer(x, fmt).astype(dtype)
1517
+
1518
+
1519
+ def index_to_slice(
1520
+ idx: _SequenceLike[int],
1521
+ *,
1522
+ idx_min: Optional[int] = None,
1523
+ idx_max: Optional[int] = None,
1524
+ step: Optional[int] = None,
1525
+ pad: bool = True,
1526
+ ) -> List[slice]:
1527
+ """Generate a slice array from an index array.
1528
+
1529
+ Parameters
1530
+ ----------
1531
+ idx : list-like
1532
+ Array of index boundaries
1533
+ idx_min, idx_max : None or int
1534
+ Minimum and maximum allowed indices
1535
+ step : None or int
1536
+ Step size for each slice. If `None`, then the default
1537
+ step of 1 is used.
1538
+ pad : boolean
1539
+ If `True`, pad ``idx`` to span the range ``idx_min:idx_max``.
1540
+
1541
+ Returns
1542
+ -------
1543
+ slices : list of slice
1544
+ ``slices[i] = slice(idx[i], idx[i+1], step)``
1545
+ Additional slice objects may be added at the beginning or end,
1546
+ depending on whether ``pad==True`` and the supplied values for
1547
+ ``idx_min`` and ``idx_max``.
1548
+
1549
+ See Also
1550
+ --------
1551
+ fix_frames
1552
+
1553
+ Examples
1554
+ --------
1555
+ >>> # Generate slices from spaced indices
1556
+ >>> librosa.util.index_to_slice(np.arange(20, 100, 15))
1557
+ [slice(20, 35, None), slice(35, 50, None), slice(50, 65, None), slice(65, 80, None),
1558
+ slice(80, 95, None)]
1559
+ >>> # Pad to span the range (0, 100)
1560
+ >>> librosa.util.index_to_slice(np.arange(20, 100, 15),
1561
+ ... idx_min=0, idx_max=100)
1562
+ [slice(0, 20, None), slice(20, 35, None), slice(35, 50, None), slice(50, 65, None),
1563
+ slice(65, 80, None), slice(80, 95, None), slice(95, 100, None)]
1564
+ >>> # Use a step of 5 for each slice
1565
+ >>> librosa.util.index_to_slice(np.arange(20, 100, 15),
1566
+ ... idx_min=0, idx_max=100, step=5)
1567
+ [slice(0, 20, 5), slice(20, 35, 5), slice(35, 50, 5), slice(50, 65, 5), slice(65, 80, 5),
1568
+ slice(80, 95, 5), slice(95, 100, 5)]
1569
+ """
1570
+
1571
+ # First, normalize the index set
1572
+ idx_fixed = fix_frames(idx, x_min=idx_min, x_max=idx_max, pad=pad)
1573
+
1574
+ # Now convert the indices to slices
1575
+ return [slice(start, end, step) for (start, end) in zip(idx_fixed, idx_fixed[1:])]
1576
+
1577
+
1578
+ @cache(level=40)
1579
+ def sync(
1580
+ data: np.ndarray,
1581
+ idx: Union[Sequence[int], Sequence[slice]],
1582
+ *,
1583
+ aggregate: Optional[Callable[..., Any]] = None,
1584
+ pad: bool = True,
1585
+ axis: int = -1,
1586
+ ) -> np.ndarray:
1587
+ """Synchronous aggregation of a multi-dimensional array between boundaries
1588
+
1589
+ .. note::
1590
+ In order to ensure total coverage, boundary points may be added
1591
+ to ``idx``.
1592
+
1593
+ If synchronizing a feature matrix against beat tracker output, ensure
1594
+ that frame index numbers are properly aligned and use the same hop length.
1595
+
1596
+ Parameters
1597
+ ----------
1598
+ data : np.ndarray
1599
+ multi-dimensional array of features
1600
+ idx : sequence of ints or slices
1601
+ Either an ordered array of boundary indices, or
1602
+ an iterable collection of slice objects.
1603
+ aggregate : function
1604
+ aggregation function (default: `np.mean`)
1605
+ pad : boolean
1606
+ If `True`, ``idx`` is padded to span the full range ``[0, data.shape[axis]]``
1607
+ axis : int
1608
+ The axis along which to aggregate data
1609
+
1610
+ Returns
1611
+ -------
1612
+ data_sync : ndarray
1613
+ ``data_sync`` will have the same dimension as ``data``, except that the ``axis``
1614
+ coordinate will be reduced according to ``idx``.
1615
+
1616
+ For example, a 2-dimensional ``data`` with ``axis=-1`` should satisfy::
1617
+
1618
+ data_sync[:, i] = aggregate(data[:, idx[i-1]:idx[i]], axis=-1)
1619
+
1620
+ Raises
1621
+ ------
1622
+ ParameterError
1623
+ If the index set is not of consistent type (all slices or all integers)
1624
+
1625
+ Notes
1626
+ -----
1627
+ This function caches at level 40.
1628
+
1629
+ Examples
1630
+ --------
1631
+ Beat-synchronous CQT spectra
1632
+
1633
+ >>> y, sr = librosa.load(librosa.ex('choice'))
1634
+ >>> tempo, beats = librosa.beat.beat_track(y=y, sr=sr, trim=False)
1635
+ >>> C = np.abs(librosa.cqt(y=y, sr=sr))
1636
+ >>> beats = librosa.util.fix_frames(beats)
1637
+
1638
+ By default, use mean aggregation
1639
+
1640
+ >>> C_avg = librosa.util.sync(C, beats)
1641
+
1642
+ Use median-aggregation instead of mean
1643
+
1644
+ >>> C_med = librosa.util.sync(C, beats,
1645
+ ... aggregate=np.median)
1646
+
1647
+ Or sub-beat synchronization
1648
+
1649
+ >>> sub_beats = librosa.segment.subsegment(C, beats)
1650
+ >>> sub_beats = librosa.util.fix_frames(sub_beats)
1651
+ >>> C_med_sub = librosa.util.sync(C, sub_beats, aggregate=np.median)
1652
+
1653
+ Plot the results
1654
+
1655
+ >>> import matplotlib.pyplot as plt
1656
+ >>> beat_t = librosa.frames_to_time(beats, sr=sr)
1657
+ >>> subbeat_t = librosa.frames_to_time(sub_beats, sr=sr)
1658
+ >>> fig, ax = plt.subplots(nrows=3, sharex=True, sharey=True)
1659
+ >>> librosa.display.specshow(librosa.amplitude_to_db(C,
1660
+ ... ref=np.max),
1661
+ ... x_axis='time', ax=ax[0])
1662
+ >>> ax[0].set(title='CQT power, shape={}'.format(C.shape))
1663
+ >>> ax[0].label_outer()
1664
+ >>> librosa.display.specshow(librosa.amplitude_to_db(C_med,
1665
+ ... ref=np.max),
1666
+ ... x_coords=beat_t, x_axis='time', ax=ax[1])
1667
+ >>> ax[1].set(title='Beat synchronous CQT power, '
1668
+ ... 'shape={}'.format(C_med.shape))
1669
+ >>> ax[1].label_outer()
1670
+ >>> librosa.display.specshow(librosa.amplitude_to_db(C_med_sub,
1671
+ ... ref=np.max),
1672
+ ... x_coords=subbeat_t, x_axis='time', ax=ax[2])
1673
+ >>> ax[2].set(title='Sub-beat synchronous CQT power, '
1674
+ ... 'shape={}'.format(C_med_sub.shape))
1675
+ """
1676
+
1677
+ if aggregate is None:
1678
+ aggregate = np.mean
1679
+
1680
+ shape = list(data.shape)
1681
+
1682
+ if np.all([isinstance(_, slice) for _ in idx]):
1683
+ slices = idx
1684
+ elif np.all([np.issubdtype(type(_), np.integer) for _ in idx]):
1685
+ slices = index_to_slice(
1686
+ np.asarray(idx), idx_min=0, idx_max=shape[axis], pad=pad
1687
+ )
1688
+ else:
1689
+ raise ParameterError(f"Invalid index set: {idx}")
1690
+
1691
+ agg_shape = list(shape)
1692
+ agg_shape[axis] = len(slices)
1693
+
1694
+ data_agg = np.empty(
1695
+ agg_shape, order="F" if np.isfortran(data) else "C", dtype=data.dtype
1696
+ )
1697
+
1698
+ idx_in = [slice(None)] * data.ndim
1699
+ idx_agg = [slice(None)] * data_agg.ndim
1700
+
1701
+ for i, segment in enumerate(slices):
1702
+ idx_in[axis] = segment # type: ignore
1703
+ idx_agg[axis] = i # type: ignore
1704
+ data_agg[tuple(idx_agg)] = aggregate(data[tuple(idx_in)], axis=axis)
1705
+
1706
+ return data_agg
1707
+
1708
+
1709
+ def softmask(
1710
+ X: np.ndarray, X_ref: np.ndarray, *, power: float = 1, split_zeros: bool = False
1711
+ ) -> np.ndarray:
1712
+ """Robustly compute a soft-mask operation.
1713
+
1714
+ ``M = X**power / (X**power + X_ref**power)``
1715
+
1716
+ Parameters
1717
+ ----------
1718
+ X : np.ndarray
1719
+ The (non-negative) input array corresponding to the positive mask elements
1720
+
1721
+ X_ref : np.ndarray
1722
+ The (non-negative) array of reference or background elements.
1723
+ Must have the same shape as ``X``.
1724
+
1725
+ power : number > 0 or np.inf
1726
+ If finite, returns the soft mask computed in a numerically stable way
1727
+
1728
+ If infinite, returns a hard (binary) mask equivalent to ``X > X_ref``.
1729
+ Note: for hard masks, ties are always broken in favor of ``X_ref`` (``mask=0``).
1730
+
1731
+ split_zeros : bool
1732
+ If `True`, entries where ``X`` and ``X_ref`` are both small (close to 0)
1733
+ will receive mask values of 0.5.
1734
+
1735
+ Otherwise, the mask is set to 0 for these entries.
1736
+
1737
+ Returns
1738
+ -------
1739
+ mask : np.ndarray, shape=X.shape
1740
+ The output mask array
1741
+
1742
+ Raises
1743
+ ------
1744
+ ParameterError
1745
+ If ``X`` and ``X_ref`` have different shapes.
1746
+
1747
+ If ``X`` or ``X_ref`` are negative anywhere
1748
+
1749
+ If ``power <= 0``
1750
+
1751
+ Examples
1752
+ --------
1753
+ >>> X = 2 * np.ones((3, 3))
1754
+ >>> X_ref = np.vander(np.arange(3.0))
1755
+ >>> X
1756
+ array([[ 2., 2., 2.],
1757
+ [ 2., 2., 2.],
1758
+ [ 2., 2., 2.]])
1759
+ >>> X_ref
1760
+ array([[ 0., 0., 1.],
1761
+ [ 1., 1., 1.],
1762
+ [ 4., 2., 1.]])
1763
+ >>> librosa.util.softmask(X, X_ref, power=1)
1764
+ array([[ 1. , 1. , 0.667],
1765
+ [ 0.667, 0.667, 0.667],
1766
+ [ 0.333, 0.5 , 0.667]])
1767
+ >>> librosa.util.softmask(X_ref, X, power=1)
1768
+ array([[ 0. , 0. , 0.333],
1769
+ [ 0.333, 0.333, 0.333],
1770
+ [ 0.667, 0.5 , 0.333]])
1771
+ >>> librosa.util.softmask(X, X_ref, power=2)
1772
+ array([[ 1. , 1. , 0.8],
1773
+ [ 0.8, 0.8, 0.8],
1774
+ [ 0.2, 0.5, 0.8]])
1775
+ >>> librosa.util.softmask(X, X_ref, power=4)
1776
+ array([[ 1. , 1. , 0.941],
1777
+ [ 0.941, 0.941, 0.941],
1778
+ [ 0.059, 0.5 , 0.941]])
1779
+ >>> librosa.util.softmask(X, X_ref, power=100)
1780
+ array([[ 1.000e+00, 1.000e+00, 1.000e+00],
1781
+ [ 1.000e+00, 1.000e+00, 1.000e+00],
1782
+ [ 7.889e-31, 5.000e-01, 1.000e+00]])
1783
+ >>> librosa.util.softmask(X, X_ref, power=np.inf)
1784
+ array([[ True, True, True],
1785
+ [ True, True, True],
1786
+ [False, False, True]], dtype=bool)
1787
+ """
1788
+ if X.shape != X_ref.shape:
1789
+ raise ParameterError(f"Shape mismatch: {X.shape}!={X_ref.shape}")
1790
+
1791
+ if np.any(X < 0) or np.any(X_ref < 0):
1792
+ raise ParameterError("X and X_ref must be non-negative")
1793
+
1794
+ if power <= 0:
1795
+ raise ParameterError("power must be strictly positive")
1796
+
1797
+ # We're working with ints, cast to float.
1798
+ dtype = X.dtype
1799
+ if not np.issubdtype(dtype, np.floating):
1800
+ dtype = np.float32
1801
+
1802
+ # Re-scale the input arrays relative to the larger value
1803
+ Z = np.maximum(X, X_ref).astype(dtype)
1804
+ bad_idx = Z < np.finfo(dtype).tiny
1805
+ Z[bad_idx] = 1
1806
+
1807
+ # For finite power, compute the softmask
1808
+ mask: np.ndarray
1809
+
1810
+ if np.isfinite(power):
1811
+ mask = (X / Z) ** power
1812
+ ref_mask = (X_ref / Z) ** power
1813
+ good_idx = ~bad_idx
1814
+ mask[good_idx] /= mask[good_idx] + ref_mask[good_idx]
1815
+ # Wherever energy is below energy in both inputs, split the mask
1816
+ if split_zeros:
1817
+ mask[bad_idx] = 0.5
1818
+ else:
1819
+ mask[bad_idx] = 0.0
1820
+ else:
1821
+ # Otherwise, compute the hard mask
1822
+ mask = X > X_ref
1823
+
1824
+ return mask
1825
+
1826
+
1827
+ def tiny(x: Union[float, np.ndarray]) -> _FloatLike_co:
1828
+ """Compute the tiny-value corresponding to an input's data type.
1829
+
1830
+ This is the smallest "usable" number representable in ``x.dtype``
1831
+ (e.g., float32).
1832
+
1833
+ This is primarily useful for determining a threshold for
1834
+ numerical underflow in division or multiplication operations.
1835
+
1836
+ Parameters
1837
+ ----------
1838
+ x : number or np.ndarray
1839
+ The array to compute the tiny-value for.
1840
+ All that matters here is ``x.dtype``
1841
+
1842
+ Returns
1843
+ -------
1844
+ tiny_value : float
1845
+ The smallest positive usable number for the type of ``x``.
1846
+ If ``x`` is integer-typed, then the tiny value for ``np.float32``
1847
+ is returned instead.
1848
+
1849
+ See Also
1850
+ --------
1851
+ numpy.finfo
1852
+
1853
+ Examples
1854
+ --------
1855
+ For a standard double-precision floating point number:
1856
+
1857
+ >>> librosa.util.tiny(1.0)
1858
+ 2.2250738585072014e-308
1859
+
1860
+ Or explicitly as double-precision
1861
+
1862
+ >>> librosa.util.tiny(np.asarray(1e-5, dtype=np.float64))
1863
+ 2.2250738585072014e-308
1864
+
1865
+ Or complex numbers
1866
+
1867
+ >>> librosa.util.tiny(1j)
1868
+ 2.2250738585072014e-308
1869
+
1870
+ Single-precision floating point:
1871
+
1872
+ >>> librosa.util.tiny(np.asarray(1e-5, dtype=np.float32))
1873
+ 1.1754944e-38
1874
+
1875
+ Integer
1876
+
1877
+ >>> librosa.util.tiny(5)
1878
+ 1.1754944e-38
1879
+ """
1880
+
1881
+ # Make sure we have an array view
1882
+ x = np.asarray(x)
1883
+
1884
+ # Only floating types generate a tiny
1885
+ if np.issubdtype(x.dtype, np.floating) or np.issubdtype(
1886
+ x.dtype, np.complexfloating
1887
+ ):
1888
+ dtype = x.dtype
1889
+ else:
1890
+ dtype = np.dtype(np.float32)
1891
+
1892
+ return np.finfo(dtype).tiny
1893
+
1894
+
1895
+ def fill_off_diagonal(x: np.ndarray, *, radius: float, value: float = 0) -> None:
1896
+ """Sets all cells of a matrix to a given ``value``
1897
+ if they lie outside a constraint region.
1898
+
1899
+ In this case, the constraint region is the
1900
+ Sakoe-Chiba band which runs with a fixed ``radius``
1901
+ along the main diagonal.
1902
+
1903
+ When ``x.shape[0] != x.shape[1]``, the radius will be
1904
+ expanded so that ``x[-1, -1] = 1`` always.
1905
+
1906
+ ``x`` will be modified in place.
1907
+
1908
+ Parameters
1909
+ ----------
1910
+ x : np.ndarray [shape=(N, M)]
1911
+ Input matrix, will be modified in place.
1912
+ radius : float
1913
+ The band radius (1/2 of the width) will be
1914
+ ``int(radius*min(x.shape))``
1915
+ value : float
1916
+ ``x[n, m] = value`` when ``(n, m)`` lies outside the band.
1917
+
1918
+ Examples
1919
+ --------
1920
+ >>> x = np.ones((8, 8))
1921
+ >>> librosa.util.fill_off_diagonal(x, radius=0.25)
1922
+ >>> x
1923
+ array([[1, 1, 0, 0, 0, 0, 0, 0],
1924
+ [1, 1, 1, 0, 0, 0, 0, 0],
1925
+ [0, 1, 1, 1, 0, 0, 0, 0],
1926
+ [0, 0, 1, 1, 1, 0, 0, 0],
1927
+ [0, 0, 0, 1, 1, 1, 0, 0],
1928
+ [0, 0, 0, 0, 1, 1, 1, 0],
1929
+ [0, 0, 0, 0, 0, 1, 1, 1],
1930
+ [0, 0, 0, 0, 0, 0, 1, 1]])
1931
+ >>> x = np.ones((8, 12))
1932
+ >>> librosa.util.fill_off_diagonal(x, radius=0.25)
1933
+ >>> x
1934
+ array([[1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
1935
+ [1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
1936
+ [0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
1937
+ [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
1938
+ [0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
1939
+ [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0],
1940
+ [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1],
1941
+ [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1]])
1942
+ """
1943
+ nx, ny = x.shape
1944
+
1945
+ # Calculate the radius in indices, rather than proportion
1946
+ radius = int(np.round(radius * np.min(x.shape)))
1947
+
1948
+ nx, ny = x.shape
1949
+ offset = np.abs((x.shape[0] - x.shape[1]))
1950
+
1951
+ if nx < ny:
1952
+ idx_u = np.triu_indices_from(x, k=radius + offset)
1953
+ idx_l = np.tril_indices_from(x, k=-radius)
1954
+ else:
1955
+ idx_u = np.triu_indices_from(x, k=radius)
1956
+ idx_l = np.tril_indices_from(x, k=-radius - offset)
1957
+
1958
+ # modify input matrix
1959
+ x[idx_u] = value
1960
+ x[idx_l] = value
1961
+
1962
+
1963
+ def cyclic_gradient(
1964
+ data: np.ndarray, *, edge_order: Literal[1, 2] = 1, axis: int = -1
1965
+ ) -> np.ndarray:
1966
+ """Estimate the gradient of a function over a uniformly sampled,
1967
+ periodic domain.
1968
+
1969
+ This is essentially the same as `np.gradient`, except that edge effects
1970
+ are handled by wrapping the observations (i.e. assuming periodicity)
1971
+ rather than extrapolation.
1972
+
1973
+ Parameters
1974
+ ----------
1975
+ data : np.ndarray
1976
+ The function values observed at uniformly spaced positions on
1977
+ a periodic domain
1978
+ edge_order : {1, 2}
1979
+ The order of the difference approximation used for estimating
1980
+ the gradient
1981
+ axis : int
1982
+ The axis along which gradients are calculated.
1983
+
1984
+ Returns
1985
+ -------
1986
+ grad : np.ndarray like ``data``
1987
+ The gradient of ``data`` taken along the specified axis.
1988
+
1989
+ See Also
1990
+ --------
1991
+ numpy.gradient
1992
+
1993
+ Examples
1994
+ --------
1995
+ This example estimates the gradient of cosine (-sine) from 64
1996
+ samples using direct (aperiodic) and periodic gradient
1997
+ calculation.
1998
+
1999
+ >>> import matplotlib.pyplot as plt
2000
+ >>> x = 2 * np.pi * np.linspace(0, 1, num=64, endpoint=False)
2001
+ >>> y = np.cos(x)
2002
+ >>> grad = np.gradient(y)
2003
+ >>> cyclic_grad = librosa.util.cyclic_gradient(y)
2004
+ >>> true_grad = -np.sin(x) * 2 * np.pi / len(x)
2005
+ >>> fig, ax = plt.subplots()
2006
+ >>> ax.plot(x, true_grad, label='True gradient', linewidth=5,
2007
+ ... alpha=0.35)
2008
+ >>> ax.plot(x, cyclic_grad, label='cyclic_gradient')
2009
+ >>> ax.plot(x, grad, label='np.gradient', linestyle=':')
2010
+ >>> ax.legend()
2011
+ >>> # Zoom into the first part of the sequence
2012
+ >>> ax.set(xlim=[0, np.pi/16], ylim=[-0.025, 0.025])
2013
+ """
2014
+ # Wrap-pad the data along the target axis by `edge_order` on each side
2015
+ padding = [(0, 0)] * data.ndim
2016
+ padding[axis] = (edge_order, edge_order)
2017
+ data_pad = np.pad(data, padding, mode="wrap")
2018
+
2019
+ # Compute the gradient
2020
+ grad = np.gradient(data_pad, edge_order=edge_order, axis=axis)
2021
+
2022
+ # Remove the padding
2023
+ slices = [slice(None)] * data.ndim
2024
+ slices[axis] = slice(edge_order, -edge_order)
2025
+ grad_slice: np.ndarray = grad[tuple(slices)]
2026
+ return grad_slice
2027
+
2028
+
2029
+ @numba.jit(nopython=True, cache=False) # type: ignore
2030
+ def __shear_dense(X: np.ndarray, *, factor: int = +1, axis: int = -1) -> np.ndarray:
2031
+ """Numba-accelerated shear for dense (ndarray) arrays"""
2032
+
2033
+ if axis == 0:
2034
+ X = X.T
2035
+
2036
+ X_shear = np.empty_like(X)
2037
+
2038
+ for i in range(X.shape[1]):
2039
+ X_shear[:, i] = np.roll(X[:, i], factor * i)
2040
+
2041
+ if axis == 0:
2042
+ X_shear = X_shear.T
2043
+
2044
+ return X_shear
2045
+
2046
+
2047
+ def __shear_sparse(
2048
+ X: scipy.sparse.spmatrix, *, factor: int = +1, axis: int = -1
2049
+ ) -> scipy.sparse.spmatrix:
2050
+ """Fast shearing for sparse matrices
2051
+
2052
+ Shearing is performed using CSC array indices,
2053
+ and the result is converted back to whatever sparse format
2054
+ the data was originally provided in.
2055
+ """
2056
+ fmt = X.format
2057
+ if axis == 0:
2058
+ X = X.T
2059
+
2060
+ # Now we're definitely rolling on the correct axis
2061
+ X_shear = X.tocsc(copy=True)
2062
+
2063
+ # The idea here is to repeat the shear amount (factor * range)
2064
+ # by the number of non-zeros for each column.
2065
+ # The number of non-zeros is computed by diffing the index pointer array
2066
+ roll = np.repeat(factor * np.arange(X_shear.shape[1]), np.diff(X_shear.indptr))
2067
+
2068
+ # In-place roll
2069
+ np.mod(X_shear.indices + roll, X_shear.shape[0], out=X_shear.indices)
2070
+
2071
+ if axis == 0:
2072
+ X_shear = X_shear.T
2073
+
2074
+ # And convert back to the input format
2075
+ return X_shear.asformat(fmt)
2076
+
2077
+
2078
+ _ArrayOrSparseMatrix = TypeVar(
2079
+ "_ArrayOrSparseMatrix", bound=Union[np.ndarray, scipy.sparse.spmatrix]
2080
+ )
2081
+
2082
+
2083
+ @overload
2084
+ def shear(X: np.ndarray, *, factor: int = ..., axis: int = ...) -> np.ndarray:
2085
+ ...
2086
+
2087
+
2088
+ @overload
2089
+ def shear(
2090
+ X: scipy.sparse.spmatrix, *, factor: int = ..., axis: int = ...
2091
+ ) -> scipy.sparse.spmatrix:
2092
+ ...
2093
+
2094
+
2095
+ def shear(
2096
+ X: _ArrayOrSparseMatrix, *, factor: int = 1, axis: int = -1
2097
+ ) -> _ArrayOrSparseMatrix:
2098
+ """Shear a matrix by a given factor.
2099
+
2100
+ The column ``X[:, n]`` will be displaced (rolled)
2101
+ by ``factor * n``
2102
+
2103
+ This is primarily useful for converting between lag and recurrence
2104
+ representations: shearing with ``factor=-1`` converts the main diagonal
2105
+ to a horizontal. Shearing with ``factor=1`` converts a horizontal to
2106
+ a diagonal.
2107
+
2108
+ Parameters
2109
+ ----------
2110
+ X : np.ndarray [ndim=2] or scipy.sparse matrix
2111
+ The array to be sheared
2112
+ factor : integer
2113
+ The shear factor: ``X[:, n] -> np.roll(X[:, n], factor * n)``
2114
+ axis : integer
2115
+ The axis along which to shear
2116
+
2117
+ Returns
2118
+ -------
2119
+ X_shear : same type as ``X``
2120
+ The sheared matrix
2121
+
2122
+ Examples
2123
+ --------
2124
+ >>> E = np.eye(3)
2125
+ >>> librosa.util.shear(E, factor=-1, axis=-1)
2126
+ array([[1., 1., 1.],
2127
+ [0., 0., 0.],
2128
+ [0., 0., 0.]])
2129
+ >>> librosa.util.shear(E, factor=-1, axis=0)
2130
+ array([[1., 0., 0.],
2131
+ [1., 0., 0.],
2132
+ [1., 0., 0.]])
2133
+ >>> librosa.util.shear(E, factor=1, axis=-1)
2134
+ array([[1., 0., 0.],
2135
+ [0., 0., 1.],
2136
+ [0., 1., 0.]])
2137
+ """
2138
+
2139
+ if not np.issubdtype(type(factor), np.integer):
2140
+ raise ParameterError(f"factor={factor} must be integer-valued")
2141
+
2142
+ # Suppress type checks because mypy doesn't like numba jitting
2143
+ # or scipy sparse conversion
2144
+ if scipy.sparse.isspmatrix(X):
2145
+ return __shear_sparse(X, factor=factor, axis=axis) # type: ignore
2146
+ else:
2147
+ return __shear_dense(X, factor=factor, axis=axis) # type: ignore
2148
+
2149
+
2150
+ def stack(arrays: List[np.ndarray], *, axis: int = 0) -> np.ndarray:
2151
+ """Stack one or more arrays along a target axis.
2152
+
2153
+ This function is similar to `np.stack`, except that memory contiguity is
2154
+ retained when stacking along the first dimension.
2155
+
2156
+ This is useful when combining multiple monophonic audio signals into a
2157
+ multi-channel signal, or when stacking multiple feature representations
2158
+ to form a multi-dimensional array.
2159
+
2160
+ Parameters
2161
+ ----------
2162
+ arrays : list
2163
+ one or more `np.ndarray`
2164
+ axis : integer
2165
+ The target axis along which to stack. ``axis=0`` creates a new first axis,
2166
+ and ``axis=-1`` creates a new last axis.
2167
+
2168
+ Returns
2169
+ -------
2170
+ arr_stack : np.ndarray [shape=(len(arrays), array_shape) or shape=(array_shape, len(arrays))]
2171
+ The input arrays, stacked along the target dimension.
2172
+
2173
+ If ``axis=0``, then ``arr_stack`` will be F-contiguous.
2174
+ Otherwise, ``arr_stack`` will be C-contiguous by default, as computed by
2175
+ `np.stack`.
2176
+
2177
+ Raises
2178
+ ------
2179
+ ParameterError
2180
+ - If ``arrays`` do not all have the same shape
2181
+ - If no ``arrays`` are given
2182
+
2183
+ See Also
2184
+ --------
2185
+ numpy.stack
2186
+ numpy.ndarray.flags
2187
+ frame
2188
+
2189
+ Examples
2190
+ --------
2191
+ Combine two buffers into a contiguous arrays
2192
+
2193
+ >>> y_left = np.ones(5)
2194
+ >>> y_right = -np.ones(5)
2195
+ >>> y_stereo = librosa.util.stack([y_left, y_right], axis=0)
2196
+ >>> y_stereo
2197
+ array([[ 1., 1., 1., 1., 1.],
2198
+ [-1., -1., -1., -1., -1.]])
2199
+ >>> y_stereo.flags
2200
+ C_CONTIGUOUS : False
2201
+ F_CONTIGUOUS : True
2202
+ OWNDATA : True
2203
+ WRITEABLE : True
2204
+ ALIGNED : True
2205
+ WRITEBACKIFCOPY : False
2206
+ UPDATEIFCOPY : False
2207
+
2208
+ Or along the trailing axis
2209
+
2210
+ >>> y_stereo = librosa.util.stack([y_left, y_right], axis=-1)
2211
+ >>> y_stereo
2212
+ array([[ 1., -1.],
2213
+ [ 1., -1.],
2214
+ [ 1., -1.],
2215
+ [ 1., -1.],
2216
+ [ 1., -1.]])
2217
+ >>> y_stereo.flags
2218
+ C_CONTIGUOUS : True
2219
+ F_CONTIGUOUS : False
2220
+ OWNDATA : True
2221
+ WRITEABLE : True
2222
+ ALIGNED : True
2223
+ WRITEBACKIFCOPY : False
2224
+ UPDATEIFCOPY : False
2225
+ """
2226
+
2227
+ shapes = {arr.shape for arr in arrays}
2228
+ if len(shapes) > 1:
2229
+ raise ParameterError("all input arrays must have the same shape")
2230
+ elif len(shapes) < 1:
2231
+ raise ParameterError("at least one input array must be provided for stack")
2232
+
2233
+ shape_in = shapes.pop()
2234
+
2235
+ if axis != 0:
2236
+ return np.stack(arrays, axis=axis)
2237
+ else:
2238
+ # If axis is 0, enforce F-ordering
2239
+ shape = tuple([len(arrays)] + list(shape_in))
2240
+
2241
+ # Find the common dtype for all inputs
2242
+ dtype = np.find_common_type([arr.dtype for arr in arrays], [])
2243
+
2244
+ # Allocate an empty array of the right shape and type
2245
+ result = np.empty(shape, dtype=dtype, order="F")
2246
+
2247
+ # Stack into the preallocated buffer
2248
+ np.stack(arrays, axis=axis, out=result)
2249
+
2250
+ return result
2251
+
2252
+
2253
+ def dtype_r2c(d: DTypeLike, *, default: Optional[type] = np.complex64) -> DTypeLike:
2254
+ """Find the complex numpy dtype corresponding to a real dtype.
2255
+
2256
+ This is used to maintain numerical precision and memory footprint
2257
+ when constructing complex arrays from real-valued data
2258
+ (e.g. in a Fourier transform).
2259
+
2260
+ A `float32` (single-precision) type maps to `complex64`,
2261
+ while a `float64` (double-precision) maps to `complex128`.
2262
+
2263
+ Parameters
2264
+ ----------
2265
+ d : np.dtype
2266
+ The real-valued dtype to convert to complex.
2267
+ If ``d`` is a complex type already, it will be returned.
2268
+ default : np.dtype, optional
2269
+ The default complex target type, if ``d`` does not match a
2270
+ known dtype
2271
+
2272
+ Returns
2273
+ -------
2274
+ d_c : np.dtype
2275
+ The complex dtype
2276
+
2277
+ See Also
2278
+ --------
2279
+ dtype_c2r
2280
+ numpy.dtype
2281
+
2282
+ Examples
2283
+ --------
2284
+ >>> librosa.util.dtype_r2c(np.float32)
2285
+ dtype('complex64')
2286
+
2287
+ >>> librosa.util.dtype_r2c(np.int16)
2288
+ dtype('complex64')
2289
+
2290
+ >>> librosa.util.dtype_r2c(np.complex128)
2291
+ dtype('complex128')
2292
+ """
2293
+ mapping: Dict[DTypeLike, type] = {
2294
+ np.dtype(np.float32): np.complex64,
2295
+ np.dtype(np.float64): np.complex128,
2296
+ np.dtype(float): np.dtype(complex).type,
2297
+ }
2298
+
2299
+ # If we're given a complex type already, return it
2300
+ dt = np.dtype(d)
2301
+ if dt.kind == "c":
2302
+ return dt
2303
+
2304
+ # Otherwise, try to map the dtype.
2305
+ # If no match is found, return the default.
2306
+ return np.dtype(mapping.get(dt, default))
2307
+
2308
+
2309
+ def dtype_c2r(d: DTypeLike, *, default: Optional[type] = np.float32) -> DTypeLike:
2310
+ """Find the real numpy dtype corresponding to a complex dtype.
2311
+
2312
+ This is used to maintain numerical precision and memory footprint
2313
+ when constructing real arrays from complex-valued data
2314
+ (e.g. in an inverse Fourier transform).
2315
+
2316
+ A `complex64` (single-precision) type maps to `float32`,
2317
+ while a `complex128` (double-precision) maps to `float64`.
2318
+
2319
+ Parameters
2320
+ ----------
2321
+ d : np.dtype
2322
+ The complex-valued dtype to convert to real.
2323
+ If ``d`` is a real (float) type already, it will be returned.
2324
+ default : np.dtype, optional
2325
+ The default real target type, if ``d`` does not match a
2326
+ known dtype
2327
+
2328
+ Returns
2329
+ -------
2330
+ d_r : np.dtype
2331
+ The real dtype
2332
+
2333
+ See Also
2334
+ --------
2335
+ dtype_r2c
2336
+ numpy.dtype
2337
+
2338
+ Examples
2339
+ --------
2340
+ >>> librosa.util.dtype_r2c(np.complex64)
2341
+ dtype('float32')
2342
+
2343
+ >>> librosa.util.dtype_r2c(np.float32)
2344
+ dtype('float32')
2345
+
2346
+ >>> librosa.util.dtype_r2c(np.int16)
2347
+ dtype('float32')
2348
+
2349
+ >>> librosa.util.dtype_r2c(np.complex128)
2350
+ dtype('float64')
2351
+ """
2352
+ mapping: Dict[DTypeLike, type] = {
2353
+ np.dtype(np.complex64): np.float32,
2354
+ np.dtype(np.complex128): np.float64,
2355
+ np.dtype(complex): np.dtype(float).type,
2356
+ }
2357
+
2358
+ # If we're given a real type already, return it
2359
+ dt = np.dtype(d)
2360
+ if dt.kind == "f":
2361
+ return dt
2362
+
2363
+ # Otherwise, try to map the dtype.
2364
+ # If no match is found, return the default.
2365
+ return np.dtype(mapping.get(dt, default))
2366
+
2367
+
2368
+ @numba.jit(nopython=True, cache=False)
2369
+ def __count_unique(x):
2370
+ """Counts the number of unique values in an array.
2371
+
2372
+ This function is a helper for `count_unique` and is not
2373
+ to be called directly.
2374
+ """
2375
+ uniques = np.unique(x)
2376
+ return uniques.shape[0]
2377
+
2378
+
2379
+ def count_unique(data: np.ndarray, *, axis: int = -1) -> np.ndarray:
2380
+ """Count the number of unique values in a multi-dimensional array
2381
+ along a given axis.
2382
+
2383
+ Parameters
2384
+ ----------
2385
+ data : np.ndarray
2386
+ The input array
2387
+ axis : int
2388
+ The target axis to count
2389
+
2390
+ Returns
2391
+ -------
2392
+ n_uniques
2393
+ The number of unique values.
2394
+ This array will have one fewer dimension than the input.
2395
+
2396
+ See Also
2397
+ --------
2398
+ is_unique
2399
+
2400
+ Examples
2401
+ --------
2402
+ >>> x = np.vander(np.arange(5))
2403
+ >>> x
2404
+ array([[ 0, 0, 0, 0, 1],
2405
+ [ 1, 1, 1, 1, 1],
2406
+ [ 16, 8, 4, 2, 1],
2407
+ [ 81, 27, 9, 3, 1],
2408
+ [256, 64, 16, 4, 1]])
2409
+ >>> # Count unique values along rows (within columns)
2410
+ >>> librosa.util.count_unique(x, axis=0)
2411
+ array([5, 5, 5, 5, 1])
2412
+ >>> # Count unique values along columns (within rows)
2413
+ >>> librosa.util.count_unique(x, axis=-1)
2414
+ array([2, 1, 5, 5, 5])
2415
+ """
2416
+ return np.apply_along_axis(__count_unique, axis, data)
2417
+
2418
+
2419
+ @numba.jit(nopython=True, cache=False)
2420
+ def __is_unique(x):
2421
+ """Determines if the input array has all unique values.
2422
+
2423
+ This function is a helper for `is_unique` and is not
2424
+ to be called directly.
2425
+ """
2426
+
2427
+ uniques = np.unique(x)
2428
+ return uniques.shape[0] == x.size
2429
+
2430
+
2431
+ def is_unique(data: np.ndarray, *, axis: int = -1) -> np.ndarray:
2432
+ """Determine if the input array consists of all unique values
2433
+ along a given axis.
2434
+
2435
+ Parameters
2436
+ ----------
2437
+ data : np.ndarray
2438
+ The input array
2439
+ axis : int
2440
+ The target axis
2441
+
2442
+ Returns
2443
+ -------
2444
+ is_unique
2445
+ Array of booleans indicating whether the data is unique along the chosen
2446
+ axis.
2447
+ This array will have one fewer dimension than the input.
2448
+
2449
+ See Also
2450
+ --------
2451
+ count_unique
2452
+
2453
+ Examples
2454
+ --------
2455
+ >>> x = np.vander(np.arange(5))
2456
+ >>> x
2457
+ array([[ 0, 0, 0, 0, 1],
2458
+ [ 1, 1, 1, 1, 1],
2459
+ [ 16, 8, 4, 2, 1],
2460
+ [ 81, 27, 9, 3, 1],
2461
+ [256, 64, 16, 4, 1]])
2462
+ >>> # Check uniqueness along rows
2463
+ >>> librosa.util.is_unique(x, axis=0)
2464
+ array([ True, True, True, True, False])
2465
+ >>> # Check uniqueness along columns
2466
+ >>> librosa.util.is_unique(x, axis=-1)
2467
+ array([False, False, True, True, True])
2468
+
2469
+ """
2470
+
2471
+ return np.apply_along_axis(__is_unique, axis, data)
2472
+
2473
+
2474
+ @numba.vectorize(
2475
+ ["float32(complex64)", "float64(complex128)"], nopython=True, cache=True, identity=0
2476
+ ) # type: ignore
2477
+ def _cabs2(x: _ComplexLike_co) -> _FloatLike_co: # pragma: no cover
2478
+ """Helper function for efficiently computing abs2 on complex inputs"""
2479
+ return x.real**2 + x.imag**2
2480
+
2481
+
2482
+ _Number = Union[complex, "np.number[Any]"]
2483
+ _NumberOrArray = TypeVar("_NumberOrArray", bound=Union[_Number, np.ndarray])
2484
+
2485
+
2486
+ def abs2(x: _NumberOrArray, dtype: Optional[DTypeLike] = None) -> _NumberOrArray:
2487
+ """Compute the squared magnitude of a real or complex array.
2488
+
2489
+ This function is equivalent to calling `np.abs(x)**2` but it
2490
+ is slightly more efficient.
2491
+
2492
+ Parameters
2493
+ ----------
2494
+ x : np.ndarray or scalar, real or complex typed
2495
+ The input data, either real (float32, float64) or complex (complex64, complex128) typed
2496
+ dtype : np.dtype, optional
2497
+ The data type of the output array.
2498
+ If not provided, it will be inferred from `x`
2499
+
2500
+ Returns
2501
+ -------
2502
+ p : np.ndarray or scale, real
2503
+ squared magnitude of `x`
2504
+
2505
+ Examples
2506
+ --------
2507
+ >>> librosa.util.abs2(3 + 4j)
2508
+ 25.0
2509
+
2510
+ >>> librosa.util.abs2((0.5j)**np.arange(8))
2511
+ array([1.000e+00, 2.500e-01, 6.250e-02, 1.562e-02, 3.906e-03, 9.766e-04,
2512
+ 2.441e-04, 6.104e-05])
2513
+ """
2514
+ if np.iscomplexobj(x):
2515
+ # suppress type check, mypy doesn't like vectorization
2516
+ y = _cabs2(x)
2517
+ if dtype is None:
2518
+ return y # type: ignore
2519
+ else:
2520
+ return y.astype(dtype) # type: ignore
2521
+ else:
2522
+ # suppress type check, mypy doesn't know this is real
2523
+ return np.power(x, 2, dtype=dtype) # type: ignore
2524
+
2525
+
2526
+ @numba.vectorize(
2527
+ ["complex64(float32)", "complex128(float64)"], nopython=True, cache=False, identity=1
2528
+ ) # type: ignore
2529
+ def _phasor_angles(x) -> np.complex_: # pragma: no cover
2530
+ return np.cos(x) + 1j * np.sin(x) # type: ignore
2531
+
2532
+
2533
+ _Real = Union[float, "np.integer[Any]", "np.floating[Any]"]
2534
+
2535
+
2536
+ @overload
2537
+ def phasor(angles: np.ndarray, *, mag: Optional[np.ndarray] = ...) -> np.ndarray:
2538
+ ...
2539
+
2540
+
2541
+ @overload
2542
+ def phasor(angles: _Real, *, mag: Optional[_Number] = ...) -> np.complex_:
2543
+ ...
2544
+
2545
+
2546
+ def phasor(
2547
+ angles: Union[np.ndarray, _Real],
2548
+ *,
2549
+ mag: Optional[Union[np.ndarray, _Number]] = None,
2550
+ ) -> Union[np.ndarray, np.complex_]:
2551
+ """Construct a complex phasor representation from angles.
2552
+
2553
+ When `mag` is not provided, this is equivalent to:
2554
+
2555
+ z = np.cos(angles) + 1j * np.sin(angles)
2556
+
2557
+ or by Euler's formula:
2558
+
2559
+ z = np.exp(1j * angles)
2560
+
2561
+ When `mag` is provided, this is equivalent to:
2562
+
2563
+ z = mag * np.exp(1j * angles)
2564
+
2565
+ This function should be more efficient (in time and memory) than the equivalent'
2566
+ formulations above, but produce numerically identical results.
2567
+
2568
+ Parameters
2569
+ ----------
2570
+ angles : np.ndarray or scalar, real-valued
2571
+ Angle(s), measured in radians
2572
+
2573
+ mag : np.ndarray or scalar, optional
2574
+ If provided, phasor(s) will be scaled by `mag`.
2575
+
2576
+ If not provided (default), phasors will have unit magnitude.
2577
+
2578
+ `mag` must be of compatible shape to multiply with `angles`.
2579
+
2580
+ Returns
2581
+ -------
2582
+ z : np.ndarray or scalar, complex-valued
2583
+ Complex number(s) z corresponding to the given angle(s)
2584
+ and optional magnitude(s).
2585
+
2586
+ Examples
2587
+ --------
2588
+ Construct unit phasors at angles 0, pi/2, and pi:
2589
+
2590
+ >>> librosa.util.phasor([0, np.pi/2, np.pi])
2591
+ array([ 1.000e+00+0.000e+00j, 6.123e-17+1.000e+00j,
2592
+ -1.000e+00+1.225e-16j])
2593
+
2594
+ Construct a phasor with magnitude 1/2:
2595
+
2596
+ >>> librosa.util.phasor(np.pi/2, mag=0.5)
2597
+ (3.061616997868383e-17+0.5j)
2598
+
2599
+ Or arrays of angles and magnitudes:
2600
+
2601
+ >>> librosa.util.phasor(np.array([0, np.pi/2]), mag=np.array([0.5, 1.5]))
2602
+ array([5.000e-01+0.j , 9.185e-17+1.5j])
2603
+ """
2604
+ z = _phasor_angles(angles)
2605
+
2606
+ if mag is not None:
2607
+ z *= mag
2608
+
2609
+ return z # type: ignore