tuney 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tuney/__init__.py +0 -0
- tuney/__main__.py +10 -0
- tuney/audio/__init__.py +8 -0
- tuney/audio/device_config.py +40 -0
- tuney/audio/file_player.py +37 -0
- tuney/audio/midi.py +20 -0
- tuney/audio/oscillator.py +49 -0
- tuney/audio/player.py +58 -0
- tuney/audio/runnable.py +22 -0
- tuney/audio/sample_data.py +36 -0
- tuney/audio/scipy.py +701 -0
- tuney/audio/synth_player.py +151 -0
- tuney/keyboard/__init__.py +13 -0
- tuney/keyboard/key_types.py +17 -0
- tuney/keyboard/listener.py +93 -0
- tuney/keyboard/queue.py +84 -0
- tuney/mapper/__init__.py +0 -0
- tuney/mapper/linear_mapper.py +29 -0
- tuney/scale/__init__.py +0 -0
- tuney/scale/nearest_note.py +23 -0
- tuney/scale/scale.py +32 -0
- tuney/scale/twelve_tet.py +50 -0
- tuney/time/__init__.py +4 -0
- tuney/time/event.py +61 -0
- tuney/time/text_timings.py +114 -0
- tuney/ui/__init__.py +0 -0
- tuney/ui/controller.py +45 -0
- tuney/ui/keyboard_controller.py +25 -0
- tuney/ui/note_grid.py +74 -0
- tuney/ui/note_grid.tcss +18 -0
- tuney/ui/text_controller.py +48 -0
- tuney-0.2.0.dist-info/METADATA +23 -0
- tuney-0.2.0.dist-info/RECORD +36 -0
- tuney-0.2.0.dist-info/WHEEL +4 -0
- tuney-0.2.0.dist-info/entry_points.txt +2 -0
- tuney-0.2.0.dist-info/licenses/LICENSE +21 -0
tuney/audio/scipy.py
ADDED
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
# From scipy
|
|
2
|
+
# Author: Travis Oliphant
|
|
3
|
+
# 2003
|
|
4
|
+
#
|
|
5
|
+
# Feb. 2010: Updated by Warren Weckesser:
|
|
6
|
+
# Rewrote much of chirp()
|
|
7
|
+
# Added sweep_poly()
|
|
8
|
+
import numpy as np
|
|
9
|
+
from numpy import (
|
|
10
|
+
asarray,
|
|
11
|
+
cos,
|
|
12
|
+
exp,
|
|
13
|
+
extract,
|
|
14
|
+
log,
|
|
15
|
+
mod,
|
|
16
|
+
nan,
|
|
17
|
+
pi,
|
|
18
|
+
place,
|
|
19
|
+
polyint,
|
|
20
|
+
polyval,
|
|
21
|
+
sin,
|
|
22
|
+
sqrt,
|
|
23
|
+
zeros,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = ['sawtooth', 'square', 'gausspulse', 'chirp', 'sweep_poly', 'unit_impulse']
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def sawtooth(t, width=1):
|
|
30
|
+
"""
|
|
31
|
+
Return a periodic sawtooth or triangle waveform.
|
|
32
|
+
|
|
33
|
+
The sawtooth waveform has a period ``2*pi``, rises from -1 to 1 on the
|
|
34
|
+
interval 0 to ``width*2*pi``, then drops from 1 to -1 on the interval
|
|
35
|
+
``width*2*pi`` to ``2*pi``. `width` must be in the interval [0, 1].
|
|
36
|
+
|
|
37
|
+
Note that this is not band-limited. It produces an infinite number
|
|
38
|
+
of harmonics, which are aliased back and forth across the frequency
|
|
39
|
+
spectrum.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
t : array_like
|
|
44
|
+
Time.
|
|
45
|
+
width : array_like, optional
|
|
46
|
+
Width of the rising ramp as a proportion of the total cycle.
|
|
47
|
+
Default is 1, producing a rising ramp, while 0 produces a falling
|
|
48
|
+
ramp. `width` = 0.5 produces a triangle wave.
|
|
49
|
+
If an array, causes wave shape to change over time, and must be the
|
|
50
|
+
same length as t.
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
y : ndarray
|
|
55
|
+
Output array containing the sawtooth waveform.
|
|
56
|
+
|
|
57
|
+
Examples
|
|
58
|
+
--------
|
|
59
|
+
A 5 Hz waveform sampled at 500 Hz for 1 second:
|
|
60
|
+
|
|
61
|
+
>>> import numpy as np
|
|
62
|
+
>>> from scipy import signal
|
|
63
|
+
>>> import matplotlib.pyplot as plt
|
|
64
|
+
>>> t = np.linspace(0, 1, 500)
|
|
65
|
+
>>> plt.plot(t, signal.sawtooth(2 * np.pi * 5 * t))
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
t, w = asarray(t), asarray(width)
|
|
69
|
+
w = asarray(w + (t - t))
|
|
70
|
+
t = asarray(t + (w - w))
|
|
71
|
+
y = zeros(t.shape, dtype='d')
|
|
72
|
+
|
|
73
|
+
# width must be between 0 and 1 inclusive
|
|
74
|
+
mask1 = (w > 1) | (w < 0)
|
|
75
|
+
place(y, mask1, nan)
|
|
76
|
+
|
|
77
|
+
# take t modulo 2*pi
|
|
78
|
+
tmod = mod(t, 2 * pi)
|
|
79
|
+
|
|
80
|
+
# on the interval 0 to width*2*pi function is
|
|
81
|
+
# tmod / (pi*w) - 1
|
|
82
|
+
mask2 = (1 - mask1) & (tmod < w * 2 * pi)
|
|
83
|
+
tsub = extract(mask2, tmod)
|
|
84
|
+
wsub = extract(mask2, w)
|
|
85
|
+
place(y, mask2, tsub / (pi * wsub) - 1)
|
|
86
|
+
|
|
87
|
+
# on the interval width*2*pi to 2*pi function is
|
|
88
|
+
# (pi*(w+1)-tmod) / (pi*(1-w))
|
|
89
|
+
|
|
90
|
+
mask3 = (1 - mask1) & (1 - mask2)
|
|
91
|
+
tsub = extract(mask3, tmod)
|
|
92
|
+
wsub = extract(mask3, w)
|
|
93
|
+
place(y, mask3, (pi * (wsub + 1) - tsub) / (pi * (1 - wsub)))
|
|
94
|
+
return y
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def square(t, duty=0.5):
|
|
98
|
+
"""
|
|
99
|
+
Return a periodic square-wave waveform.
|
|
100
|
+
|
|
101
|
+
The square wave has a period ``2*pi``, has value +1 from 0 to
|
|
102
|
+
``2*pi*duty`` and -1 from ``2*pi*duty`` to ``2*pi``. `duty` must be in
|
|
103
|
+
the interval [0,1].
|
|
104
|
+
|
|
105
|
+
Note that this is not band-limited. It produces an infinite number
|
|
106
|
+
of harmonics, which are aliased back and forth across the frequency
|
|
107
|
+
spectrum.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
t : array_like
|
|
112
|
+
The input time array.
|
|
113
|
+
duty : array_like, optional
|
|
114
|
+
Duty cycle. Default is 0.5 (50% duty cycle).
|
|
115
|
+
If an array, causes wave shape to change over time, and must be the
|
|
116
|
+
same length as t.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
y : ndarray
|
|
121
|
+
Output array containing the square waveform.
|
|
122
|
+
|
|
123
|
+
Examples
|
|
124
|
+
--------
|
|
125
|
+
A 5 Hz waveform sampled at 500 Hz for 1 second:
|
|
126
|
+
|
|
127
|
+
>>> import numpy as np
|
|
128
|
+
>>> from scipy import signal
|
|
129
|
+
>>> import matplotlib.pyplot as plt
|
|
130
|
+
>>> t = np.linspace(0, 1, 500, endpoint=False)
|
|
131
|
+
>>> plt.plot(t, signal.square(2 * np.pi * 5 * t))
|
|
132
|
+
>>> plt.ylim(-2, 2)
|
|
133
|
+
|
|
134
|
+
A pulse-width modulated sine wave:
|
|
135
|
+
|
|
136
|
+
>>> plt.figure()
|
|
137
|
+
>>> sig = np.sin(2 * np.pi * t)
|
|
138
|
+
>>> pwm = signal.square(2 * np.pi * 30 * t, duty=(sig + 1)/2)
|
|
139
|
+
>>> plt.subplot(2, 1, 1)
|
|
140
|
+
>>> plt.plot(t, sig)
|
|
141
|
+
>>> plt.subplot(2, 1, 2)
|
|
142
|
+
>>> plt.plot(t, pwm)
|
|
143
|
+
>>> plt.ylim(-1.5, 1.5)
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
t, w = asarray(t), asarray(duty)
|
|
147
|
+
w = asarray(w + (t - t))
|
|
148
|
+
t = asarray(t + (w - w))
|
|
149
|
+
y = zeros(t.shape, dtype='d')
|
|
150
|
+
|
|
151
|
+
# width must be between 0 and 1 inclusive
|
|
152
|
+
mask1 = (w > 1) | (w < 0)
|
|
153
|
+
place(y, mask1, nan)
|
|
154
|
+
|
|
155
|
+
# on the interval 0 to duty*2*pi function is 1
|
|
156
|
+
tmod = mod(t, 2 * pi)
|
|
157
|
+
mask2 = (1 - mask1) & (tmod < w * 2 * pi)
|
|
158
|
+
place(y, mask2, 1)
|
|
159
|
+
|
|
160
|
+
# on the interval duty*2*pi to 2*pi function is
|
|
161
|
+
# (pi*(w+1)-tmod) / (pi*(1-w))
|
|
162
|
+
mask3 = (1 - mask1) & (1 - mask2)
|
|
163
|
+
place(y, mask3, -1)
|
|
164
|
+
return y
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def gausspulse(t, fc=1000, bw=0.5, bwr=-6, tpr=-60, retquad=False, retenv=False):
|
|
168
|
+
"""
|
|
169
|
+
Return a Gaussian modulated sinusoid:
|
|
170
|
+
|
|
171
|
+
``exp(-a t^2) exp(1j*2*pi*fc*t).``
|
|
172
|
+
|
|
173
|
+
If `retquad` is True, then return the real and imaginary parts
|
|
174
|
+
(in-phase and quadrature).
|
|
175
|
+
If `retenv` is True, then return the envelope (unmodulated signal).
|
|
176
|
+
Otherwise, return the real part of the modulated sinusoid.
|
|
177
|
+
|
|
178
|
+
Parameters
|
|
179
|
+
----------
|
|
180
|
+
t : ndarray or the string 'cutoff'
|
|
181
|
+
Input array.
|
|
182
|
+
fc : float, optional
|
|
183
|
+
Center frequency (e.g. Hz). Default is 1000.
|
|
184
|
+
bw : float, optional
|
|
185
|
+
Fractional bandwidth in frequency domain of pulse (e.g. Hz).
|
|
186
|
+
Default is 0.5.
|
|
187
|
+
bwr : float, optional
|
|
188
|
+
Reference level at which fractional bandwidth is calculated (dB).
|
|
189
|
+
Default is -6.
|
|
190
|
+
tpr : float, optional
|
|
191
|
+
If `t` is 'cutoff', then the function returns the cutoff
|
|
192
|
+
time for when the pulse amplitude falls below `tpr` (in dB).
|
|
193
|
+
Default is -60.
|
|
194
|
+
retquad : bool, optional
|
|
195
|
+
If True, return the quadrature (imaginary) as well as the real part
|
|
196
|
+
of the signal. Default is False.
|
|
197
|
+
retenv : bool, optional
|
|
198
|
+
If True, return the envelope of the signal. Default is False.
|
|
199
|
+
|
|
200
|
+
Returns
|
|
201
|
+
-------
|
|
202
|
+
yI : ndarray
|
|
203
|
+
Real part of signal. Always returned.
|
|
204
|
+
yQ : ndarray
|
|
205
|
+
Imaginary part of signal. Only returned if `retquad` is True.
|
|
206
|
+
yenv : ndarray
|
|
207
|
+
Envelope of signal. Only returned if `retenv` is True.
|
|
208
|
+
|
|
209
|
+
Examples
|
|
210
|
+
--------
|
|
211
|
+
Plot real component, imaginary component, and envelope for a 5 Hz pulse,
|
|
212
|
+
sampled at 100 Hz for 2 seconds:
|
|
213
|
+
|
|
214
|
+
>>> import numpy as np
|
|
215
|
+
>>> from scipy import signal
|
|
216
|
+
>>> import matplotlib.pyplot as plt
|
|
217
|
+
>>> t = np.linspace(-1, 1, 2 * 100, endpoint=False)
|
|
218
|
+
>>> i, q, e = signal.gausspulse(t, fc=5, retquad=True, retenv=True)
|
|
219
|
+
>>> plt.plot(t, i, t, q, t, e, '--')
|
|
220
|
+
|
|
221
|
+
"""
|
|
222
|
+
if fc < 0:
|
|
223
|
+
raise ValueError(f'Center frequency (fc={fc:.2f}) must be >=0.')
|
|
224
|
+
if bw <= 0:
|
|
225
|
+
raise ValueError(f'Fractional bandwidth (bw={bw:.2f}) must be > 0.')
|
|
226
|
+
if bwr >= 0:
|
|
227
|
+
raise ValueError(
|
|
228
|
+
f'Reference level for bandwidth (bwr={bwr:.2f}) must be < 0 dB'
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# exp(-a t^2) <-> sqrt(pi/a) exp(-pi^2/a * f^2) = g(f)
|
|
232
|
+
|
|
233
|
+
ref = pow(10.0, bwr / 20.0)
|
|
234
|
+
# fdel = fc*bw/2: g(fdel) = ref --- solve this for a
|
|
235
|
+
#
|
|
236
|
+
# pi^2/a * fc^2 * bw^2 /4=-log(ref)
|
|
237
|
+
a = -((pi * fc * bw) ** 2) / (4.0 * log(ref))
|
|
238
|
+
|
|
239
|
+
if isinstance(t, str):
|
|
240
|
+
if t == 'cutoff': # compute cut_off point
|
|
241
|
+
# Solve exp(-a tc**2) = tref for tc
|
|
242
|
+
# tc = sqrt(-log(tref) / a) where tref = 10^(tpr/20)
|
|
243
|
+
if tpr >= 0:
|
|
244
|
+
raise ValueError('Reference level for time cutoff must be < 0 dB')
|
|
245
|
+
tref = pow(10.0, tpr / 20.0)
|
|
246
|
+
return sqrt(-log(tref) / a)
|
|
247
|
+
else:
|
|
248
|
+
raise ValueError("If `t` is a string, it must be 'cutoff'")
|
|
249
|
+
|
|
250
|
+
yenv = exp(-a * t * t)
|
|
251
|
+
yI = yenv * cos(2 * pi * fc * t)
|
|
252
|
+
yQ = yenv * sin(2 * pi * fc * t)
|
|
253
|
+
if not retquad and not retenv:
|
|
254
|
+
return yI
|
|
255
|
+
if not retquad and retenv:
|
|
256
|
+
return yI, yenv
|
|
257
|
+
if retquad and not retenv:
|
|
258
|
+
return yI, yQ
|
|
259
|
+
if retquad and retenv:
|
|
260
|
+
return yI, yQ, yenv
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def chirp(t, f0, t1, f1, method='linear', phi=0, vertex_zero=True, *, complex=False):
|
|
264
|
+
r"""Frequency-swept cosine generator.
|
|
265
|
+
|
|
266
|
+
In the following, 'Hz' should be interpreted as 'cycles per unit';
|
|
267
|
+
there is no requirement here that the unit is one second. The
|
|
268
|
+
important distinction is that the units of rotation are cycles, not
|
|
269
|
+
radians. Likewise, `t` could be a measurement of space instead of time.
|
|
270
|
+
|
|
271
|
+
Parameters
|
|
272
|
+
----------
|
|
273
|
+
t : array_like
|
|
274
|
+
Times at which to evaluate the waveform.
|
|
275
|
+
f0 : float
|
|
276
|
+
Frequency (e.g. Hz) at time t=0.
|
|
277
|
+
t1 : float
|
|
278
|
+
Time at which `f1` is specified.
|
|
279
|
+
f1 : float
|
|
280
|
+
Frequency (e.g. Hz) of the waveform at time `t1`.
|
|
281
|
+
method : {'linear', 'quadratic', 'logarithmic', 'hyperbolic'}, optional
|
|
282
|
+
Kind of frequency sweep. If not given, `linear` is assumed. See
|
|
283
|
+
Notes below for more details.
|
|
284
|
+
phi : float, optional
|
|
285
|
+
Phase offset, in degrees. Default is 0.
|
|
286
|
+
vertex_zero : bool, optional
|
|
287
|
+
This parameter is only used when `method` is 'quadratic'.
|
|
288
|
+
It determines whether the vertex of the parabola that is the graph
|
|
289
|
+
of the frequency is at t=0 or t=t1.
|
|
290
|
+
complex : bool, optional
|
|
291
|
+
This parameter creates a complex-valued analytic signal instead of a
|
|
292
|
+
real-valued signal. It allows the use of complex baseband (in communications
|
|
293
|
+
domain). Default is False.
|
|
294
|
+
|
|
295
|
+
.. versionadded:: 1.15.0
|
|
296
|
+
|
|
297
|
+
Returns
|
|
298
|
+
-------
|
|
299
|
+
y : ndarray
|
|
300
|
+
A numpy array containing the signal evaluated at `t` with the requested
|
|
301
|
+
time-varying frequency. More precisely, the function returns
|
|
302
|
+
``exp(1j*phase + 1j*(pi/180)*phi) if complex else cos(phase + (pi/180)*phi)``
|
|
303
|
+
where `phase` is the integral (from 0 to `t`) of ``2*pi*f(t)``.
|
|
304
|
+
The instantaneous frequency ``f(t)`` is defined below.
|
|
305
|
+
|
|
306
|
+
See Also
|
|
307
|
+
--------
|
|
308
|
+
sweep_poly
|
|
309
|
+
|
|
310
|
+
Notes
|
|
311
|
+
-----
|
|
312
|
+
There are four possible options for the parameter `method`, which have a (long)
|
|
313
|
+
standard form and some allowed abbreviations. The formulas for the instantaneous
|
|
314
|
+
frequency :math:`f(t)` of the generated signal are as follows:
|
|
315
|
+
|
|
316
|
+
1. Parameter `method` in ``('linear', 'lin', 'li')``:
|
|
317
|
+
|
|
318
|
+
.. math::
|
|
319
|
+
f(t) = f_0 + \beta\, t \quad\text{with}\quad
|
|
320
|
+
\beta = \frac{f_1 - f_0}{t_1}
|
|
321
|
+
|
|
322
|
+
Frequency :math:`f(t)` varies linearly over time with a constant rate
|
|
323
|
+
:math:`\beta`.
|
|
324
|
+
|
|
325
|
+
2. Parameter `method` in ``('quadratic', 'quad', 'q')``:
|
|
326
|
+
|
|
327
|
+
.. math::
|
|
328
|
+
f(t) =
|
|
329
|
+
\begin{cases}
|
|
330
|
+
f_0 + \beta\, t^2 & \text{if vertex_zero is True,}\\
|
|
331
|
+
f_1 + \beta\, (t_1 - t)^2 & \text{otherwise,}
|
|
332
|
+
\end{cases}
|
|
333
|
+
\quad\text{with}\quad
|
|
334
|
+
\beta = \frac{f_1 - f_0}{t_1^2}
|
|
335
|
+
|
|
336
|
+
The graph of the frequency f(t) is a parabola through :math:`(0, f_0)` and
|
|
337
|
+
:math:`(t_1, f_1)`. By default, the vertex of the parabola is at
|
|
338
|
+
:math:`(0, f_0)`. If `vertex_zero` is ``False``, then the vertex is at
|
|
339
|
+
:math:`(t_1, f_1)`.
|
|
340
|
+
To use a more general quadratic function, or an arbitrary
|
|
341
|
+
polynomial, use the function `scipy.signal.sweep_poly`.
|
|
342
|
+
|
|
343
|
+
3. Parameter `method` in ``('logarithmic', 'log', 'lo')``:
|
|
344
|
+
|
|
345
|
+
.. math::
|
|
346
|
+
f(t) = f_0 \left(\frac{f_1}{f_0}\right)^{t/t_1}
|
|
347
|
+
|
|
348
|
+
:math:`f_0` and :math:`f_1` must be nonzero and have the same sign.
|
|
349
|
+
This signal is also known as a geometric or exponential chirp.
|
|
350
|
+
|
|
351
|
+
4. Parameter `method` in ``('hyperbolic', 'hyp')``:
|
|
352
|
+
|
|
353
|
+
.. math::
|
|
354
|
+
f(t) = \frac{\alpha}{\beta\, t + \gamma} \quad\text{with}\quad
|
|
355
|
+
\alpha = f_0 f_1 t_1, \ \beta = f_0 - f_1, \ \gamma = f_1 t_1
|
|
356
|
+
|
|
357
|
+
:math:`f_0` and :math:`f_1` must be nonzero.
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
Examples
|
|
361
|
+
--------
|
|
362
|
+
For the first example, a linear chirp ranging from 6 Hz to 1 Hz over 10 seconds is
|
|
363
|
+
plotted:
|
|
364
|
+
|
|
365
|
+
>>> import numpy as np
|
|
366
|
+
>>> from matplotlib.pyplot import tight_layout
|
|
367
|
+
>>> from scipy.signal import chirp, square, ShortTimeFFT
|
|
368
|
+
>>> from scipy.signal.windows import gaussian
|
|
369
|
+
>>> import matplotlib.pyplot as plt
|
|
370
|
+
...
|
|
371
|
+
>>> N, T = 1000, 0.01 # number of samples and sampling interval for 10 s signal
|
|
372
|
+
>>> t = np.arange(N) * T # timestamps
|
|
373
|
+
...
|
|
374
|
+
>>> x_lin = chirp(t, f0=6, f1=1, t1=10, method='linear')
|
|
375
|
+
...
|
|
376
|
+
>>> fg0, ax0 = plt.subplots()
|
|
377
|
+
>>> ax0.set_title(r"Linear Chirp from $f(0)=6\,$Hz to $f(10)=1\,$Hz")
|
|
378
|
+
>>> ax0.set(xlabel="Time $t$ in Seconds", ylabel=r"Amplitude $x_\text{lin}(t)$")
|
|
379
|
+
>>> ax0.plot(t, x_lin)
|
|
380
|
+
>>> plt.show()
|
|
381
|
+
|
|
382
|
+
The following four plots each show the short-time Fourier transform of a chirp
|
|
383
|
+
ranging from 45 Hz to 5 Hz with different values for the parameter `method`
|
|
384
|
+
(and `vertex_zero`):
|
|
385
|
+
|
|
386
|
+
>>> x_qu0 = chirp(t, f0=45, f1=5, t1=N*T, method='quadratic', vertex_zero=True)
|
|
387
|
+
>>> x_qu1 = chirp(t, f0=45, f1=5, t1=N*T, method='quadratic', vertex_zero=False)
|
|
388
|
+
>>> x_log = chirp(t, f0=45, f1=5, t1=N*T, method='logarithmic')
|
|
389
|
+
>>> x_hyp = chirp(t, f0=45, f1=5, t1=N*T, method='hyperbolic')
|
|
390
|
+
...
|
|
391
|
+
>>> win = gaussian(50, std=12, sym=True)
|
|
392
|
+
>>> SFT = ShortTimeFFT(win, hop=2, fs=1/T, mfft=800, scale_to='magnitude')
|
|
393
|
+
>>> ts = ("'quadratic', vertex_zero=True", "'quadratic', vertex_zero=False",
|
|
394
|
+
... "'logarithmic'", "'hyperbolic'")
|
|
395
|
+
>>> fg1, ax1s = plt.subplots(2, 2, sharex='all', sharey='all',
|
|
396
|
+
... figsize=(6, 5), layout="constrained")
|
|
397
|
+
>>> for x_, ax_, t_ in zip([x_qu0, x_qu1, x_log, x_hyp], ax1s.ravel(), ts):
|
|
398
|
+
... aSx = abs(SFT.stft(x_))
|
|
399
|
+
... im_ = ax_.imshow(aSx, origin='lower', aspect='auto', extent=SFT.extent(N),
|
|
400
|
+
... cmap='plasma')
|
|
401
|
+
... ax_.set_title(t_)
|
|
402
|
+
... if t_ == "'hyperbolic'":
|
|
403
|
+
... fg1.colorbar(im_, ax=ax1s, label='Magnitude $|S_z(t,f)|$')
|
|
404
|
+
>>> _ = fg1.supxlabel("Time $t$ in Seconds") # `_ =` is needed to pass doctests
|
|
405
|
+
>>> _ = fg1.supylabel("Frequency $f$ in Hertz")
|
|
406
|
+
>>> plt.show()
|
|
407
|
+
|
|
408
|
+
Finally, the short-time Fourier transform of a complex-valued linear chirp
|
|
409
|
+
ranging from -30 Hz to 30 Hz is depicted:
|
|
410
|
+
|
|
411
|
+
>>> z_lin = chirp(t, f0=-30, f1=30, t1=N*T, method="linear", complex=True)
|
|
412
|
+
>>> SFT.fft_mode = 'centered' # needed to work with complex signals
|
|
413
|
+
>>> aSz = abs(SFT.stft(z_lin))
|
|
414
|
+
...
|
|
415
|
+
>>> fg2, ax2 = plt.subplots()
|
|
416
|
+
>>> ax2.set_title(r"Linear Chirp from $-30\,$Hz to $30\,$Hz")
|
|
417
|
+
>>> ax2.set(xlabel="Time $t$ in Seconds", ylabel="Frequency $f$ in Hertz")
|
|
418
|
+
>>> im2 = ax2.imshow(aSz, origin='lower', aspect='auto',
|
|
419
|
+
... extent=SFT.extent(N), cmap='viridis')
|
|
420
|
+
>>> fg2.colorbar(im2, label='Magnitude $|S_z(t,f)|$')
|
|
421
|
+
>>> plt.show()
|
|
422
|
+
|
|
423
|
+
Note that using negative frequencies makes only sense with complex-valued signals.
|
|
424
|
+
Furthermore, the magnitude of the complex exponential function is one whereas the
|
|
425
|
+
magnitude of the real-valued cosine function is only 1/2.
|
|
426
|
+
"""
|
|
427
|
+
# 'phase' is computed in _chirp_phase, to make testing easier.
|
|
428
|
+
phase = _chirp_phase(t, f0, t1, f1, method, vertex_zero) + np.deg2rad(phi)
|
|
429
|
+
return np.exp(1j * phase) if complex else np.cos(phase)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _chirp_phase(t, f0, t1, f1, method='linear', vertex_zero=True):
|
|
433
|
+
"""
|
|
434
|
+
Calculate the phase used by `chirp` to generate its output.
|
|
435
|
+
|
|
436
|
+
See `chirp` for a description of the arguments.
|
|
437
|
+
|
|
438
|
+
"""
|
|
439
|
+
t = asarray(t)
|
|
440
|
+
f0 = float(f0)
|
|
441
|
+
t1 = float(t1)
|
|
442
|
+
f1 = float(f1)
|
|
443
|
+
if method in ['linear', 'lin', 'li']:
|
|
444
|
+
beta = (f1 - f0) / t1
|
|
445
|
+
phase = 2 * pi * (f0 * t + 0.5 * beta * t * t)
|
|
446
|
+
|
|
447
|
+
elif method in ['quadratic', 'quad', 'q']:
|
|
448
|
+
beta = (f1 - f0) / (t1**2)
|
|
449
|
+
if vertex_zero:
|
|
450
|
+
phase = 2 * pi * (f0 * t + beta * t**3 / 3)
|
|
451
|
+
else:
|
|
452
|
+
phase = 2 * pi * (f1 * t + beta * ((t1 - t) ** 3 - t1**3) / 3)
|
|
453
|
+
|
|
454
|
+
elif method in ['logarithmic', 'log', 'lo']:
|
|
455
|
+
if f0 * f1 <= 0.0:
|
|
456
|
+
raise ValueError(
|
|
457
|
+
'For a logarithmic chirp, f0 and f1 must be '
|
|
458
|
+
'nonzero and have the same sign.'
|
|
459
|
+
)
|
|
460
|
+
if f0 == f1:
|
|
461
|
+
phase = 2 * pi * f0 * t
|
|
462
|
+
else:
|
|
463
|
+
beta = t1 / log(f1 / f0)
|
|
464
|
+
phase = 2 * pi * beta * f0 * (pow(f1 / f0, t / t1) - 1.0)
|
|
465
|
+
|
|
466
|
+
elif method in ['hyperbolic', 'hyp']:
|
|
467
|
+
if f0 == 0 or f1 == 0:
|
|
468
|
+
raise ValueError('For a hyperbolic chirp, f0 and f1 must be nonzero.')
|
|
469
|
+
if f0 == f1:
|
|
470
|
+
# Degenerate case: constant frequency.
|
|
471
|
+
phase = 2 * pi * f0 * t
|
|
472
|
+
else:
|
|
473
|
+
# Singular point: the instantaneous frequency blows up
|
|
474
|
+
# when t == sing.
|
|
475
|
+
sing = -f1 * t1 / (f0 - f1)
|
|
476
|
+
phase = 2 * pi * (-sing * f0) * log(np.abs(1 - t / sing))
|
|
477
|
+
|
|
478
|
+
else:
|
|
479
|
+
raise ValueError(
|
|
480
|
+
"method must be 'linear', 'quadratic', 'logarithmic', "
|
|
481
|
+
f"or 'hyperbolic', but a value of {method!r} was given."
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
return phase
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def sweep_poly(t, poly, phi=0):
|
|
488
|
+
"""
|
|
489
|
+
Frequency-swept cosine generator, with a time-dependent frequency.
|
|
490
|
+
|
|
491
|
+
This function generates a sinusoidal function whose instantaneous
|
|
492
|
+
frequency varies with time. The frequency at time `t` is given by
|
|
493
|
+
the polynomial `poly`.
|
|
494
|
+
|
|
495
|
+
Parameters
|
|
496
|
+
----------
|
|
497
|
+
t : ndarray
|
|
498
|
+
Times at which to evaluate the waveform.
|
|
499
|
+
poly : 1-D array_like or instance of numpy.poly1d
|
|
500
|
+
The desired frequency expressed as a polynomial. If `poly` is
|
|
501
|
+
a list or ndarray of length n, then the elements of `poly` are
|
|
502
|
+
the coefficients of the polynomial, and the instantaneous
|
|
503
|
+
frequency is
|
|
504
|
+
|
|
505
|
+
``f(t) = poly[0]*t**(n-1) + poly[1]*t**(n-2) + ... + poly[n-1]``
|
|
506
|
+
|
|
507
|
+
If `poly` is an instance of numpy.poly1d, then the
|
|
508
|
+
instantaneous frequency is
|
|
509
|
+
|
|
510
|
+
``f(t) = poly(t)``
|
|
511
|
+
|
|
512
|
+
phi : float, optional
|
|
513
|
+
Phase offset, in degrees, Default: 0.
|
|
514
|
+
|
|
515
|
+
Returns
|
|
516
|
+
-------
|
|
517
|
+
sweep_poly : ndarray
|
|
518
|
+
A numpy array containing the signal evaluated at `t` with the
|
|
519
|
+
requested time-varying frequency. More precisely, the function
|
|
520
|
+
returns ``cos(phase + (pi/180)*phi)``, where `phase` is the integral
|
|
521
|
+
(from 0 to t) of ``2 * pi * f(t)``; ``f(t)`` is defined above.
|
|
522
|
+
|
|
523
|
+
See Also
|
|
524
|
+
--------
|
|
525
|
+
chirp
|
|
526
|
+
|
|
527
|
+
Notes
|
|
528
|
+
-----
|
|
529
|
+
.. versionadded:: 0.8.0
|
|
530
|
+
|
|
531
|
+
If `poly` is a list or ndarray of length `n`, then the elements of
|
|
532
|
+
`poly` are the coefficients of the polynomial, and the instantaneous
|
|
533
|
+
frequency is:
|
|
534
|
+
|
|
535
|
+
``f(t) = poly[0]*t**(n-1) + poly[1]*t**(n-2) + ... + poly[n-1]``
|
|
536
|
+
|
|
537
|
+
If `poly` is an instance of `numpy.poly1d`, then the instantaneous
|
|
538
|
+
frequency is:
|
|
539
|
+
|
|
540
|
+
``f(t) = poly(t)``
|
|
541
|
+
|
|
542
|
+
Finally, the output `s` is:
|
|
543
|
+
|
|
544
|
+
``cos(phase + (pi/180)*phi)``
|
|
545
|
+
|
|
546
|
+
where `phase` is the integral from 0 to `t` of ``2 * pi * f(t)``,
|
|
547
|
+
``f(t)`` as defined above.
|
|
548
|
+
|
|
549
|
+
Examples
|
|
550
|
+
--------
|
|
551
|
+
Compute the waveform with instantaneous frequency::
|
|
552
|
+
|
|
553
|
+
f(t) = 0.025*t**3 - 0.36*t**2 + 1.25*t + 2
|
|
554
|
+
|
|
555
|
+
over the interval 0 <= t <= 10.
|
|
556
|
+
|
|
557
|
+
>>> import numpy as np
|
|
558
|
+
>>> from scipy.signal import sweep_poly
|
|
559
|
+
>>> p = np.poly1d([0.025, -0.36, 1.25, 2.0])
|
|
560
|
+
>>> t = np.linspace(0, 10, 5001)
|
|
561
|
+
>>> w = sweep_poly(t, p)
|
|
562
|
+
|
|
563
|
+
Plot it:
|
|
564
|
+
|
|
565
|
+
>>> import matplotlib.pyplot as plt
|
|
566
|
+
>>> plt.subplot(2, 1, 1)
|
|
567
|
+
>>> plt.plot(t, w)
|
|
568
|
+
>>> plt.title("Sweep Poly\\nwith frequency " +
|
|
569
|
+
... "$f(t) = 0.025t^3 - 0.36t^2 + 1.25t + 2$")
|
|
570
|
+
>>> plt.subplot(2, 1, 2)
|
|
571
|
+
>>> plt.plot(t, p(t), 'r', label='f(t)')
|
|
572
|
+
>>> plt.legend()
|
|
573
|
+
>>> plt.xlabel('t')
|
|
574
|
+
>>> plt.tight_layout()
|
|
575
|
+
>>> plt.show()
|
|
576
|
+
|
|
577
|
+
"""
|
|
578
|
+
# 'phase' is computed in _sweep_poly_phase, to make testing easier.
|
|
579
|
+
phase = _sweep_poly_phase(t, poly)
|
|
580
|
+
# Convert to radians.
|
|
581
|
+
phi *= pi / 180
|
|
582
|
+
return cos(phase + phi)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _sweep_poly_phase(t, poly):
|
|
586
|
+
"""
|
|
587
|
+
Calculate the phase used by sweep_poly to generate its output.
|
|
588
|
+
|
|
589
|
+
See `sweep_poly` for a description of the arguments.
|
|
590
|
+
|
|
591
|
+
"""
|
|
592
|
+
# polyint handles lists, ndarrays and instances of poly1d automatically.
|
|
593
|
+
intpoly = polyint(poly)
|
|
594
|
+
phase = 2 * pi * polyval(intpoly, t)
|
|
595
|
+
return phase
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def unit_impulse(shape, idx=None, dtype=float):
|
|
599
|
+
r"""
|
|
600
|
+
Unit impulse signal (discrete delta function) or unit basis vector.
|
|
601
|
+
|
|
602
|
+
Parameters
|
|
603
|
+
----------
|
|
604
|
+
shape : int or tuple of int
|
|
605
|
+
Number of samples in the output (1-D), or a tuple that represents the
|
|
606
|
+
shape of the output (N-D).
|
|
607
|
+
idx : None or int or tuple of int or 'mid', optional
|
|
608
|
+
Index at which the value is 1. If None, defaults to the 0th element.
|
|
609
|
+
If ``idx='mid'``, the impulse will be centered at ``shape // 2`` in
|
|
610
|
+
all dimensions. If an int, the impulse will be at `idx` in all
|
|
611
|
+
dimensions.
|
|
612
|
+
dtype : data-type, optional
|
|
613
|
+
The desired data-type for the array, e.g., ``numpy.int8``. Default is
|
|
614
|
+
``numpy.float64``.
|
|
615
|
+
|
|
616
|
+
Returns
|
|
617
|
+
-------
|
|
618
|
+
y : ndarray
|
|
619
|
+
Output array containing an impulse signal.
|
|
620
|
+
|
|
621
|
+
Notes
|
|
622
|
+
-----
|
|
623
|
+
In digital signal processing literature the unit impulse signal is often
|
|
624
|
+
represented by the Kronecker delta. [1]_ I.e., a signal :math:`u_k[n]`,
|
|
625
|
+
which is zero everywhere except being one at the :math:`k`-th sample,
|
|
626
|
+
can be expressed as
|
|
627
|
+
|
|
628
|
+
.. math::
|
|
629
|
+
|
|
630
|
+
u_k[n] = \delta[n-k] \equiv \delta_{n,k}\ .
|
|
631
|
+
|
|
632
|
+
Furthermore, the unit impulse is frequently interpreted as the discrete-time
|
|
633
|
+
version of the continuous-time Dirac distribution. [2]_
|
|
634
|
+
|
|
635
|
+
References
|
|
636
|
+
----------
|
|
637
|
+
.. [1] "Kronecker delta", *Wikipedia*,
|
|
638
|
+
https://en.wikipedia.org/wiki/Kronecker_delta#Digital_signal_processing
|
|
639
|
+
.. [2] "Dirac delta function" *Wikipedia*,
|
|
640
|
+
https://en.wikipedia.org/wiki/Dirac_delta_function#Relationship_to_the_Kronecker_delta
|
|
641
|
+
|
|
642
|
+
.. versionadded:: 0.19.0
|
|
643
|
+
|
|
644
|
+
Examples
|
|
645
|
+
--------
|
|
646
|
+
An impulse at the 0th element (:math:`\\delta[n]`):
|
|
647
|
+
|
|
648
|
+
>>> from scipy import signal
|
|
649
|
+
>>> signal.unit_impulse(8)
|
|
650
|
+
array([ 1., 0., 0., 0., 0., 0., 0., 0.])
|
|
651
|
+
|
|
652
|
+
Impulse offset by 2 samples (:math:`\\delta[n-2]`):
|
|
653
|
+
|
|
654
|
+
>>> signal.unit_impulse(7, 2)
|
|
655
|
+
array([ 0., 0., 1., 0., 0., 0., 0.])
|
|
656
|
+
|
|
657
|
+
2-dimensional impulse, centered:
|
|
658
|
+
|
|
659
|
+
>>> signal.unit_impulse((3, 3), 'mid')
|
|
660
|
+
array([[ 0., 0., 0.],
|
|
661
|
+
[ 0., 1., 0.],
|
|
662
|
+
[ 0., 0., 0.]])
|
|
663
|
+
|
|
664
|
+
Impulse at (2, 2), using broadcasting:
|
|
665
|
+
|
|
666
|
+
>>> signal.unit_impulse((4, 4), 2)
|
|
667
|
+
array([[ 0., 0., 0., 0.],
|
|
668
|
+
[ 0., 0., 0., 0.],
|
|
669
|
+
[ 0., 0., 1., 0.],
|
|
670
|
+
[ 0., 0., 0., 0.]])
|
|
671
|
+
|
|
672
|
+
Plot the impulse response of a 4th-order Butterworth lowpass filter:
|
|
673
|
+
|
|
674
|
+
>>> imp = signal.unit_impulse(100, 'mid')
|
|
675
|
+
>>> b, a = signal.butter(4, 0.2)
|
|
676
|
+
>>> response = signal.lfilter(b, a, imp)
|
|
677
|
+
|
|
678
|
+
>>> import numpy as np
|
|
679
|
+
>>> import matplotlib.pyplot as plt
|
|
680
|
+
>>> plt.plot(np.arange(-50, 50), imp)
|
|
681
|
+
>>> plt.plot(np.arange(-50, 50), response)
|
|
682
|
+
>>> plt.margins(0.1, 0.1)
|
|
683
|
+
>>> plt.xlabel('Time [samples]')
|
|
684
|
+
>>> plt.ylabel('Amplitude')
|
|
685
|
+
>>> plt.grid(True)
|
|
686
|
+
>>> plt.show()
|
|
687
|
+
|
|
688
|
+
"""
|
|
689
|
+
out = zeros(shape, dtype)
|
|
690
|
+
|
|
691
|
+
shape = np.atleast_1d(shape)
|
|
692
|
+
|
|
693
|
+
if idx is None:
|
|
694
|
+
idx = (0,) * len(shape)
|
|
695
|
+
elif idx == 'mid':
|
|
696
|
+
idx = tuple(shape // 2)
|
|
697
|
+
elif not hasattr(idx, '__iter__'):
|
|
698
|
+
idx = (idx,) * len(shape)
|
|
699
|
+
|
|
700
|
+
out[idx] = 1
|
|
701
|
+
return out
|