├── images
├── lpc_plot.png
├── filt_plot.png
├── cascade_plot.png
└── dft_lpc_plot.png
├── MANIFEST.in
├── examples
├── windows_plot.py
├── roll_magnitude.py
├── lpc_plot.py
├── io_wire.py
├── robotize.py
├── pi.py
├── shepard.py
├── lptv.py
├── butterworth_scipy.py
├── chirp_constant_phon.py
├── gammatone_plots.py
├── iso226_plot.py
├── formants.py
├── keyboard.py
├── zcross_pitch.py
├── dft_pitch.py
├── play_bach_choral.py
├── butterworth_with_noise.py
├── animated_plot.py
├── ode_to_joy.py
├── fmbench.py
├── save_and_memoize_synth.py
└── mcfm.py
├── audiolazy
├── tests
│ ├── test_synth_numpy.py
│ ├── __init__.py
│ ├── test_analysis_numpy.py
│ ├── test_poly_extdep.py
│ ├── test_text.py
│ ├── test_auditory.py
│ ├── test_midi.py
│ ├── test_math.py
│ ├── test_io.py
│ ├── test_itertools.py
│ ├── test_wav.py
│ └── test_synth.py
├── __init__.py
├── lazy_itertools.py
├── lazy_math.py
├── lazy_compat.py
├── lazy_wav.py
├── _internals.py
└── lazy_midi.py
├── docs
├── make_all_docs.py
└── rst_creator.py
├── conftest.py
├── math
├── README.rst
├── lowpass_highpass_bilinear.py
├── lowpass_highpass_digital.py
└── lowpass_highpass_matched_z.py
├── tox.ini
└── setup.py
/images/lpc_plot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danilobellini/audiolazy/HEAD/images/lpc_plot.png
--------------------------------------------------------------------------------
/images/filt_plot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danilobellini/audiolazy/HEAD/images/filt_plot.png
--------------------------------------------------------------------------------
/images/cascade_plot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danilobellini/audiolazy/HEAD/images/cascade_plot.png
--------------------------------------------------------------------------------
/images/dft_lpc_plot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danilobellini/audiolazy/HEAD/images/dft_lpc_plot.png
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.rst *.txt *.ini *.py */*.py */README.rst images/*.png
2 | include audiolazy/tests/*.py audiolazy/tests/*.json
3 |
--------------------------------------------------------------------------------
/examples/windows_plot.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Window functions (in time) plot example
19 | """
20 |
21 | import pylab as plt
22 | from audiolazy import wsymm # Try using "window" instead of wsymm
23 |
24 | size = 256
25 |
26 | for func in wsymm:
27 | plt.plot(func(size), label=func.__name__)
28 |
29 | plt.legend(loc="best")
30 | plt.axis(xmin=-5, xmax=size + 5 - 1, ymin=-.05, ymax=1.05)
31 | plt.title("AudioLazy windows for size of {} samples".format(size))
32 | plt.tight_layout()
33 | plt.ioff()
34 | plt.show()
35 |
--------------------------------------------------------------------------------
/examples/roll_magnitude.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Realtime STFT effect to "roll" the magnitude spectrum while keeping the phase
19 | """
20 |
21 | from audiolazy import *
22 | import numpy as np
23 | import sys
24 |
25 | @stft(size=2048, hop=682, wnd=window.hann, ola_wnd=window.hann)
26 | def roll_mag(spectrum):
27 | mag = abs(spectrum)
28 | phases = np.angle(spectrum)
29 | return np.roll(mag, 16) * np.exp(1j * phases)
30 |
31 | api = sys.argv[1] if sys.argv[1:] else None
32 | chunks.size = 1 if api == "jack" else 16
33 | with AudioIO(True, api=api) as pr:
34 | pr.play(roll_mag(pr.record()))
35 |
--------------------------------------------------------------------------------
/examples/lpc_plot.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | LPC plot with DFT, showing two formants (magnitude peaks)
19 | """
20 |
21 | from audiolazy import sHz, sin_table, str2freq, lpc
22 | import pylab
23 |
24 | rate = 22050
25 | s, Hz = sHz(rate)
26 | size = 512
27 | table = sin_table.harmonize({1: 1, 2: 5, 3: 3, 4: 2, 6: 9, 8: 1}).normalize()
28 |
29 | data = table(str2freq("Bb3") * Hz).take(size)
30 | filt = lpc(data, order=14) # Analysis filter
31 | gain = 1e-2 # Gain just for alignment with DFT
32 |
33 | # Plots the synthesis filter
34 | # - If blk is given, plots the block DFT together with the filter
35 | # - If rate is given, shows the frequency range in Hz
36 | (gain / filt).plot(blk=data, rate=rate, samples=1024, unwrap=False)
37 | pylab.ioff()
38 | pylab.show()
39 |
--------------------------------------------------------------------------------
/examples/io_wire.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Simple I/O wire example, connecting the input directly to the output
19 |
20 | This example uses the default PortAudio API, however you can change it by
21 | using the "api" keyword argument in AudioIO creation, like
22 |
23 | with AudioIO(True, api="jack") as pr:
24 |
25 | obviously, you can use another API instead (like "alsa").
26 |
27 | Note
28 | ----
29 | When using JACK, keep chunks.size = 1
30 | """
31 |
32 | from audiolazy import chunks, AudioIO
33 | import sys
34 |
35 | # Choose API via command-line
36 | api = sys.argv[1] if sys.argv[1:] else None
37 |
38 | # Amount of samples per chunk to be sent to PortAudio
39 | chunks.size = 1 if api == "jack" else 16
40 |
41 | with AudioIO(True, api=api) as pr: # A player-recorder
42 | pr.play(pr.record())
43 |
--------------------------------------------------------------------------------
/examples/robotize.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Realtime STFT effect to robotize a voice (or anything else)
19 |
20 | This is done by removing (zeroing) the phases, which means a single spectrum
21 | block processing function that keeps the magnitudes and removes the phase, a
22 | function a.k.a. "abs", the absolute value. The initial zero-phasing isn't
23 | needed at all since the phases are going to be removed, so the "before" step
24 | can be safely removed.
25 | """
26 |
27 | from audiolazy import window, stft, chunks, AudioIO
28 | import sys
29 |
30 | wnd = window.hann
31 | robotize = stft(abs, size=1024, hop=441, before=None, wnd=wnd, ola_wnd=wnd)
32 |
33 | api = sys.argv[1] if sys.argv[1:] else None
34 | chunks.size = 1 if api == "jack" else 16
35 | with AudioIO(True, api=api) as pr:
36 | pr.play(robotize(pr.record()))
37 |
--------------------------------------------------------------------------------
/examples/pi.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Calculate "pi" using the Madhava-Gregory-Leibniz series and Machin formula
19 | """
20 |
21 | from __future__ import division, print_function
22 | from audiolazy import Stream, thub, count, z, pi # For comparison
23 |
24 | def mgl_seq(x):
25 | """
26 | Sequence whose sum is the Madhava-Gregory-Leibniz series.
27 |
28 | [x, -x^3/3, x^5/5, -x^7/7, x^9/9, -x^11/11, ...]
29 |
30 | Returns
31 | -------
32 | An endless sequence that has the property
33 | ``atan(x) = sum(mgl_seq(x))``.
34 | Usually you would use the ``atan()`` function, not this one.
35 |
36 | """
37 | odd_numbers = thub(count(start=1, step=2), 2)
38 | return Stream(1, -1) * x ** odd_numbers / odd_numbers
39 |
40 |
41 | def atan_mgl(x, n=10):
42 | """
43 | Finds the arctan using the Madhava-Gregory-Leibniz series.
44 | """
45 | acc = 1 / (1 - z ** -1) # Accumulator filter
46 | return acc(mgl_seq(x)).skip(n-1).take()
47 |
48 |
49 | if __name__ == "__main__":
50 | print("Reference (for comparison):", repr(pi))
51 | print()
52 |
53 | print("Machin formula (fast)")
54 | pi_machin = 4 * (4 * atan_mgl(1/5) - atan_mgl(1/239))
55 | print("Found:", repr(pi_machin))
56 | print("Error:", repr(abs(pi - pi_machin)))
57 | print()
58 |
59 | print("Madhava-Gregory-Leibniz series for 45 degrees (slow)")
60 | pi_mgl_series = 4 * atan_mgl(1, n=1e6) # Sums 1,000,000 items...slow...
61 | print("Found:", repr(pi_mgl_series))
62 | print("Error:", repr(abs(pi - pi_mgl_series)))
63 | print()
64 |
--------------------------------------------------------------------------------
/audiolazy/tests/test_synth_numpy.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # This file is part of AudioLazy, the signal processing Python package.
3 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
4 | #
5 | # AudioLazy is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, version 3 of the License.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 | """
17 | Testing module for the lazy_synth module by using numpy as an oracle
18 | """
19 |
20 | import pytest
21 | p = pytest.mark.parametrize
22 |
23 | import numpy as np
24 | from math import pi
25 |
26 | # Audiolazy internal imports
27 | from ..lazy_misc import almost_eq, sHz
28 | from ..lazy_synth import adsr, sinusoid
29 |
30 |
31 | def test_adsr():
32 | rate = 44100
33 | dur = 3 * rate
34 | sustain_level = .8
35 | attack = np.linspace(0., 1., num=int(np.round(20e-3 * rate)), endpoint=False)
36 | decay = np.linspace(1., sustain_level, num=int(np.round(30e-3 * rate)),
37 | endpoint=False)
38 | release = np.linspace(sustain_level, 0., num=int(np.round(50e-3 * rate)),
39 | endpoint=False)
40 | sustain_dur = dur - len(attack) - len(decay) - len(release)
41 | sustain = sustain_level * np.ones(sustain_dur)
42 | env = np.hstack([attack, decay, sustain, release])
43 |
44 | s, Hz = sHz(rate)
45 | ms = 1e-3 * s
46 | assert almost_eq(env, adsr(dur=3*s, a=20*ms, d=30*ms, s=.8, r=50*ms))
47 |
48 |
49 | def test_sinusoid():
50 | rate = 44100
51 | dur = 3 * rate
52 |
53 | freq220 = 220 * (2 * np.pi / rate)
54 | freq440 = 440 * (2 * np.pi / rate)
55 | phase220 = np.arange(dur, dtype=np.float64) * freq220
56 | phase440 = np.arange(dur, dtype=np.float64) * freq440
57 | sin_data = np.sin(phase440 + np.sin(phase220) * np.pi)
58 |
59 | s, Hz = sHz(rate)
60 | assert almost_eq.diff(sin_data,
61 | sinusoid(freq=440*Hz,
62 | phase=sinusoid(220*Hz) * pi
63 | ).take(int(3 * s)),
64 | max_diff=1e-8
65 | )
66 |
--------------------------------------------------------------------------------
/examples/shepard.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Example based on the Shepard tone
19 | """
20 |
21 | from __future__ import division
22 | from audiolazy import (sHz, Streamix, log2, line, window, sinusoid, AudioIO,
23 | chunks)
24 | import sys
25 |
26 | # Basic initialization
27 | rate = 44100
28 | s, Hz = sHz(rate)
29 | kHz = 1e3 * Hz
30 |
31 | # Some parameters
32 | table_len = 8192
33 | min_freq = 20 * Hz
34 | max_freq = 10 * kHz
35 | duration = 60 * s
36 |
37 | # "Track-by-track" partials configuration
38 | noctaves = abs(log2(max_freq/min_freq))
39 | octave_duration = duration / noctaves
40 | smix = Streamix()
41 | data = [] # Global: keeps one parcial "track" for all uses (but the first)
42 |
43 | # Inits "data"
44 | def partial():
45 | smix.add(octave_duration, partial_cached()) # Next track/partial event
46 | # Octave-based frequency values sequence
47 | scale = 2 ** line(duration, finish=True)
48 | partial_freq = (scale - 1) * (max_freq - min_freq) + min_freq
49 | # Envelope to "hide" the partial beginning/ending
50 | env = [k ** 2 for k in window.hamming(int(round(duration)))]
51 | # The generator, properly:
52 | for el in env * sinusoid(partial_freq) / noctaves:
53 | data.append(el)
54 | yield el
55 |
56 | # Replicator ("track" data generator)
57 | def partial_cached():
58 | smix.add(octave_duration, partial_cached()) # Next track/partial event
59 | for el in data:
60 | yield el
61 |
62 | # Play!
63 | smix.add(0, partial()) # Starts the mixing with the first track/partial
64 | api = sys.argv[1] if sys.argv[1:] else None # Choose API via command-line
65 | chunks.size = 1 if api == "jack" else 16
66 | with AudioIO(True, api=api) as player:
67 | player.play(smix, rate=rate)
68 |
--------------------------------------------------------------------------------
/examples/lptv.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | LPTV (Linear Periodically Time Variant) filter example (a.k.a. PLTV)
19 | """
20 |
21 | from audiolazy import sHz, sinusoid, Stream, AudioIO, z, pi, chunks
22 | import time, sys
23 |
24 | # Basic initialization
25 | rate = 44100
26 | s, Hz = sHz(rate)
27 |
28 | # Some time-variant coefficients
29 | cycle_a1 = [.1, .2, .1, 0, -.1, -.2, -.1, 0]
30 | cycle_a2 = [.1, 0, -.1, 0, 0]
31 | a1 = Stream(*cycle_a1)
32 | a2 = Stream(*cycle_a2) * 2
33 | b1 = sinusoid(18 * Hz) # Sine phase
34 | b2 = sinusoid(freq=7 * Hz, phase=pi/2) # Cosine phase
35 |
36 | # The filter
37 | filt = (1 + b1 * z ** -1 + b2 * z ** -2 + .7 * z ** -5)
38 | filt /= (1 - a1 * z ** -1 - a2 * z ** -2 - .1 * z ** -3)
39 |
40 | # A really simple input
41 | input_data = sinusoid(220 * Hz)
42 |
43 | # Let's play it!
44 | api = sys.argv[1] if sys.argv[1:] else None # Choose API via command-line
45 | chunks.size = 1 if api == "jack" else 16
46 | with AudioIO(api=api) as player:
47 | th = player.play(input_data, rate=rate)
48 | time.sleep(1) # Wait a sec
49 | th.stop()
50 | time.sleep(1) # One sec "paused"
51 | player.play(filt(input_data), rate=rate) # It's nice with rate/2 here =)
52 | time.sleep(3) # Play the "filtered" input (3 secs)
53 |
54 | # Quiz!
55 | #
56 | # Question 1: What's the filter "cycle" duration?
57 | # Hint: Who cares?
58 | #
59 | # Question 2: Does the filter need to be periodic?
60 | # Hint: Import white_noise and try to put this before defining the filt:
61 | # a1 *= white_noise()
62 | # a2 *= white_noise()
63 | #
64 | # Question 3: Does the input need to be periodic?
65 | # Hint: Import comb and white_noise. Now try to use this as the input:
66 | # .9 * sinusoid(220 * Hz) + .01 * comb(200, .9)(white_noise())
67 |
--------------------------------------------------------------------------------
/examples/butterworth_scipy.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Butterworth filter from SciPy as a ZFilter instance, with plots
19 |
20 | One resonator (first order filter) is used for comparison with the
21 | butterworth from the example (third order filter). Both has zeros at
22 | 1 (DC level) and -1 (Nyquist).
23 | """
24 |
25 | from __future__ import print_function
26 | from audiolazy import sHz, ZFilter, dB10, resonator, pi
27 | from scipy.signal import butter, buttord
28 | import pylab
29 |
30 | # Example
31 | rate = 44100
32 | s, Hz = sHz(rate)
33 | wp = pylab.array([100 * Hz, 240 * Hz]) # Bandpass range in rad/sample
34 | ws = pylab.array([80 * Hz, 260 * Hz]) # Bandstop range in rad/sample
35 |
36 | # Let's use wp/pi since SciPy defaults freq from 0 to 1 (Nyquist frequency)
37 | order, new_wp_divpi = buttord(wp/pi, ws/pi, gpass=dB10(.6), gstop=dB10(.4))
38 | ssfilt = butter(order, new_wp_divpi, btype="bandpass")
39 | filt_butter = ZFilter(ssfilt[0].tolist(), ssfilt[1].tolist())
40 |
41 | # Some debug information
42 | new_wp = new_wp_divpi * pi
43 | print("Butterworth filter order:", order) # Should be 3
44 | print("Bandpass ~3dB range (in Hz):", new_wp / Hz)
45 |
46 | # Resonator using only the frequency and bandwidth from the Butterworth filter
47 | freq = new_wp.mean()
48 | bw = new_wp[1] - new_wp[0]
49 | filt_reson = resonator.z_exp(freq, bw)
50 |
51 | # Plots with MatPlotLib
52 | kwargs = {
53 | "min_freq": 10 * Hz,
54 | "max_freq": 800 * Hz,
55 | "rate": rate, # Ensure frequency unit in plot is Hz
56 | }
57 | filt_butter.plot(pylab.figure("From scipy.signal.butter"), **kwargs)
58 | filt_reson.plot(pylab.figure("From audiolazy.resonator.z_exp"), **kwargs)
59 | filt_butter.zplot(pylab.figure("Zeros/Poles from scipy.signal.butter"))
60 | filt_reson.zplot(pylab.figure("Zeros/Poles from audiolazy.resonator.z_exp"))
61 | pylab.ioff()
62 | pylab.show()
63 |
--------------------------------------------------------------------------------
/docs/make_all_docs.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | AudioLazy documentation creator via Sphinx
19 |
20 | Note
21 | ----
22 | You should call rst_creator first!
23 | """
24 |
25 | import shlex, sphinx, sys
26 | from subprocess import call
27 |
28 | # Call string templates
29 | sphinx_template = "sphinx-build -b {out_type} -d {build_dir}/doctrees "\
30 | "-D latex_paper_size=a4 . {build_dir}/{out_type}"
31 | make_template = "make -C {build_dir}/{out_type} {make_param}"
32 |
33 | # Make targets given the output type
34 | make_target = {"latex": "all-pdf",
35 | "texinfo": "info"}
36 |
37 | def call_sphinx(out_type, build_dir = "build"):
38 | """
39 | Call the ``sphinx-build`` for the given output type and the ``make`` when
40 | the target has this possibility.
41 |
42 | Parameters
43 | ----------
44 | out_type :
45 | A builder name for ``sphinx-build``. See the full list at
46 | ``_.
47 | build_dir :
48 | Directory for storing the output. Defaults to "build".
49 |
50 | """
51 | sphinx_string = sphinx_template.format(build_dir=build_dir,
52 | out_type=out_type)
53 | if sphinx.main(shlex.split(sphinx_string)) != 0:
54 | raise RuntimeError("Something went wrong while building '{0}'"
55 | .format(out_type))
56 | if out_type in make_target:
57 | make_string = make_template.format(build_dir=build_dir,
58 | out_type=out_type,
59 | make_param=make_target[out_type])
60 | call(shlex.split(make_string)) # Errors here don't need to stop anything
61 |
62 | # Calling this as a script builds/makes all targets in the list below
63 | if __name__ == "__main__":
64 | for target in sys.argv[1:] or ["text", "html", "latex", "man",
65 | "texinfo", "epub"]:
66 | call_sphinx(target)
67 |
--------------------------------------------------------------------------------
/examples/chirp_constant_phon.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Constant phon (ISO226) sinusoid glissando/chirp/glide
19 |
20 | Plays a chirp from ``fstart`` to ``fend``, but a fade in/out is
21 | done on each of these two frequencies, before/after the chirp is played
22 | and with the same duration each, using the ``total_duration`` and
23 | ``chirp_duration`` values.
24 |
25 | Obviously the actual sound depends on the hardware, and a constant phon curve
26 | needs a hardware dB SPL calibration for the needed frequency range.
27 |
28 | Besides AudioLazy, this example needs Scipy and Pyaudio.
29 | """
30 | from audiolazy import *
31 | if PYTHON2:
32 | input = raw_input
33 |
34 | rate = 44100 # samples/s
35 | fstart, fend = 16, 20000 # Hz
36 | intensity = 50 # phons
37 | chirp_duration = 5 # seconds
38 | total_duration = 9 # seconds
39 |
40 | assert total_duration > chirp_duration
41 |
42 | def finalize(zeros_dur):
43 | print("Finished!")
44 | for el in zeros(zeros_dur):
45 | yield el
46 |
47 | def dB2magnitude(logpower):
48 | return 10 ** (logpower / 20)
49 |
50 | s, Hz = sHz(rate)
51 | freq2dB = phon2dB.iso226(intensity)
52 |
53 | freq = thub(2 ** line(chirp_duration * s, log2(fstart), log2(fend)), 2)
54 | gain = thub(dB2magnitude(freq2dB(freq)), 2)
55 | maxgain = max(gain)
56 |
57 | unclick_dur = rint((total_duration - chirp_duration) * s / 2)
58 | gstart = line(unclick_dur, 0, dB2magnitude(freq2dB(fstart)) / maxgain)
59 | gend = line(unclick_dur, dB2magnitude(freq2dB(fend)) / maxgain, 0)
60 |
61 | sfreq = chain(repeat(fstart, unclick_dur), freq, repeat(fend, unclick_dur))
62 | sgain = chain(gstart, gain / maxgain, gend)
63 |
64 | snd = sinusoid(sfreq * Hz) * sgain
65 |
66 | with AudioIO(True) as player:
67 | refgain = dB2magnitude(freq2dB(1e3)) / maxgain
68 | th = player.play(sinusoid(1e3 * Hz) * refgain)
69 | input("Playing the 1 kHz reference tone. You should calibrate the output "
70 | "to get {0} dB SPL and press enter to continue.".format(intensity))
71 | th.stop()
72 | print("Playing the chirp!")
73 | player.play(chain(snd, finalize(.5 * s)), rate=rate)
74 |
--------------------------------------------------------------------------------
/examples/gammatone_plots.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Gammatone frequency and impulse response plots example
19 | """
20 |
21 | from __future__ import division
22 | from audiolazy import (erb, gammatone, gammatone_erb_constants, sHz, impulse,
23 | dB20)
24 | from numpy import linspace, ceil
25 | from matplotlib import pyplot as plt
26 |
27 | # Initialization info
28 | rate = 44100
29 | s, Hz = sHz(rate)
30 | ms = 1e-3 * s
31 | plot_freq_time = {80.: 60 * ms,
32 | 100.: 50 * ms,
33 | 200.: 40 * ms,
34 | 500.: 25 * ms,
35 | 800.: 20 * ms,
36 | 1000.: 15 * ms}
37 | freq = linspace(0.1, 2 * max(freq for freq in plot_freq_time), 100)
38 |
39 | fig1 = plt.figure("Frequency response", figsize=(16, 9), dpi=60)
40 | fig2 = plt.figure("Impulse response", figsize=(16, 9), dpi=60)
41 |
42 | # Plotting loop
43 | for idx, (fc, endtime) in enumerate(sorted(plot_freq_time.items()), 1):
44 | # Configuration for the given frequency
45 | num_samples = int(round(endtime))
46 | time_scale = linspace(0, num_samples / ms, num_samples)
47 | bw = gammatone_erb_constants(4)[0] * erb(fc * Hz, Hz)
48 |
49 | # Subplot configuration
50 | plt.figure(1)
51 | plt.subplot(2, ceil(len(plot_freq_time) / 2), idx)
52 | plt.title("Frequency response - {0} Hz".format(fc))
53 | plt.xlabel("Frequency (Hz)")
54 | plt.ylabel("Gain (dB)")
55 |
56 | plt.figure(2)
57 | plt.subplot(2, ceil(len(plot_freq_time) / 2), idx)
58 | plt.title("Impulse response - {0} Hz".format(fc))
59 | plt.xlabel("Time (ms)")
60 | plt.ylabel("Amplitude")
61 |
62 | # Plots each filter frequency and impulse response
63 | for gt, config in zip(gammatone, ["b-", "g--", "r-.", "k:"]):
64 | filt = gt(fc * Hz, bw)
65 |
66 | plt.figure(1)
67 | plt.plot(freq, dB20(filt.freq_response(freq * Hz)), config,
68 | label=gt.__name__)
69 |
70 | plt.figure(2)
71 | plt.plot(time_scale, filt(impulse()).take(num_samples), config,
72 | label=gt.__name__)
73 |
74 | # Finish
75 | for graph in fig1.axes + fig2.axes:
76 | graph.grid()
77 | graph.legend(loc="best")
78 |
79 | fig1.tight_layout()
80 | fig2.tight_layout()
81 |
82 | plt.ioff()
83 | plt.show()
--------------------------------------------------------------------------------
/conftest.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # This file is part of AudioLazy, the signal processing Python package.
3 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
4 | #
5 | # AudioLazy is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, version 3 of the License.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 | """
17 | AudioLazy testing configuration module for py.test
18 | """
19 |
20 | def pytest_configure(config):
21 | """
22 | Called by py.test, this function is needed to ensure that doctests from
23 | strategies docstrings inside StrategyDict instances are collected for
24 | testing.
25 | """
26 | # Any import done by this function won't count for coverage afterwards, so
27 | # AudioLazy can't be imported here! Solution is monkeypatching the doctest
28 | # finding mechanism to import AudioLazy just there
29 | import doctest, types, functools
30 | old_find = doctest.DocTestFinder.find
31 |
32 | @functools.wraps(old_find)
33 | def find(self, obj, name=None, module=None, **kwargs):
34 | tests = old_find(self, obj, name=name, module=module, **kwargs)
35 | if not isinstance(obj, types.ModuleType):
36 | return tests
37 |
38 | # Adds the doctests from strategies inside StrategyDict instances
39 | from audiolazy import StrategyDict
40 | module_name = obj.__name__
41 | for name, attr in vars(obj).items(): # We know it's a module
42 | if isinstance(attr, StrategyDict):
43 | for st in attr: # Each strategy can have a doctest
44 | if st.__module__ == module_name: # Avoid stuff from otherwhere
45 | sname = ".".join([module_name, name, st.__name__])
46 | tests.extend(old_find(self, st, name=sname, module=obj, **kwargs))
47 | tests.sort()
48 | return tests
49 |
50 | doctest.DocTestFinder.find = find
51 |
52 |
53 | try:
54 | import numpy as np
55 |
56 | if np is not None and np.__version__ >= "1.14":
57 | np.set_printoptions(legacy="1.13")
58 |
59 | except ImportError:
60 | from _pytest.doctest import DoctestItem
61 | import pytest, re
62 |
63 | nn_regex = re.compile(".*#[^#]*\s*needs?\s*numpy\s*$", re.IGNORECASE)
64 |
65 | def pytest_runtest_setup(item):
66 | """
67 | Skip doctests that need Numpy, if it's not found. A doctest that needs
68 | numpy should include a doctest example that ends with a comment with
69 | the words "Need Numpy" (or "Needs Numpy"), no matter the case nor the
70 | amount of whitespaces.
71 | """
72 | if isinstance(item, DoctestItem) and \
73 | any(nn_regex.match(ex.source) for ex in item.dtest.examples):
74 | pytest.skip("Module numpy not found")
75 |
--------------------------------------------------------------------------------
/examples/iso226_plot.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Plots ISO/FDIS 226:2003 equal loudness contour curves
19 |
20 | This is based on figure A.1 of ISO226, and needs Scipy and Matplotlib
21 | """
22 |
23 | from __future__ import division
24 |
25 | from audiolazy import exp, line, ln, phon2dB, xrange
26 | import pylab
27 |
28 | title = "ISO226 equal loudness curves"
29 | freqs = list(exp(line(2048, ln(20), ln(12500), finish=True)))
30 | pylab.figure(title, figsize=[8, 4.5], dpi=120)
31 |
32 | # Plots threshold
33 | freq2dB_threshold = phon2dB.iso226(None) # Threshold
34 | pylab.plot(freqs, freq2dB_threshold(freqs), color="blue", linestyle="--")
35 | pylab.text(300, 5, "Hearing threshold", fontsize=8,
36 | horizontalalignment="right")
37 |
38 | # Plots 20 to 80 phons
39 | for loudness in xrange(20, 81, 10): # in phons
40 | freq2dB = phon2dB.iso226(loudness)
41 | pylab.plot(freqs, freq2dB(freqs), color="black")
42 | pylab.text(850, loudness + 2, "%d phon" % loudness, fontsize=8,
43 | horizontalalignment="center")
44 |
45 | # Plots 90 phons
46 | freq2dB_90phon = phon2dB.iso226(90)
47 | freqs4k1 = list(exp(line(2048, ln(20), ln(4100), finish=True)))
48 | pylab.plot(freqs4k1, freq2dB_90phon(freqs4k1), color="black")
49 | pylab.text(850, 92, "90 phon", fontsize=8, horizontalalignment="center")
50 |
51 | # Plots 10 and 100 phons
52 | freq2dB_10phon = phon2dB.iso226(10)
53 | freq2dB_100phon = phon2dB.iso226(100)
54 | freqs1k = list(exp(line(1024, ln(20), ln(1000), finish=True)))
55 | pylab.plot(freqs, freq2dB_10phon(freqs), color="green", linestyle=":")
56 | pylab.plot(freqs1k, freq2dB_100phon(freqs1k), color="green", linestyle=":")
57 | pylab.text(850, 12, "10 phon", fontsize=8, horizontalalignment="center")
58 | pylab.text(850, 102, "100 phon", fontsize=8, horizontalalignment="center")
59 |
60 | # Plot axis config
61 | pylab.axis(xmin=16, xmax=16000, ymin=-10, ymax=130)
62 | pylab.xscale("log")
63 | pylab.yticks(list(xrange(-10, 131, 10)))
64 | xticks_values = [16, 31.5, 63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000]
65 | pylab.xticks(xticks_values, xticks_values)
66 | pylab.grid() # The grid follows the ticks
67 |
68 | # Plot labels
69 | pylab.title(title)
70 | pylab.xlabel("Frequency (Hz)")
71 | pylab.ylabel("Sound Pressure (dB)")
72 |
73 | # Finish
74 | pylab.tight_layout()
75 | pylab.ioff()
76 | pylab.show()
77 |
--------------------------------------------------------------------------------
/audiolazy/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # This file is part of AudioLazy, the signal processing Python package.
3 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
4 | #
5 | # AudioLazy is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, version 3 of the License.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 | """
17 | AudioLazy testing sub-package
18 | """
19 |
20 | import pytest
21 | import types
22 | from importlib import import_module, sys
23 |
24 | # Audiolazy internal imports
25 | from ..lazy_compat import meta
26 | from ..lazy_core import AbstractOperatorOverloaderMeta
27 |
28 |
29 | def skipper(msg="There's something not supported in this environment"):
30 | """
31 | Internal function to work as the last argument in a ``getattr`` call to
32 | help skip environment-specific tests when needed.
33 | """
34 | def skip(*args, **kwargs):
35 | pytest.skip(msg.format(*args, **kwargs))
36 | return skip
37 |
38 |
39 | class XFailerMeta(AbstractOperatorOverloaderMeta):
40 | """
41 | Metaclass for XFailer, ensuring every operator use is a pytest.xfail call.
42 | """
43 | def __binary__(cls, op):
44 | return lambda self, other: self()
45 | __unary__ = __rbinary__ = __binary__
46 |
47 |
48 | class XFailer(meta(metaclass=XFailerMeta)):
49 | """
50 | Class that responds to mostly uses as a pytest.xfail call.
51 | """
52 | def __init__(self, module):
53 | self.module = module
54 |
55 | def __call__(self, *args, **kwargs):
56 | pytest.xfail(reason="Module {} not found".format(self.module))
57 |
58 | def __getattr__(self, name):
59 | return self.__call__
60 |
61 | __iter__ = __call__
62 |
63 |
64 | class XFailerModule(types.ModuleType):
65 | """
66 | Internal fake module creator to ensure xfail in all functions, if module
67 | doesn't exist.
68 | """
69 | def __init__(self, name):
70 | try:
71 | if isinstance(import_module(name.split(".", 1)[0]), XFailerModule):
72 | raise ImportError
73 | import_module(name)
74 | except (ImportError, pytest.xfail.Exception):
75 | sys.modules[name] = self
76 | self.__name__ = name
77 |
78 | __file__ = __path__ = __loader__ = ""
79 |
80 | def __getattr__(self, name):
81 | return XFailer(self.__name__)
82 |
83 |
84 | # Creates an XFailer for each module that isn't available
85 | XFailerModule("numpy")
86 | XFailerModule("numpy.fft")
87 | XFailerModule("numpy.linalg")
88 | XFailerModule("_portaudio")
89 | XFailerModule("pyaudio")
90 | XFailerModule("scipy")
91 | XFailerModule("scipy.optimize")
92 | XFailerModule("scipy.signal")
93 | XFailerModule("scipy.interpolate")
94 | XFailerModule("sympy")
95 |
--------------------------------------------------------------------------------
/examples/formants.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Voiced "ah-eh-ee-oh-oo" based on resonators at formant frequencies
19 | """
20 |
21 | from __future__ import unicode_literals, print_function
22 |
23 | from audiolazy import (sHz, maverage, rint, AudioIO, ControlStream,
24 | CascadeFilter, resonator, saw_table, chunks)
25 | from time import sleep
26 | import sys
27 |
28 | # Script input, change this with symbols from the table below
29 | vowels = "aɛiɒu"
30 |
31 | # Formant table from in http://en.wikipedia.org/wiki/Formant
32 | formants = {
33 | "i": [240, 2400],
34 | "y": [235, 2100],
35 | "e": [390, 2300],
36 | "ø": [370, 1900],
37 | "ɛ": [610, 1900],
38 | "œ": [585, 1710],
39 | "a": [850, 1610],
40 | "æ": [820, 1530],
41 | "ɑ": [750, 940],
42 | "ɒ": [700, 760],
43 | "ʌ": [600, 1170],
44 | "ɔ": [500, 700],
45 | "ɤ": [460, 1310],
46 | "o": [360, 640],
47 | "ɯ": [300, 1390],
48 | "u": [250, 595],
49 | }
50 |
51 |
52 | # Initialization
53 | rate = 44100
54 | s, Hz = sHz(rate)
55 | inertia_dur = .5 * s
56 | inertia_filter = maverage(rint(inertia_dur))
57 |
58 | api = sys.argv[1] if sys.argv[1:] else None # Choose API via command-line
59 | chunks.size = 1 if api == "jack" else 16
60 |
61 | with AudioIO(api=api) as player:
62 | first_coeffs = formants[vowels[0]]
63 |
64 | # These are signals to be changed during the synthesis
65 | f1 = ControlStream(first_coeffs[0] * Hz)
66 | f2 = ControlStream(first_coeffs[1] * Hz)
67 | gain = ControlStream(0) # For fading in
68 |
69 | # Creates the playing signal
70 | filt = CascadeFilter([
71 | resonator.z_exp(inertia_filter(f1).skip(inertia_dur), 400 * Hz),
72 | resonator.z_exp(inertia_filter(f2).skip(inertia_dur), 2000 * Hz),
73 | ])
74 | sig = filt((saw_table)(100 * Hz)) * inertia_filter(gain)
75 |
76 | th = player.play(sig)
77 | for vowel in vowels:
78 | coeffs = formants[vowel]
79 | print("Now playing: ", vowel)
80 | f1.value = coeffs[0] * Hz
81 | f2.value = coeffs[1] * Hz
82 | gain.value = 1 # Fade in the first vowel, changes nothing afterwards
83 | sleep(2)
84 |
85 | # Fade out
86 | gain.value = 0
87 | sleep(inertia_dur / s + .2) # Divide by s because here it's already
88 | # expecting a value in seconds, and we don't
89 | # want ot give a value in a time-squaed unit
90 | # like s ** 2
91 |
--------------------------------------------------------------------------------
/math/README.rst:
--------------------------------------------------------------------------------
1 | ..
2 | This file is part of AudioLazy, the signal processing Python package.
3 | Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
4 |
5 | AudioLazy is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU General Public License as published by
7 | the Free Software Foundation, version 3 of the License.
8 |
9 | This program is distributed in the hope that it will be useful,
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | GNU General Public License for more details.
13 |
14 | You should have received a copy of the GNU General Public License
15 | along with this program. If not, see .
16 |
17 | AudioLazy Math
18 | ==============
19 |
20 | This directory have scripts with some symbolic math calculations that should
21 | be seen as a proof, or perhaps as a helper on understanding, for some
22 | equations written as part of the AudioLazy code. For such a symbolic
23 | processing, the `Sympy `__ CAS (Computer Algebra System)
24 | Python package was used.
25 |
26 | Originally, some of these results were done manually, but all were needed
27 | for designing some part of AudioLazy. Regardless of their difficulty,
28 | the blocks implemented in AudioLazy just use the results from here, and
29 | doesn't require Sympy to work.
30 |
31 |
32 | Running
33 | -------
34 |
35 | They're all scripts, you should just run them or call Python. Works on both
36 | Python 2 and 3, but be sure to have Sympy installed before that::
37 |
38 | pip install sympy
39 |
40 | So you can run them directly like you would with an AudioLazy example, with
41 | one of the following lines::
42 |
43 | ./script_name.py
44 | python script_name.py
45 | python3 script_name.py
46 |
47 | (where obviously you should replace ``script_name`` with the script file
48 | name).
49 |
50 |
51 | Proofs
52 | ------
53 |
54 | * `lowpass_highpass_bilinear.py `__
55 |
56 | An extra proof using the bilinear transformation method for designing the
57 | single pole and single zero IIR lowpass and highpass filters from their
58 | respective Laplace filters prewarped at the desired cut-off frequencies.
59 | The result matches the ``highpass.z`` and ``lowpass.z`` strategies.
60 |
61 | * `lowpass_highpass_digital.py `__
62 |
63 | Includes the digital filter design of the ``lowpass.pole``, ``highpass.z``,
64 | ``highpass.pole`` and ``lowpass.z`` strategies.
65 |
66 | * `lowpass_highpass_matched_z.py `__
67 |
68 | Includes the analog filter design (Laplace) for a single pole lowpass IIR
69 | filter, and for a single zero and single pole highpass IIR filter. These
70 | are then converted to digital filters as a matched Z-Transform filter
71 | (pole-zero mapping/matching), which yields the equations used by the
72 | ``lowpass.pole_exp`` and ``highpass.z_exp`` filter strategies. These are
73 | then mirrored to get the ``lowpass.z_exp`` and ``highpass.pole_exp`` filter
74 | strategies.
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py{27,py,32,33,34,35,36,37,py3,38,39,310}
3 | skip_missing_interpreters = True
4 | toxworkdir = /tmp/tox_audiolazy
5 | minversion = 2.9.1
6 | requires =
7 | pip<8
8 | tox-pip-version
9 | tox-run-before
10 | tox-venv
11 | typing<3.7
12 | virtualenv<14
13 |
14 | [testenv]
15 | pip_version = 21.3.1
16 | ; Note: in order to compile the old numpy versions,
17 | ; an old glibc header, xlocale.h, is required.
18 | ; This command should enable that process by creating a symbolic link:
19 | ; sudo ln -s /usr/include/locale.h /usr/local/include/xlocale.h
20 | ; After compiling/installing numpy, the xlocale.h link can be removed:
21 | ; sudo unlink /usr/local/include/xlocale.h
22 | deps =
23 | ; Required by pytest
24 | py34: attrs<21.1
25 | py27: pyparsing<2.5
26 | ; Required by pytest-cov
27 | py32: coverage<4
28 | py{33,34}: coverage<5
29 | py{27,py,35}: coverage<6
30 | ; Testing tools
31 | py32: pytest<3
32 | py33: pytest<3.3
33 | py{27,py,34}: pytest<5
34 | py35: pytest<6.2
35 | py{36,37,py3,38,39,310}: pytest
36 | py{32,33}: pytest-cov<2.6
37 | py34: pytest-cov<2.9
38 | py{27,py,35}: pytest-cov<3
39 | py{36,37,py3,38,39,310}: pytest-cov
40 | py{32,33}: pytest-timeout<1.2.1
41 | py{27,py,34}: pytest-timeout<2
42 | py{35,36,37,py3,38,39,310}: pytest-timeout
43 | py32: sympy<1.1
44 | py33: sympy<1.2
45 | py34: sympy<1.5
46 | py{27,py}: sympy<1.6
47 | py35: sympy<1.7
48 | py{36,37,py3,38,39,310}: sympy
49 | py{32,33}: numpy<1.12
50 | py34: numpy<1.16
51 | py{27,py}: numpy<1.17
52 | py35: numpy<1.19
53 | py36: numpy<1.20
54 | py37: numpy<1.22
55 | py{py3,38,39,310}: numpy
56 | py33: scipy<0.17 # pip crashes while trying to install scipy<0.18
57 | py{27,34}: scipy<1.3
58 | py35: scipy<1.5
59 | py36: scipy<1.6
60 | py37: scipy<1.8
61 | py{py3,38,39,310}: scipy
62 | commands =
63 | python -m pytest {posargs}
64 |
65 | [testenv:py27]
66 | pip_version = 20.3.4
67 |
68 | [testenv:pypy]
69 | pip_version = 20.3.4
70 |
71 | [testenv:py32]
72 | basepython = python3.2
73 | pip_version = 7.1.2
74 |
75 | [testenv:py33]
76 | basepython = python3.3
77 | pip_version = 10.0.1
78 | ; Disable the tox-venv plugin for this Python version
79 | ; as it crashes when the directory exists (unless "--upgrade" is set).
80 | ; It will crash the first time it runs
81 | ; because the plugin was already loaded (and it isn't reloaded).
82 | run_before =
83 | find "{toxworkdir}"/.tox/ -path '*/tox_venv/hooks.py' | xargs sed -i 's/version >= (3, 3)/version >= (3, 4)/g'
84 |
85 | [testenv:py34]
86 | pip_version = 19.1.1
87 |
88 | [testenv:py35]
89 | pip_version = 20.3.4
90 |
91 | ; These are required because of the tox version running
92 | [testenv:py39]
93 | basepython = python3.9
94 |
95 | [testenv:py310]
96 | basepython = python3.10
97 |
98 | [pytest]
99 | addopts =
100 | --cov-config=tox.ini
101 | --cov=audiolazy
102 | --doctest-modules
103 | --ignore=examples
104 | --ignore=docs
105 | --ignore=math
106 | --ignore=setup.py
107 |
108 | [run]
109 | branch = True
110 | omit = audiolazy/tests/*
111 |
112 | [report]
113 | show_missing = True
114 | precision = 2
115 |
--------------------------------------------------------------------------------
/audiolazy/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # This file is part of AudioLazy, the signal processing Python package.
3 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
4 | #
5 | # AudioLazy is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, version 3 of the License.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 | """
17 | AudioLazy package
18 |
19 | This is the main package file, that already imports all modules into the
20 | system. As the full name might not be small enough for typing it everywhere,
21 | you can import with a helpful alias:
22 |
23 | >>> import audiolazy as lz
24 | >>> lz.Stream(1, 3, 2).take(8)
25 | [1, 3, 2, 1, 3, 2, 1, 3]
26 |
27 | But there's some parts of the code you probably will find it cleaner to import
28 | directly, like the ``z`` object:
29 |
30 | >>> from audiolazy import z, Stream
31 | >>> filt = 1 / (1 - z ** -1) # Accumulator linear filter
32 | >>> filt(Stream(1, 3, 2), zero=0).take(8)
33 | [1, 4, 6, 7, 10, 12, 13, 16]
34 |
35 | For a single use within a console or for trying some new experimental ideas
36 | (perhaps with IPython), you would perhaps find easier to import the full
37 | package contents:
38 |
39 | >>> from audiolazy import *
40 | >>> s, Hz = sHz(44100)
41 | >>> delay_a4 = freq2lag(440 * Hz)
42 | >>> filt = ParallelFilter(comb.tau(delay_a4, 20 * s),
43 | ... resonator(440 * Hz, bandwidth=100 * Hz)
44 | ... )
45 | >>> len(filt)
46 | 2
47 |
48 | There's documentation inside the package classes and functions docstrings.
49 | If you try ``dir(audiolazy)`` [or ``dir(lz)``] after importing it [with the
50 | suggested alias], you'll see all the package contents, and the names starting
51 | with ``lazy`` followed by an underscore are modules. If you're starting now,
52 | try to see the docstring from the Stream and ZFilter classes with the
53 | ``help(lz.Stream)`` and ``help(lz.ZFilter)`` commands, and then the help from
54 | the other functionalities used above. If you didn't know the ``dir`` and
55 | ``help`` built-ins before reading this, it's strongly suggested you to read
56 | first a Python documentation or tutorial, at least enough for you to
57 | understand the basic behaviour and syntax of ``for`` loops, iterators,
58 | iterables, lists, generators, list comprehensions and decorators.
59 |
60 | This package was created by Danilo J. S. Bellini and is a free software,
61 | under the terms of the GPLv3.
62 | """
63 |
64 | # Some dunders and summary docstrings initialization
65 | __modules__, __all__, __doc__ = \
66 | __import__(__name__ + "._internals", fromlist=[__name__]
67 | ).init_package(__path__, __name__, __doc__)
68 |
69 | # Import all modules contents to the main namespace
70 | exec(("from .{} import *\n" * len(__modules__)).format(*__modules__))
71 |
72 | # Metadata (used by setup.py); Should use only local assignments!
73 | __version__ = "0.6.1dev"
74 | __author__ = "Danilo de Jesus da Silva Bellini"
75 | __author_email__ = "danilo.bellini@gmail.com"
76 | __url__ = "http://github.com/danilobellini/audiolazy"
77 |
--------------------------------------------------------------------------------
/audiolazy/tests/test_analysis_numpy.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # This file is part of AudioLazy, the signal processing Python package.
3 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
4 | #
5 | # AudioLazy is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, version 3 of the License.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 | """
17 | Testing module for the lazy_analysis module by using Numpy
18 | """
19 |
20 | from __future__ import division
21 |
22 | import pytest
23 | p = pytest.mark.parametrize
24 |
25 | from numpy.fft import fft, ifft
26 |
27 | # Audiolazy internal imports
28 | from ..lazy_analysis import dft, stft
29 | from ..lazy_math import pi, cexp, phase
30 | from ..lazy_misc import almost_eq, rint
31 | from ..lazy_synth import line
32 | from ..lazy_stream import Stream
33 | from ..lazy_itertools import chain
34 |
35 |
36 | class TestDFT(object):
37 |
38 | blk_table = [
39 | [20],
40 | [1, 2, 3],
41 | [0, 1, 0, -1],
42 | [5] * 8,
43 | ]
44 |
45 | @p("blk", blk_table)
46 | @p("size_multiplier", [.5, 1, 2, 3, 1.5, 1.2])
47 | def test_empty(self, blk, size_multiplier):
48 | full_size = len(blk)
49 | size = rint(full_size * size_multiplier)
50 | np_data = fft(blk, size).tolist()
51 | lz_data = dft(blk[:size],
52 | line(size, 0, 2 * pi, finish=False),
53 | normalize=False
54 | )
55 | assert almost_eq.diff(np_data, lz_data, max_diff=1e-12)
56 |
57 |
58 | class TestSTFT(object):
59 |
60 | @p("strategy", [stft.real, stft.complex, stft.complex_real])
61 | def test_whitenize_with_decorator_size_4_without_fft_window(self, strategy):
62 | @strategy(size=4, hop=4)
63 | def whitenize(blk):
64 | return cexp(phase(blk) * 1j)
65 |
66 | sig = Stream(0, 3, 4, 0) # fft([0, 3, 4, 0]) is [7, -4.-3.j, 1, -4.+3.j]
67 | # fft([4, 0, 0, 3]) is [7, 4.+3.j, 1, 4.-3.j]
68 | data4003 = ifft([1, (4+3j)/5, 1, (4-3j)/5]) # From block [4, 0, 0, 3]
69 | data0340 = ifft([1, (-4-3j)/5, 1, (-4+3j)/5]) # From block [0, 3, 4, 0]
70 |
71 | result = whitenize(sig) # No overlap-add window (default behavior)
72 | assert isinstance(result, Stream)
73 |
74 | expected = Stream(*data0340)
75 | assert almost_eq(result.take(64), expected.take(64))
76 |
77 | # Using a "triangle" as the overlap-add window
78 | wnd = [0.5, 1, 1, 0.5] # Normalized triangle
79 | new_result = whitenize(sig, ola_wnd=[1, 2, 2, 1])
80 | assert isinstance(result, Stream)
81 | new_expected = Stream(*data0340) * Stream(*wnd)
82 | assert almost_eq(new_result.take(64), new_expected.take(64))
83 |
84 | # With real overlap
85 | wnd_hop2 = [1/3, 2/3, 2/3, 1/3] # Normalized triangle for the new hop
86 | overlap_result = whitenize(sig, hop=2, ola_wnd=[1, 2, 2, 1])
87 | assert isinstance(result, Stream)
88 | overlap_expected = Stream(*data0340) * Stream(*wnd_hop2) \
89 | + chain([0, 0], Stream(*data4003) * Stream(*wnd_hop2))
90 | assert almost_eq(overlap_result.take(64), overlap_expected.take(64))
91 |
--------------------------------------------------------------------------------
/audiolazy/tests/test_poly_extdep.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # This file is part of AudioLazy, the signal processing Python package.
3 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
4 | #
5 | # AudioLazy is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, version 3 of the License.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 | """
17 | Testing module for the lazy_poly module by using Sympy
18 | """
19 |
20 | from __future__ import division
21 |
22 | import pytest
23 | p = pytest.mark.parametrize
24 |
25 | import sympy
26 |
27 | # Audiolazy internal imports
28 | from ..lazy_poly import Poly, x
29 | from ..lazy_compat import builtins, PYTHON2
30 |
31 |
32 | class TestPolySympy(object):
33 |
34 | def test_call_horner_simple_polinomial(self):
35 | poly = x ** 2 + 2 * x + 18 - x ** 7
36 | a = sympy.Symbol("a")
37 | expected_horner = 18 + (2 + (1 + -a ** 5) * a) * a
38 | expected_direct = 18 + 2 * a + a ** 2 - a ** 7
39 |
40 | # "Testing the test"
41 | assert expected_horner.expand() == expected_direct
42 | assert expected_horner != expected_direct
43 |
44 | # Applying the value
45 | assert poly(a, horner=True) == expected_horner == poly(a)
46 | assert poly(a, horner=False) == expected_direct
47 |
48 | def test_call_horner_laurent_polinomial(self):
49 | poly = x ** 2 + 2 * x ** -3 + 9 - 5 * x ** 7
50 | a = sympy.Symbol("a")
51 | expected_horner = (2 + (9 + (1 - 5 * a ** 5) * a ** 2) * a ** 3) * a ** -3
52 | expected_direct = 2 * a ** -3 + 9 + a ** 2 - 5 * a ** 7
53 |
54 | # "Testing the test"
55 | assert expected_horner.expand() == expected_direct
56 | assert expected_horner != expected_direct
57 |
58 | # Applying the value
59 | assert poly(a, horner=True) == expected_horner
60 | assert poly(a, horner=False) == expected_direct == poly(a)
61 |
62 | def test_call_horner_sum_of_symbolic_powers(self):
63 |
64 | def sorted_mock(iterable, reverse=False):
65 | """ Used internally by terms to sort the powers """
66 | data = list(iterable)
67 | if data and isinstance(data[0], sympy.Basic):
68 | return builtins.sorted(data, reverse=reverse, key=str)
69 | return builtins.sorted(data, reverse=reverse)
70 |
71 | # Mocks the sorted, but just internally to the Poly.terms
72 | if PYTHON2:
73 | terms_globals = Poly.terms.im_func.func_globals
74 | else:
75 | terms_globals = Poly.terms.__globals__
76 | terms_globals["sorted"] = sorted_mock
77 |
78 | try:
79 | a, b, c, d, k = sympy.symbols("a b c d k")
80 | poly = d * x ** c - d * x ** a + x ** b
81 | expected_horner = (-d + (1 + d * k ** (c - b)) * k ** (b - a)) * k ** a
82 | expected_direct = d * k ** c - d * k ** a + k ** b
83 |
84 | # "Testing the test"
85 | assert expected_horner.expand() == expected_direct
86 | assert expected_horner != expected_direct
87 |
88 | # Applying the value
89 | assert poly(k, horner=True) == expected_horner
90 | assert poly(k, horner=False) == expected_direct == poly(k)
91 |
92 | finally:
93 | del terms_globals["sorted"] # Clean the mock
94 |
--------------------------------------------------------------------------------
/examples/keyboard.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Musical keyboard synth example with a QWERTY keyboard
19 | """
20 |
21 | from audiolazy import (str2midi, midi2freq, saw_table, sHz, Streamix, Stream,
22 | line, AudioIO, chunks)
23 | import sys
24 | try:
25 | import tkinter
26 | except ImportError:
27 | import Tkinter as tkinter
28 |
29 | keys = "awsedftgyhujkolp;" # Chromatic scale
30 | first_note = str2midi("C3")
31 |
32 | pairs = list(enumerate(keys.upper(), first_note + 12)) + \
33 | list(enumerate(keys, first_note))
34 | notes = {k: midi2freq(idx) for idx, k in pairs}
35 | synth = saw_table
36 |
37 | txt = """
38 | Press keys
39 |
40 | W E T Y U O P
41 | A S D F G H J K L ;
42 |
43 | The above should be
44 | seen as piano keys.
45 |
46 | Using lower/upper
47 | letters changes the
48 | octave.
49 | """
50 |
51 | tk = tkinter.Tk()
52 | tk.title("Keyboard Example")
53 | lbl = tkinter.Label(tk, text=txt, font=("Mono", 30))
54 | lbl.pack(expand=True, fill=tkinter.BOTH)
55 |
56 | rate = 44100
57 | s, Hz = sHz(rate)
58 | ms = 1e-3 * s
59 | attack = 30 * ms
60 | release = 50 * ms
61 | level = .2 # Highest amplitude value per note
62 |
63 | smix = Streamix(True)
64 | cstreams = {}
65 |
66 | class ChangeableStream(Stream):
67 | """
68 | Stream that can be changed after being used if the limit/append methods are
69 | called while playing. It uses an iterator that keep taking samples from the
70 | Stream instead of an iterator to the internal data itself.
71 | """
72 | def __iter__(self):
73 | while True:
74 | yield next(self._data)
75 |
76 | has_after = None
77 |
78 | def on_key_down(evt):
79 | # Ignores key up if it came together with a key down (debounce)
80 | global has_after
81 | if has_after:
82 | tk.after_cancel(has_after)
83 | has_after = None
84 |
85 | ch = evt.char
86 | if not ch in cstreams and ch in notes:
87 | # Prepares the synth
88 | freq = notes[ch]
89 | cs = ChangeableStream(level)
90 | env = line(attack, 0, level).append(cs)
91 | snd = env * synth(freq * Hz)
92 |
93 | # Mix it, storing the ChangeableStream to be changed afterwards
94 | cstreams[ch] = cs
95 | smix.add(0, snd)
96 |
97 | def on_key_up(evt):
98 | global has_after
99 | has_after = tk.after_idle(on_key_up_process, evt)
100 |
101 | def on_key_up_process(evt):
102 | ch = evt.char
103 | if ch in cstreams:
104 | cstreams[ch].limit(0).append(line(release, level, 0))
105 | del cstreams[ch]
106 |
107 | tk.bind("", on_key_down)
108 | tk.bind("", on_key_up)
109 |
110 | api = sys.argv[1] if sys.argv[1:] else None # Choose API via command-line
111 | chunks.size = 1 if api == "jack" else 16
112 |
113 | with AudioIO(api=api) as player:
114 | player.play(smix, rate=rate)
115 | tk.mainloop()
116 |
--------------------------------------------------------------------------------
/examples/zcross_pitch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Pitch follower via zero-crossing rate with Tkinter GUI
19 | """
20 |
21 | # ------------------------
22 | # AudioLazy pitch follower
23 | # ------------------------
24 | import sys
25 | from audiolazy import (tostream, zcross, lag2freq, AudioIO, freq2str, sHz,
26 | lowpass, envelope, pi, maverage, Stream, thub, chunks)
27 |
28 | def limiter(sig, threshold=.1, size=256, env=envelope.rms, cutoff=pi/2048):
29 | sig = thub(sig, 2)
30 | return sig * Stream( 1. if el <= threshold else threshold / el
31 | for el in maverage(size)(env(sig, cutoff=cutoff)) )
32 |
33 | @tostream
34 | def zcross_pitch(sig, size=2048, hop=None):
35 | for blk in zcross(sig, hysteresis=.01).blocks(size=size, hop=hop):
36 | crossings = sum(blk)
37 | yield 0. if crossings == 0 else lag2freq(2. * size / crossings)
38 |
39 |
40 | def pitch_from_mic(upd_time_in_ms):
41 | rate = 44100
42 | s, Hz = sHz(rate)
43 |
44 | api = sys.argv[1] if sys.argv[1:] else None # Choose API via command-line
45 | chunks.size = 1 if api == "jack" else 16
46 |
47 | with AudioIO(api=api) as recorder:
48 | snd = recorder.record(rate=rate)
49 | sndlow = lowpass(400 * Hz)(limiter(snd, cutoff=20 * Hz))
50 | hop = int(upd_time_in_ms * 1e-3 * s)
51 | for pitch in freq2str(zcross_pitch(sndlow, size=2*hop, hop=hop) / Hz):
52 | yield pitch
53 |
54 |
55 | # ----------------
56 | # GUI with tkinter
57 | # ----------------
58 | if __name__ == "__main__":
59 | try:
60 | import tkinter
61 | except ImportError:
62 | import Tkinter as tkinter
63 | import threading
64 | import re
65 |
66 | # Window (Tk init), text label and button
67 | tk = tkinter.Tk()
68 | tk.title(__doc__.strip().splitlines()[0])
69 | lbldata = tkinter.StringVar(tk)
70 | lbltext = tkinter.Label(tk, textvariable=lbldata, font=("Purisa", 72),
71 | width=10)
72 | lbltext.pack(expand=True, fill=tkinter.BOTH)
73 | btnclose = tkinter.Button(tk, text="Close", command=tk.destroy,
74 | default="active")
75 | btnclose.pack(fill=tkinter.X)
76 |
77 | # Needed data
78 | regex_note = re.compile(r"^([A-Gb#]*-?[0-9]*)([?+-]?)(.*?%?)$")
79 | upd_time_in_ms = 200
80 |
81 | # Update functions for each thread
82 | def upd_value(): # Recording thread
83 | pitches = iter(pitch_from_mic(upd_time_in_ms))
84 | while not tk.should_finish:
85 | tk.value = next(pitches)
86 |
87 | def upd_timer(): # GUI mainloop thread
88 | lbldata.set("\n".join(regex_note.findall(tk.value)[0]))
89 | tk.after(upd_time_in_ms, upd_timer)
90 |
91 | # Multi-thread management initialization
92 | tk.should_finish = False
93 | tk.value = freq2str(0) # Starting value
94 | lbldata.set(tk.value)
95 | tk.upd_thread = threading.Thread(target=upd_value)
96 |
97 | # Go
98 | tk.upd_thread.start()
99 | tk.after_idle(upd_timer)
100 | tk.mainloop()
101 | tk.should_finish = True
102 | tk.upd_thread.join()
103 |
--------------------------------------------------------------------------------
/examples/dft_pitch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Pitch follower via DFT peak with Tkinter GUI
19 | """
20 |
21 | # ------------------------
22 | # AudioLazy pitch follower
23 | # ------------------------
24 | import sys
25 | from audiolazy import (tostream, AudioIO, freq2str, sHz, chunks,
26 | lowpass, envelope, pi, thub, Stream, maverage)
27 | from numpy.fft import rfft
28 |
29 | def limiter(sig, threshold=.1, size=256, env=envelope.rms, cutoff=pi/2048):
30 | sig = thub(sig, 2)
31 | return sig * Stream( 1. if el <= threshold else threshold / el
32 | for el in maverage(size)(env(sig, cutoff=cutoff)) )
33 |
34 |
35 | @tostream
36 | def dft_pitch(sig, size=2048, hop=None):
37 | for blk in Stream(sig).blocks(size=size, hop=hop):
38 | dft_data = rfft(blk)
39 | idx, vmax = max(enumerate(dft_data),
40 | key=lambda el: abs(el[1]) / (2 * el[0] / size + 1)
41 | )
42 | yield 2 * pi * idx / size
43 |
44 |
45 | def pitch_from_mic(upd_time_in_ms):
46 | rate = 44100
47 | s, Hz = sHz(rate)
48 |
49 | api = sys.argv[1] if sys.argv[1:] else None # Choose API via command-line
50 | chunks.size = 1 if api == "jack" else 16
51 |
52 | with AudioIO(api=api) as recorder:
53 | snd = recorder.record(rate=rate)
54 | sndlow = lowpass(400 * Hz)(limiter(snd, cutoff=20 * Hz))
55 | hop = int(upd_time_in_ms * 1e-3 * s)
56 | for pitch in freq2str(dft_pitch(sndlow, size=2*hop, hop=hop) / Hz):
57 | yield pitch
58 |
59 |
60 | # ----------------
61 | # GUI with tkinter
62 | # ----------------
63 | if __name__ == "__main__":
64 | try:
65 | import tkinter
66 | except ImportError:
67 | import Tkinter as tkinter
68 | import threading
69 | import re
70 |
71 | # Window (Tk init), text label and button
72 | tk = tkinter.Tk()
73 | tk.title(__doc__.strip().splitlines()[0])
74 | lbldata = tkinter.StringVar(tk)
75 | lbltext = tkinter.Label(tk, textvariable=lbldata, font=("Purisa", 72),
76 | width=10)
77 | lbltext.pack(expand=True, fill=tkinter.BOTH)
78 | btnclose = tkinter.Button(tk, text="Close", command=tk.destroy,
79 | default="active")
80 | btnclose.pack(fill=tkinter.X)
81 |
82 | # Needed data
83 | regex_note = re.compile(r"^([A-Gb#]*-?[0-9]*)([?+-]?)(.*?%?)$")
84 | upd_time_in_ms = 200
85 |
86 | # Update functions for each thread
87 | def upd_value(): # Recording thread
88 | pitches = iter(pitch_from_mic(upd_time_in_ms))
89 | while not tk.should_finish:
90 | tk.value = next(pitches)
91 |
92 | def upd_timer(): # GUI mainloop thread
93 | lbldata.set("\n".join(regex_note.findall(tk.value)[0]))
94 | tk.after(upd_time_in_ms, upd_timer)
95 |
96 | # Multi-thread management initialization
97 | tk.should_finish = False
98 | tk.value = freq2str(0) # Starting value
99 | lbldata.set(tk.value)
100 | tk.upd_thread = threading.Thread(target=upd_value)
101 |
102 | # Go
103 | tk.upd_thread.start()
104 | tk.after_idle(upd_timer)
105 | tk.mainloop()
106 | tk.should_finish = True
107 | tk.upd_thread.join()
108 |
--------------------------------------------------------------------------------
/audiolazy/lazy_itertools.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # This file is part of AudioLazy, the signal processing Python package.
3 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
4 | #
5 | # AudioLazy is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, version 3 of the License.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 | """
17 | Itertools module "decorated" replica, where all outputs are Stream instances
18 | """
19 |
20 | import itertools as it
21 | try:
22 | from collections.abc import Iterator
23 | except ImportError:
24 | from collections import Iterator
25 |
26 | # Audiolazy internal imports
27 | from .lazy_stream import tostream, Stream
28 | from .lazy_compat import xrange, xzip, PYTHON2
29 | from .lazy_core import StrategyDict
30 | from .lazy_filters import z
31 |
32 |
33 | # "Decorates" all functions from itertools
34 | __all__ = ["chain", "izip", "tee", "accumulate"]
35 | it_names = set(dir(it)).difference(__all__)
36 | for func in filter(callable, [getattr(it, name) for name in it_names]):
37 | name = func.__name__
38 | if name in ["filterfalse", "zip_longest"]: # These were renamed in Python 3
39 | name = "i" + name # In AudioLazy, keep the Python 2 names
40 | __all__.append(name)
41 | locals()[name] = tostream(func, module_name=__name__)
42 |
43 |
44 | # StrategyDict chain, following "from_iterable" from original itertool
45 | chain = StrategyDict("chain")
46 | chain.strategy("chain")(tostream(it.chain, module_name=__name__))
47 | chain.strategy("star", "from_iterable")(tostream(it.chain.from_iterable,
48 | module_name=__name__))
49 |
50 |
51 | # StrategyDict izip, allowing izip.longest instead of izip_longest
52 | izip = StrategyDict("izip")
53 | izip.strategy("izip", "smallest")(tostream(xzip, module_name=__name__))
54 | izip["longest"] = izip_longest
55 |
56 |
57 | # Includes the imap and ifilter (they're not from itertools in Python 3)
58 | for name, func in zip(["imap", "ifilter"], [map, filter]):
59 | if name not in __all__:
60 | __all__.append(name)
61 | locals()[name] = tostream(func, module_name=__name__)
62 |
63 |
64 | accumulate = StrategyDict("accumulate")
65 | if not PYTHON2:
66 | accumulate.strategy("accumulate", "itertools") \
67 | (tostream(it.accumulate, module_name=__name__))
68 |
69 |
70 | @accumulate.strategy("func", "pure_python")
71 | @tostream
72 | def accumulate(iterable):
73 | " Return series of accumulated sums. "
74 | iterator = iter(iterable)
75 | sum_data = next(iterator)
76 | yield sum_data
77 | for el in iterator:
78 | sum_data += el
79 | yield sum_data
80 |
81 |
82 | accumulate.strategy("z")(1 / (1 - z ** -1))
83 |
84 |
85 | def tee(data, n=2):
86 | """
87 | Tee or "T" copy to help working with Stream instances as well as with
88 | numbers.
89 |
90 | Parameters
91 | ----------
92 | data :
93 | Input to be copied. Can be anything.
94 | n :
95 | Size of returned tuple. Defaults to 2.
96 |
97 | Returns
98 | -------
99 | Tuple of n independent Stream instances, if the input is a Stream or an
100 | iterator, otherwise a tuple with n times the same object.
101 |
102 | See Also
103 | --------
104 | thub :
105 | use Stream instances *almost* like constants in your equations.
106 |
107 | """
108 | if isinstance(data, (Stream, Iterator)):
109 | return tuple(Stream(cp) for cp in it.tee(data, n))
110 | else:
111 | return tuple(data for unused in xrange(n))
112 |
--------------------------------------------------------------------------------
/audiolazy/tests/test_text.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # This file is part of AudioLazy, the signal processing Python package.
3 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
4 | #
5 | # AudioLazy is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, version 3 of the License.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 | """
17 | Testing module for the lazy_text module
18 | """
19 |
20 | from __future__ import unicode_literals
21 |
22 | import pytest
23 | p = pytest.mark.parametrize
24 |
25 | # Audiolazy internal imports
26 | from ..lazy_text import rst_table, format_docstring
27 |
28 |
29 | class TestRSTTable(object):
30 |
31 | simple_input = [
32 | [1, 2, 3, "hybrid"],
33 | [3, "mixed", .5, 123123]
34 | ]
35 |
36 | def test_simple_input_table(self):
37 | assert rst_table(
38 | self.simple_input,
39 | "this is_ a test".split()
40 | ) == [
41 | "==== ===== === ======",
42 | "this is_ a test ",
43 | "==== ===== === ======",
44 | "1 2 3 hybrid",
45 | "3 mixed 0.5 123123",
46 | "==== ===== === ======",
47 | ]
48 |
49 |
50 | class TestFormatDocstring(object):
51 |
52 | def test_template_without_parameters(self):
53 | docstring = "This is a docstring"
54 | @format_docstring(docstring)
55 | def not_so_useful():
56 | return
57 | assert not_so_useful.__doc__ == docstring
58 |
59 | def test_template_positional_parameter_automatic_counting(self):
60 | docstring = "Function {} docstring {}"
61 | @format_docstring(docstring, "Unused", "is weird!")
62 | def unused_func():
63 | return
64 | assert unused_func.__doc__ == "Function Unused docstring is weird!"
65 |
66 | def test_template_positional_parameter_numbered(self):
67 | docstring = "Let {3}e {1} {0} wi{3} {2}!"
68 | @format_docstring(docstring, "be", "force", "us", "th")
69 | def another_unused_func():
70 | return
71 | assert another_unused_func.__doc__ == "Let the force be with us!"
72 |
73 | def test_template_keyword_parameters(self):
74 | docstring = "{name} is a function for {explanation}"
75 | @format_docstring(docstring, name="Unk", explanation="uncles!")
76 | def unused():
77 | return
78 | assert unused.__doc__ == "Unk is a function for uncles!"
79 |
80 | def test_template_mixed_keywords_and_positional_params(self):
81 | docstring = "The {name} has to do with {0} and {1}"
82 | @format_docstring(docstring, "Freud", "psychoanalysis", name="ego")
83 | def alter():
84 | return
85 | assert alter.__doc__ == "The ego has to do with Freud and psychoanalysis"
86 |
87 | def test_with_docstring_in_function(self):
88 | dok = "This is the {a_name} docstring:{__doc__}with a {0} and a {1}."
89 | @format_docstring(dok, "prefix", "suffix", a_name="doc'ed")
90 | def dokked():
91 | """
92 | A docstring
93 | with two lines (but this indeed have 4 lines)
94 | """
95 | assert dokked.__doc__ == "\n".join([
96 | "This is the doc'ed docstring:",
97 | " A docstring",
98 | " with two lines (but this indeed have 4 lines)",
99 | " with a prefix and a suffix.",
100 | ])
101 |
102 | def test_fill_docstring_in_function(self):
103 | @format_docstring(descr="dangerous")
104 | def danger():
105 | """ A {descr} doc! """
106 | assert danger.__doc__ == " A dangerous doc! "
107 |
--------------------------------------------------------------------------------
/audiolazy/lazy_math.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # This file is part of AudioLazy, the signal processing Python package.
3 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
4 | #
5 | # AudioLazy is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, version 3 of the License.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 | """
17 | Math modules "decorated" and complemented to work elementwise when needed
18 | """
19 |
20 | import math
21 | import cmath
22 | import operator
23 | import itertools as it
24 | from functools import reduce
25 |
26 | # Audiolazy internal imports
27 | from .lazy_misc import elementwise
28 | from .lazy_compat import INT_TYPES
29 |
30 | __all__ = ["absolute", "pi", "e", "cexp", "ln", "log", "log1p", "log10",
31 | "log2", "factorial", "dB10", "dB20", "inf", "nan", "phase", "sign"]
32 |
33 | # All functions from math with one numeric input
34 | _math_names = ["acos", "acosh", "asin", "asinh", "atan", "atanh", "ceil",
35 | "cos", "cosh", "degrees", "erf", "erfc", "exp", "expm1",
36 | "fabs", "floor", "frexp", "gamma", "isinf", "isnan", "lgamma",
37 | "modf", "radians", "sin", "sinh", "sqrt", "tan", "tanh",
38 | "trunc"]
39 | __all__.extend(_math_names)
40 |
41 |
42 | for func in [getattr(math, name) for name in _math_names]:
43 | locals()[func.__name__] = elementwise("x", 0)(func)
44 |
45 |
46 | @elementwise("x", 0)
47 | def log(x, base=None):
48 | if base is None:
49 | if x == 0:
50 | return -inf
51 | elif isinstance(x, complex) or x < 0:
52 | return cmath.log(x)
53 | else:
54 | return math.log(x)
55 | else: # base is given
56 | if base <= 0 or base == 1:
57 | raise ValueError("Not a valid logarithm base")
58 | elif x == 0:
59 | return -inf
60 | elif isinstance(x, complex) or x < 0:
61 | return cmath.log(x, base)
62 | else:
63 | return math.log(x, base)
64 |
65 |
66 | @elementwise("x", 0)
67 | def log1p(x):
68 | if x == -1:
69 | return -inf
70 | elif isinstance(x, complex) or x < -1:
71 | return cmath.log(1 + x)
72 | else:
73 | return math.log1p(x)
74 |
75 |
76 | def log10(x):
77 | return log(x, 10)
78 |
79 |
80 | def log2(x):
81 | return log(x, 2)
82 |
83 |
84 | ln = log
85 | absolute = elementwise("number", 0)(abs)
86 | pi = math.pi
87 | e = math.e
88 | cexp = elementwise("x", 0)(cmath.exp)
89 | inf = float("inf")
90 | nan = float("nan")
91 | phase = elementwise("z", 0)(cmath.phase)
92 |
93 |
94 | @elementwise("n", 0)
95 | def factorial(n):
96 | """
97 | Factorial function that works with really big numbers.
98 | """
99 | if isinstance(n, float):
100 | if n.is_integer():
101 | n = int(n)
102 | if not isinstance(n, INT_TYPES):
103 | raise TypeError("Non-integer input (perhaps you need Euler Gamma "
104 | "function or Gauss Pi function)")
105 | if n < 0:
106 | raise ValueError("Input shouldn't be negative")
107 | return reduce(operator.mul,
108 | it.takewhile(lambda m: m <= n, it.count(2)),
109 | 1)
110 |
111 |
112 | @elementwise("data", 0)
113 | def dB10(data):
114 | """
115 | Convert a gain value to dB, from a squared amplitude value to a power gain.
116 | """
117 | return 10 * math.log10(abs(data)) if data != 0 else -inf
118 |
119 |
120 | @elementwise("data", 0)
121 | def dB20(data):
122 | """
123 | Convert a gain value to dB, from an amplitude value to a power gain.
124 | """
125 | return 20 * math.log10(abs(data)) if data != 0 else -inf
126 |
127 |
128 | @elementwise("x", 0)
129 | def sign(x):
130 | """
131 | Signal of ``x``: 1 if positive, -1 if negative, 0 otherwise.
132 | """
133 | return +(x > 0) or -(x < 0)
134 |
--------------------------------------------------------------------------------
/examples/play_bach_choral.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Random Bach Choral playing example (needs Music21 corpus)
19 |
20 | This example uses a personalized synth based on the Karplus-Strong model.
21 | You can also get the synth models and effects from the ode_to_joy.py example
22 | and adapt them to get used here.
23 | """
24 |
25 | from __future__ import unicode_literals, print_function
26 | from music21 import corpus
27 | from music21.expressions import Fermata
28 | import audiolazy as lz
29 | import random, operator, sys, time
30 | from functools import reduce
31 |
32 |
33 | def ks_synth(freq):
34 | """
35 | Synthesize the given frequency into a Stream by using a model based on
36 | Karplus-Strong.
37 | """
38 | ks_mem = (sum(lz.sinusoid(x * freq) for x in [1, 3, 9]) +
39 | lz.white_noise() + lz.Stream(-1, 1)) / 5
40 | return lz.karplus_strong(freq, memory=ks_mem)
41 |
42 |
43 | def get_random_choral(log=True):
44 | """ Gets a choral from the J. S. Bach chorals corpus (in Music21). """
45 | choral_file = corpus.getBachChorales()[random.randint(0, 399)]
46 | choral = corpus.parse(choral_file)
47 | if log:
48 | print("Chosen choral:", choral.metadata.title)
49 | return choral
50 |
51 |
52 | def m21_to_stream(score, synth=ks_synth, beat=90, fdur=2., pad_dur=.5,
53 | rate=lz.DEFAULT_SAMPLE_RATE):
54 | """
55 | Converts Music21 data to a Stream object.
56 |
57 | Parameters
58 | ----------
59 | score :
60 | A Music21 data, usually a music21.stream.Score instance.
61 | synth :
62 | A function that receives a frequency as input and should yield a Stream
63 | instance with the note being played.
64 | beat :
65 | The BPM (beats per minute) value to be used in playing.
66 | fdur :
67 | Relative duration of a fermata. For example, 1.0 ignores the fermata, and
68 | 2.0 (default) doubles its duration.
69 | pad_dur :
70 | Duration in seconds, but not multiplied by ``s``, to be used as a
71 | zero-padding ending event (avoids clicks at the end when playing).
72 | rate :
73 | The sample rate, given in samples per second.
74 |
75 | """
76 | # Configuration
77 | s, Hz = lz.sHz(rate)
78 | step = 60. / beat * s
79 |
80 | # Creates a score from the music21 data
81 | score = reduce(operator.concat,
82 | [[(pitch.frequency * Hz, # Note
83 | note.offset * step, # Starting time
84 | note.quarterLength * step, # Duration
85 | Fermata in note.expressions) for pitch in note.pitches]
86 | for note in score.flat.notes]
87 | )
88 |
89 | # Mix all notes into song
90 | song = lz.Streamix()
91 | last_start = 0
92 | for freq, start, dur, has_fermata in score:
93 | delta = start - last_start
94 | if has_fermata:
95 | delta *= 2
96 | song.add(delta, synth(freq).limit(dur))
97 | last_start = start
98 |
99 | # Zero-padding and finishing
100 | song.add(dur + pad_dur * s, lz.Stream([]))
101 | return song
102 |
103 |
104 | # Play the song!
105 | if __name__ == "__main__":
106 | api = next(arg for arg in sys.argv[1:] + [None] if arg != "loop")
107 | lz.chunks.size = 1 if api == "jack" else 16
108 | rate = 44100
109 | while True:
110 | with lz.AudioIO(True, api=api) as player:
111 | player.play(m21_to_stream(get_random_choral(), rate=rate), rate=rate)
112 | if not "loop" in sys.argv[1:]:
113 | break
114 | time.sleep(3)
115 |
--------------------------------------------------------------------------------
/examples/butterworth_with_noise.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Two butterworth filters with Scipy applied to white noise
19 |
20 | This example is based on the experiment number 34 from
21 |
22 | Demonstrations to accompany Bregman’s Auditory Scene Analysis
23 |
24 | by Albert S. Bregman and Pierre A. Ahad.
25 |
26 | This experiment shows the audio is perceived as pitched differently on the
27 | 100ms glimpses when the context changes, although they are physically
28 | identical, i.e., the glimpses are indeed perceptually segregated from the
29 | background noise.
30 |
31 | Noise ranges are from [0; 2kHz] and [2kHz; 4kHz] and durations are 100ms and
32 | 400ms instead of the values declared in the original text. IIR filters are
33 | being used instead of FIR ones to get the noise, and the fact that originally
34 | there's no silence between the higher and lower pitch contexts, but the core
35 | idea of the experiment remains the same.
36 | """
37 |
38 | from audiolazy import (sHz, dB10, ZFilter, pi, ControlStream, white_noise,
39 | chunks, AudioIO, xrange, z)
40 | from scipy.signal import butter, buttord
41 | import numpy as np
42 | from time import sleep
43 | import sys
44 |
45 | rate = 44100
46 | s, Hz = sHz(rate)
47 | kHz = 1e3 * Hz
48 | tol = 100 * Hz
49 | freq = 2 * kHz
50 |
51 | wp = freq - tol # Bandpass frequency in rad/sample (from zero)
52 | ws = freq + tol # Bandstop frequency in rad/sample (up to Nyquist frequency)
53 | order, new_wp_divpi = buttord(wp/pi, ws/pi, gpass=dB10(.6), gstop=dB10(.4))
54 | ssfilt = butter(order, new_wp_divpi, btype="lowpass")
55 | filt_low = ZFilter(ssfilt[0].tolist(), ssfilt[1].tolist())
56 |
57 | ## That can be done without scipy using the equation directly:
58 | #filt_low = ((2.90e-4 + 1.16e-3 * z ** -1 + 1.74e-3 * z ** -2
59 | # + 1.16e-3 * z ** -3 + 2.90e-4 * z ** -4) /
60 | # (1 - 3.26 * z ** -1 + 4.04 * z ** -2
61 | # - 2.25 * z ** -3 + .474 * z ** -4))
62 |
63 | wp = np.array([freq + tol, 2 * freq - tol]) # Bandpass range in rad/sample
64 | ws = np.array([freq - tol, 2 * freq + tol]) # Bandstop range in rad/sample
65 | order, new_wp_divpi = buttord(wp/pi, ws/pi, gpass=dB10(.6), gstop=dB10(.4))
66 | ssfilt = butter(order, new_wp_divpi, btype="bandpass")
67 | filt_high = ZFilter(ssfilt[0].tolist(), ssfilt[1].tolist())
68 |
69 | ## Likewise, using the equation directly this one would be:
70 | #filt_high = ((2.13e-3 * (1 - z ** -6) - 6.39e-3 * (z ** -2 - z ** -4)) /
71 | # (1 - 4.99173 * z ** -1 + 10.7810 * z ** -2 - 12.8597 * z ** -3
72 | # + 8.93092 * z ** -4 - 3.42634 * z ** -5 + .569237 * z ** -6))
73 |
74 | gain_low = ControlStream(0)
75 | gain_high = ControlStream(0)
76 |
77 | low = filt_low(white_noise())
78 | high = filt_high(white_noise())
79 | low /= 2 * max(low.take(2000))
80 | high /= 2 * max(high.take(2000))
81 |
82 | api = sys.argv[1] if sys.argv[1:] else None # Choose API via command-line
83 | chunks.size = 1 if api == "jack" else 16
84 | with AudioIO(api=api) as player:
85 | player.play(low * gain_low + high * gain_high)
86 | gain_low.value = 1
87 | while True:
88 | gain_high.value = 0
89 | sleep(1)
90 | for unused in xrange(5): # Keeps low playing
91 | sleep(.1)
92 | gain_high.value = 0
93 | sleep(.4)
94 | gain_high.value = 1
95 |
96 | gain_low.value = 0
97 | sleep(1)
98 | for unused in xrange(5): # Keeps high playing
99 | sleep(.1)
100 | gain_low.value = 0
101 | sleep(.4)
102 | gain_low.value = 1
103 |
--------------------------------------------------------------------------------
/examples/animated_plot.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Matplotlib animated plot with mic input data.
19 |
20 | Call with the API name like ...
21 | ./animated_plot.py jack
22 | ... or without nothing for the default PortAudio API.
23 | """
24 | from __future__ import division
25 | from audiolazy import sHz, chunks, AudioIO, line, pi, window
26 | from matplotlib import pyplot as plt
27 | from matplotlib.animation import FuncAnimation
28 | from numpy.fft import rfft
29 | import numpy as np
30 | import collections, sys, threading
31 |
32 | # AudioLazy init
33 | rate = 44100
34 | s, Hz = sHz(rate)
35 | ms = 1e-3 * s
36 |
37 | length = 2 ** 12
38 | data = collections.deque([0.] * length, maxlen=length)
39 | wnd = np.array(window.hamming(length)) # For FFT
40 |
41 | api = sys.argv[1] if sys.argv[1:] else None # Choose API via command-line
42 | chunks.size = 1 if api == "jack" else 16
43 |
44 | # Creates a data updater callback
45 | def update_data():
46 | with AudioIO(api=api) as rec:
47 | for el in rec.record(rate=rate):
48 | data.append(el)
49 | if update_data.finish:
50 | break
51 |
52 | # Creates the data updater thread
53 | update_data.finish = False
54 | th = threading.Thread(target=update_data)
55 | th.start() # Already start updating data
56 |
57 | # Plot setup
58 | fig = plt.figure("AudioLazy in a Matplotlib animation", facecolor='#cccccc')
59 |
60 | time_values = np.array(list(line(length, -length / ms, 0)))
61 | time_ax = plt.subplot(2, 1, 1,
62 | xlim=(time_values[0], time_values[-1]),
63 | ylim=(-1., 1.),
64 | axisbg="black")
65 | time_ax.set_xlabel("Time (ms)")
66 | time_plot_line = time_ax.plot([], [], linewidth=2, color="#00aaff")[0]
67 |
68 | dft_max_min, dft_max_max = .01, 1.
69 | freq_values = np.array(line(length, 0, 2 * pi / Hz).take(length // 2 + 1))
70 | freq_ax = plt.subplot(2, 1, 2,
71 | xlim=(freq_values[0], freq_values[-1]),
72 | ylim=(0., .5 * (dft_max_max + dft_max_min)),
73 | axisbg="black")
74 | freq_ax.set_xlabel("Frequency (Hz)")
75 | freq_plot_line = freq_ax.plot([], [], linewidth=2, color="#00aaff")[0]
76 |
77 | # Functions to setup and update plot
78 | def init(): # Called twice on init, also called on each resize
79 | time_plot_line.set_data([], []) # Clear
80 | freq_plot_line.set_data([], [])
81 | fig.tight_layout()
82 | return [] if init.rempty else [time_plot_line, freq_plot_line]
83 |
84 | init.rempty = False # At first, init() should show what belongs to the plot
85 |
86 | def animate(idx):
87 | array_data = np.array(data)
88 | spectrum = np.abs(rfft(array_data * wnd)) / length
89 |
90 | time_plot_line.set_data(time_values, array_data)
91 | freq_plot_line.set_data(freq_values, spectrum)
92 |
93 | # Update y range if needed
94 | smax = spectrum.max()
95 | top = freq_ax.get_ylim()[1]
96 | if top < dft_max_max and abs(smax/top) > 1:
97 | freq_ax.set_ylim(top=top * 2)
98 | elif top > dft_max_min and abs(smax/top) < .3:
99 | freq_ax.set_ylim(top=top / 2)
100 | else:
101 | init.rempty = True # So "init" return [] (update everything on resizing)
102 | return [time_plot_line, freq_plot_line] # Update only what changed
103 | return []
104 |
105 | # Animate! (assignment to anim is needed to avoid garbage collecting it)
106 | anim = FuncAnimation(fig, animate, init_func=init, interval=10, blit=True)
107 | plt.ioff()
108 | plt.show() # Blocking
109 |
110 | # Stop the recording thread after closing the window
111 | update_data.finish = True
112 | th.join()
113 |
--------------------------------------------------------------------------------
/audiolazy/tests/test_auditory.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # This file is part of AudioLazy, the signal processing Python package.
3 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
4 | #
5 | # AudioLazy is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, version 3 of the License.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 | """
17 | Testing module for the lazy_auditory module
18 | """
19 |
20 | from __future__ import division
21 |
22 | import pytest
23 | p = pytest.mark.parametrize
24 |
25 | import itertools as it
26 | import os, json
27 |
28 | # Audiolazy internal imports
29 | from ..lazy_auditory import erb, gammatone_erb_constants, gammatone, phon2dB
30 | from ..lazy_misc import almost_eq, sHz
31 | from ..lazy_math import pi
32 | from ..lazy_filters import CascadeFilter
33 | from ..lazy_stream import Stream
34 | from ..lazy_compat import iteritems
35 |
36 |
37 | class TestERB(object):
38 |
39 | @p(("freq", "bandwidth"),
40 | [(1000, 132.639),
41 | (3000, 348.517),
42 | ])
43 | def test_glasberg_moore_slaney_example(self, freq, bandwidth):
44 | assert almost_eq.diff(erb["gm90"](freq), bandwidth, max_diff=5e-4)
45 |
46 | @p("erb_func", erb)
47 | @p("rate", [8000, 22050, 44100])
48 | @p("freq", [440, 20, 2e4])
49 | def test_two_input_methods(self, erb_func, rate, freq):
50 | Hz = sHz(rate)[1]
51 | assert almost_eq(erb_func(freq) * Hz, erb_func(freq * Hz, Hz))
52 | if freq < rate:
53 | with pytest.raises(ValueError):
54 | erb_func(freq * Hz)
55 |
56 |
57 | class TestGammatoneERBConstants(object):
58 |
59 | @p(("n", "an", "aninv", "cn", "cninv"), # Some paper values were changed:
60 | [(1, 3.142, 0.318, 2.000, 0.500), # + a1 was 3.141 (it should be pi)
61 | (2, 1.571, 0.637, 1.287, 0.777), # + a2 was 1.570, c2 was 1.288
62 | (3, 1.178, 0.849, 1.020, 0.981), # + 1/c3 was 0.980
63 | (4, 0.982, 1.019, 0.870, 1.149),
64 | (5, 0.859, 1.164, 0.771, 1.297), # + a5 was 0.889 (typo?), c5 was
65 | (6, 0.773, 1.293, 0.700, 1.429), # 0.772 and 1/c5 was 1.296
66 | (7, 0.709, 1.411, 0.645, 1.550), # + c7 was 0.646
67 | (8, 0.658, 1.520, 0.602, 1.662), # Doctests also suffered from this
68 | (9, 0.617, 1.621, 0.566, 1.767) # rounding issue.
69 | ])
70 | def test_annex_c_table_1(self, n, an, aninv, cn, cninv):
71 | x, y = gammatone_erb_constants(n)
72 | assert almost_eq.diff(x, aninv, max_diff=5e-4)
73 | assert almost_eq.diff(y, cn, max_diff=5e-4)
74 | assert almost_eq.diff(1./x, an, max_diff=5e-4)
75 | assert almost_eq.diff(1./y, cninv, max_diff=5e-4)
76 |
77 |
78 | class TestGammatone(object):
79 |
80 | some_data = [pi / 7, Stream(0, 1, 2, 1), [pi/3, pi/4, pi/5, pi/6]]
81 |
82 | @p(("filt_func", "freq", "bw"),
83 | [(gf, pi / 5, pi / 19) for gf in gammatone] +
84 | [(gammatone.klapuri, freq, bw) for freq, bw
85 | in it.product(some_data,some_data)]
86 | )
87 | def test_number_of_poles_order(self, filt_func, freq, bw):
88 | cfilt = filt_func(freq=freq, bandwidth=bw)
89 | assert isinstance(cfilt, CascadeFilter)
90 | assert len(cfilt) == 4
91 | for filt in cfilt:
92 | assert len(filt.denominator) == 3
93 |
94 |
95 | class TestPhon2DB(object):
96 |
97 | # Values from image analysis over the figure A.1 in the ISO/FDIS 226:2003
98 | # Annex A, page 5
99 | directory = os.path.split(__file__)[0]
100 | iso226_json_filename = os.path.join(directory, "iso226.json")
101 | with open(iso226_json_filename) as f:
102 | iso226_image_data = {None if k == "None" else int(k): v
103 | for k, v in iteritems(json.load(f))}
104 |
105 | @p(("loudness", "curve_data"), iso226_image_data.items())
106 | def test_match_curve_from_image_data(self, loudness, curve_data):
107 | freq2dB = phon2dB(loudness)
108 | for freq, spl in curve_data:
109 | assert almost_eq.diff(freq2dB(freq), spl, max_diff=.5)
110 |
--------------------------------------------------------------------------------
/audiolazy/lazy_compat.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # This file is part of AudioLazy, the signal processing Python package.
3 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
4 | #
5 | # AudioLazy is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, version 3 of the License.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 | """
17 | Compatibility tools to keep the same source working in both Python 2 and 3
18 | """
19 |
20 | import types
21 | import itertools as it
22 | import sys
23 |
24 | __all__ = ["orange", "PYTHON2", "builtins", "xrange", "xzip", "xzip_longest",
25 | "xmap", "xfilter", "STR_TYPES", "INT_TYPES", "SOME_GEN_TYPES",
26 | "NEXT_NAME", "iteritems", "itervalues", "im_func", "meta"]
27 |
28 |
29 | def orange(*args, **kwargs):
30 | """
31 | Old Python 2 range (returns a list), working both in Python 2 and 3.
32 | """
33 | return list(range(*args, **kwargs))
34 |
35 |
36 | PYTHON2 = sys.version_info.major == 2
37 | if PYTHON2:
38 | builtins = sys.modules["__builtin__"]
39 | else:
40 | import builtins
41 |
42 |
43 | xrange = getattr(builtins, "xrange", range)
44 | xzip = getattr(it, "izip", zip)
45 | xzip_longest = getattr(it, "izip_longest", getattr(it, "zip_longest", None))
46 | xmap = getattr(it, "imap", map)
47 | xfilter = getattr(it, "ifilter", filter)
48 |
49 |
50 | STR_TYPES = (getattr(builtins, "basestring", str),)
51 | INT_TYPES = (int, getattr(builtins, "long", None)) if PYTHON2 else (int,)
52 | SOME_GEN_TYPES = (types.GeneratorType, xrange(0).__class__, enumerate, xzip,
53 | xzip_longest, xmap, xfilter)
54 | NEXT_NAME = "next" if PYTHON2 else "__next__"
55 | HAS_MATMUL = sys.version_info >= (3,5)
56 |
57 |
58 | def iteritems(dictionary):
59 | """
60 | Function to use the generator-based items iterator over built-in
61 | dictionaries in both Python 2 and 3.
62 | """
63 | try:
64 | return getattr(dictionary, "iteritems")()
65 | except AttributeError:
66 | return iter(getattr(dictionary, "items")())
67 |
68 |
69 | def itervalues(dictionary):
70 | """
71 | Function to use the generator-based value iterator over built-in
72 | dictionaries in both Python 2 and 3.
73 | """
74 | try:
75 | return getattr(dictionary, "itervalues")()
76 | except AttributeError:
77 | return iter(getattr(dictionary, "values")())
78 |
79 |
80 | def im_func(method):
81 | """ Gets the function from the method in both Python 2 and 3. """
82 | return getattr(method, "im_func", method)
83 |
84 |
85 | def meta(*bases, **kwargs):
86 | """
87 | Allows unique syntax similar to Python 3 for working with metaclasses in
88 | both Python 2 and Python 3.
89 |
90 | Examples
91 | --------
92 | >>> class BadMeta(type): # An usual metaclass definition
93 | ... def __new__(mcls, name, bases, namespace):
94 | ... if "bad" not in namespace: # A bad constraint
95 | ... raise Exception("Oops, not bad enough")
96 | ... value = len(name) # To ensure this metaclass is called again
97 | ... def really_bad(self):
98 | ... return self.bad() * value
99 | ... namespace["really_bad"] = really_bad
100 | ... return super(BadMeta, mcls).__new__(mcls, name, bases, namespace)
101 | ...
102 | >>> class Bady(meta(object, metaclass=BadMeta)):
103 | ... def bad(self):
104 | ... return "HUA "
105 | ...
106 | >>> class BadGuy(Bady):
107 | ... def bad(self):
108 | ... return "R"
109 | ...
110 | >>> issubclass(BadGuy, Bady)
111 | True
112 | >>> Bady().really_bad() # Here value = 4
113 | 'HUA HUA HUA HUA '
114 | >>> BadGuy().really_bad() # Called metaclass ``__new__`` again, so value = 6
115 | 'RRRRRR'
116 |
117 | """
118 | metaclass = kwargs.get("metaclass", type)
119 | if not bases:
120 | bases = (object,)
121 | class NewMeta(type):
122 | def __new__(mcls, name, mbases, namespace):
123 | if name:
124 | return metaclass.__new__(metaclass, name, bases, namespace)
125 | return super(NewMeta, mcls).__new__(mcls, "", mbases, {})
126 | return NewMeta("", tuple(), {})
127 |
--------------------------------------------------------------------------------
/audiolazy/lazy_wav.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # This file is part of AudioLazy, the signal processing Python package.
3 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
4 | #
5 | # AudioLazy is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, version 3 of the License.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 | """
17 | Resources for opening data from Wave (.wav) files
18 | """
19 |
20 | from __future__ import division
21 |
22 | from struct import Struct
23 | import wave
24 |
25 | # Audiolazy internal imports
26 | from .lazy_stream import Stream
27 |
28 | __all__ = ["WavStream"]
29 |
30 |
31 | class WavStream(Stream):
32 | """
33 | A Stream related to a Wave file
34 |
35 | A WavStream instance is a Stream with extra attributes:
36 |
37 | * ``rate``: sample rate in samples per second;
38 | * ``channels``: number of channels (1 for mono, 2 for stereo);
39 | * ``bits``: bits per sample, a value in ``[8, 16, 24, 32]``.
40 |
41 | Example
42 | -------
43 |
44 | .. code-block:: python
45 |
46 | song = WavStream("my_song.wav")
47 | with AudioIO(True) as player:
48 | player.play(song, rate=song.rate, channels=song.channels)
49 |
50 | Note
51 | ----
52 | Stereo data is kept serialized/flat, so the resulting Stream yields first a
53 | sample from one channel, then the sample from the other channel for that
54 | same time instant. Use ``Stream.blocks(2)`` to get a Stream with the stereo
55 | blocks.
56 | """
57 | _unpackers = {
58 | 8 : ord, # The only unsigned
59 | 16: (lambda a: lambda v: a(v)[0])(Struct("> 8)(Struct(".
16 | """
17 | Testing module for the lazy_midi module
18 | """
19 |
20 | import pytest
21 | p = pytest.mark.parametrize
22 |
23 | from random import random
24 |
25 | # Audiolazy internal imports
26 | from ..lazy_midi import (MIDI_A4, FREQ_A4, SEMITONE_RATIO, midi2freq,
27 | str2midi, freq2midi, midi2str)
28 | from ..lazy_misc import almost_eq
29 | from ..lazy_compat import xzip
30 | from ..lazy_math import inf, nan, isinf, isnan
31 |
32 |
33 | class TestMIDI2Freq(object):
34 | table = [(MIDI_A4, FREQ_A4),
35 | (MIDI_A4 + 12, FREQ_A4 * 2),
36 | (MIDI_A4 + 24, FREQ_A4 * 4),
37 | (MIDI_A4 - 12, FREQ_A4 * .5),
38 | (MIDI_A4 - 24, FREQ_A4 * .25),
39 | (MIDI_A4 + 1, FREQ_A4 * SEMITONE_RATIO),
40 | (MIDI_A4 + 2, FREQ_A4 * SEMITONE_RATIO ** 2),
41 | (MIDI_A4 - 1, FREQ_A4 / SEMITONE_RATIO),
42 | (MIDI_A4 - 13, FREQ_A4 * .5 / SEMITONE_RATIO),
43 | (MIDI_A4 - 3, FREQ_A4 / SEMITONE_RATIO ** 3),
44 | (MIDI_A4 - 11, FREQ_A4 * SEMITONE_RATIO / 2),
45 | ]
46 |
47 | @p(("note", "freq"), table)
48 | def test_single_note(self, note, freq):
49 | assert almost_eq(midi2freq(note), freq)
50 |
51 | @p("data_type", [tuple, list])
52 | def test_note_list_tuple(self, data_type):
53 | notes, freqs = xzip(*self.table)
54 | assert almost_eq(midi2freq(data_type(notes)), data_type(freqs))
55 |
56 | invalid_table = [
57 | (inf, lambda x: isinf(x) and x > 0),
58 | (-inf, lambda x: x == 0),
59 | (nan, isnan),
60 | ]
61 | @p(("note", "func_result"), invalid_table)
62 | def test_invalid_inputs(self, note, func_result):
63 | assert func_result(midi2freq(note))
64 |
65 |
66 | class TestFreq2MIDI(object):
67 | @p(("note", "freq"), TestMIDI2Freq.table)
68 | def test_single_note(self, note, freq):
69 | assert almost_eq(freq2midi(freq), note)
70 |
71 | invalid_table = [
72 | (inf, lambda x: isinf(x) and x > 0),
73 | (0, lambda x: isinf(x) and x < 0),
74 | (-1, isnan),
75 | (-inf, isnan),
76 | (nan, isnan),
77 | ]
78 | @p(("freq", "func_result"), invalid_table)
79 | def test_invalid_inputs(self, freq, func_result):
80 | assert func_result(freq2midi(freq))
81 |
82 |
83 | class TestStr2MIDI(object):
84 | table = [("A4", MIDI_A4),
85 | ("A5", MIDI_A4 + 12),
86 | ("A3", MIDI_A4 - 12),
87 | ("Bb4", MIDI_A4 + 1),
88 | ("B4", MIDI_A4 + 2), # TestMIDI2Str.test_name_with_errors:
89 | ("C5", MIDI_A4 + 3), # These "go beyond" octave by a small amount
90 | ("C#5", MIDI_A4 + 4),
91 | ("Db3", MIDI_A4 - 20),
92 | ]
93 |
94 | @p(("name", "note"), table)
95 | def test_single_name(self, name, note):
96 | assert str2midi(name) == note
97 | assert str2midi(name.lower()) == note
98 | assert str2midi(name.upper()) == note
99 | assert str2midi(" " + name + " ") == note
100 |
101 | @p("data_type", [tuple, list])
102 | def test_name_list_tuple(self, data_type):
103 | names, notes = xzip(*self.table)
104 | assert str2midi(data_type(names)) == data_type(notes)
105 |
106 | def test_interrogation_input(self):
107 | assert isnan(str2midi("?"))
108 |
109 |
110 | class TestMIDI2Str(object):
111 | @p(("name", "note"), TestStr2MIDI.table)
112 | def test_single_name(self, name, note):
113 | assert midi2str(note, sharp="#" in name) == name
114 |
115 | @p(("name", "note"), TestStr2MIDI.table)
116 | def test_name_with_errors(self, name, note):
117 | error = round(random() / 3 + .1, 3) # Minimum is greater than tolerance
118 |
119 | full_name = name + "+{}%".format("%.1f" % (error * 100))
120 | assert midi2str(note + error, sharp="#" in name) == full_name
121 |
122 | full_name = name + "-{}%".format("%.1f" % (error * 100))
123 | assert midi2str(note - error, sharp="#" in name) == full_name
124 |
125 | @p("note", [inf, -inf, nan])
126 | def test_interrogation_output(self, note):
127 | assert midi2str(note) == "?"
128 |
--------------------------------------------------------------------------------
/audiolazy/tests/test_math.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # This file is part of AudioLazy, the signal processing Python package.
3 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
4 | #
5 | # AudioLazy is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, version 3 of the License.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 | """
17 | Testing module for the lazy_math module
18 | """
19 |
20 | import pytest
21 | p = pytest.mark.parametrize
22 |
23 | import itertools as it
24 |
25 | # Audiolazy internal imports
26 | from ..lazy_math import (factorial, dB10, dB20, inf, ln, log, log2, log10,
27 | log1p, pi, e, absolute, sign)
28 | from ..lazy_misc import almost_eq
29 | from ..lazy_stream import Stream
30 |
31 |
32 | class TestLog(object):
33 |
34 | funcs = {ln: e, log2: 2, log10: 10,
35 | (lambda x: log1p(x - 1)): e}
36 |
37 | @p("func", list(funcs))
38 | def test_zero(self, func):
39 | assert func(0) == func(0.) == func(0 + 0.j) == -inf
40 |
41 | @p("func", list(funcs))
42 | def test_one(self, func):
43 | assert func(1) == func(1.) == func(1. + 0j) == 0
44 |
45 | @p(("func", "base"), list(funcs.items()))
46 | def test_minus_one(self, func, base):
47 | for pair in it.combinations([func(-1),
48 | func(-1.),
49 | func(-1. + 0j),
50 | 1j * pi * func(e),
51 | ], 2):
52 | assert almost_eq(*pair)
53 |
54 | @p("base", [-1, -.5, 0., 1.])
55 | def test_invalid_bases(self, base):
56 | for val in [-10, 0, 10, base, base*base]:
57 | with pytest.raises(ValueError):
58 | log(val, base=base)
59 |
60 |
61 | class TestFactorial(object):
62 |
63 | @p(("n", "expected"), [(0, 1),
64 | (1, 1),
65 | (2, 2),
66 | (3, 6),
67 | (4, 24),
68 | (5, 120),
69 | (10, 3628800),
70 | (14, 87178291200),
71 | (29, 8841761993739701954543616000000),
72 | (30, 265252859812191058636308480000000),
73 | (6.0, 720),
74 | (7.0, 5040)
75 | ]
76 | )
77 | def test_valid_values(self, n, expected):
78 | assert factorial(n) == expected
79 |
80 | @p("n", [2.1, "7", 21j, "-8", -7.5, factorial])
81 | def test_non_integer(self, n):
82 | with pytest.raises(TypeError):
83 | factorial(n)
84 |
85 | @p("n", [-1, -2, -3, -4.0, -3.0, -factorial(30)])
86 | def test_negative(self, n):
87 | with pytest.raises(ValueError):
88 | factorial(n)
89 |
90 | @p(("n", "length"), [(2*factorial(7), 35980),
91 | (factorial(8), 168187)]
92 | )
93 | def test_really_big_number_length(self, n, length):
94 | assert len(str(factorial(n))) == length
95 |
96 |
97 | class TestDB10DB20(object):
98 |
99 | @p("func", [dB10, dB20])
100 | def test_zero(self, func):
101 | assert func(0) == -inf
102 |
103 |
104 | class TestAbsolute(object):
105 |
106 | def test_absolute(self):
107 | assert absolute(25) == 25
108 | assert absolute(-2) == 2
109 | assert absolute(-4j) == 4.
110 | assert almost_eq(absolute(3 + 4j), 5)
111 | assert absolute([5, -12, 14j, -2j, 0]) == [5, 12, 14., 2., 0]
112 | assert almost_eq(absolute([1.2, -1.57e-3, -(pi ** 2), -2j, 8 - 4j]),
113 | [1.2, 1.57e-3, pi ** 2, 2., 4 * 5 ** .5])
114 |
115 |
116 | class TestSign(object):
117 |
118 | def test_ints(self):
119 | assert sign(25) == 1
120 | assert sign(-2) == -1
121 | assert sign(0) == 0
122 | assert sign([4, -1, 3, 7, 0, 1, -8]) == [1, -1, 1, 1, 0, 1, -1]
123 |
124 | def test_floats(self):
125 | assert sign(.1) == 1
126 | assert sign(-.4) == -1
127 | assert sign(0.) == 0
128 | assert sign(-0.) == 0
129 | assert sign([-1., 5.3e-18, 0., 2.3, -1e37, 1.]) == [-1, 1, 0, 1, -1, 1]
130 |
131 | def test_complex_and_mixed(self):
132 | with pytest.raises(TypeError):
133 | sign(2j)
134 | with pytest.raises(TypeError):
135 | sign([1, 1 + 1e-25j])
136 | data = sign(Stream(3, -1, .3, 0j))
137 | assert data.take(3) == [1, -1, 1]
138 | with pytest.raises(TypeError):
139 | data.peek() # 0j is complex
140 |
--------------------------------------------------------------------------------
/math/lowpass_highpass_bilinear.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Digital filter design for the simplest lowpass and highpass filters using the
19 | bilinear transformation method with prewarping, using Sympy. The results
20 | matches the exact AudioLazy filter strategies ``highpass.z`` and
21 | ``lowpass.z``.
22 | """
23 | from __future__ import division, print_function, unicode_literals
24 | from audiolazy import Stream
25 | from sympy import (Symbol, init_printing, sympify, exp, pprint, Eq,
26 | factor, solve, I, sin, tan, pretty, together, radsimp)
27 | init_printing(use_unicode=True)
28 |
29 |
30 | def print_header(msg):
31 | msg_full = " ".join(["##", msg, "##"])
32 | msg_detail = "#" * len(msg_full)
33 | print(msg_detail, msg_full, msg_detail, sep="\n")
34 |
35 | def taylor(f, n=2, **kwargs):
36 | """
37 | Taylor/Mclaurin polynomial aproximation for the given function.
38 | The ``n`` (default 2) is the amount of aproximation terms for ``f``. Other
39 | arguments are keyword-only and will be passed to the ``f.series`` method.
40 | """
41 | return sum(Stream(f.series(n=None, **kwargs)).limit(n))
42 |
43 |
44 | # Symbols used
45 | p = Symbol("p", real=True) # Laplace pole
46 | f = Symbol("Omega", positive=True) # Frequency in rad/s (analog)
47 | s = Symbol("s") # Laplace Transform complex variable
48 |
49 | rate = Symbol("rate", positive=True) # Rate in samples/s
50 |
51 | G = Symbol("G", positive=True) # Digital gain (linear)
52 | R = Symbol("R", real=True) # Digital pole ("radius")
53 | w = Symbol("omega", real=True) # Frequency (rad/sample) usually in [0;pi]
54 | z = Symbol("z") # Z-Transform complex variable
55 |
56 | zinv = Symbol("z^-1") # z ** -1
57 |
58 |
59 | # Bilinear transform equation
60 | print_header("Bilinear transformation method")
61 | print("\nLaplace and Z Transforms are related by:")
62 | pprint(Eq(z, exp(s / rate)))
63 |
64 | print("\nBilinear transform approximation (no prewarping):")
65 | z_num = exp( s / (2 * rate))
66 | z_den = exp(-s / (2 * rate))
67 | assert z_num / z_den == exp(s / rate)
68 | z_bilinear = together(taylor(z_num, x=s, x0=0) / taylor(z_den, x=s, x0=0))
69 | pprint(Eq(z, z_bilinear))
70 |
71 | print("\nWhich also means:")
72 | s_bilinear = solve(Eq(z, z_bilinear), s)[0]
73 | pprint(Eq(s, radsimp(s_bilinear.subs(z, 1 / zinv))))
74 |
75 | print("\nPrewarping H(z) = H(s) at a frequency " +
76 | pretty(w) + " (rad/sample) to " +
77 | pretty(f) + " (rad/s):")
78 | pprint(Eq(z, exp(I * w)))
79 | pprint(Eq(s, I * f))
80 | f_prewarped = (s_bilinear / I).subs(z, exp(I * w)).rewrite(sin) \
81 | .rewrite(tan).cancel()
82 | pprint(Eq(f, f_prewarped))
83 |
84 |
85 | # Lowpass/highpass filters with prewarped bilinear transform equation
86 | T = tan(w / 2)
87 | for name, afilt_str in [("high", "s / (s - p)"),
88 | ("low", "-p / (s - p)")]:
89 | print()
90 | print_header("Laplace {0}pass filter (matches {0}pass.z)".format(name))
91 | print("\nFilter equations:")
92 | print("H(s) = " + afilt_str)
93 | afilt = sympify(afilt_str, dict(p=-f, s=s))
94 | pprint(Eq(p, -f)) # Proof is given in lowpass_highpass_matched_z.py
95 | print("where " + pretty(f) + " is the cut-off frequency in rad/s.")
96 |
97 | print("\nBilinear transformation (prewarping at the cut-off frequency):")
98 | filt = afilt.subs({f: f_prewarped,
99 | s: s_bilinear,
100 | z: 1 / zinv}).cancel().collect(zinv)
101 | pprint(Eq(Symbol("H(z)"), (filt)))
102 | print("where " + pretty(w) + " is the cut-off frequency in rad/sample.")
103 |
104 | print("\nThe single pole found is:")
105 | pole = 1 / solve(filt.as_numer_denom()[1], zinv)[0]
106 | pprint(Eq(Symbol("pole"), pole))
107 |
108 | print("\nSo we can assume ...")
109 | R_subs = -pole if name == "low" else pole
110 | RT_eq = Eq(R, R_subs)
111 | pprint(RT_eq)
112 |
113 | print("\n... and get a simpler equation:")
114 | pprint(Eq(Symbol("H(z)"), factor(filt.subs(T, solve(RT_eq, T)[0]))))
115 |
--------------------------------------------------------------------------------
/examples/ode_to_joy.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Playing Ode to Joy with a "score" written in code.
19 |
20 | You can change the synth, the music, the effects, etc.. There are some
21 | comments suggesting some modifications to this script.
22 | """
23 | from audiolazy import *
24 | import sys
25 |
26 | # Initialization
27 | rate = 44100
28 | s, Hz = sHz(rate)
29 | ms = 1e-3 * s
30 | beat = 120 # bpm
31 | quarter_dur = 60 * s / beat
32 |
33 |
34 | def delay(sig):
35 | """ Simple feedforward delay effect """
36 | smix = Streamix()
37 | sig = thub(sig, 3) # Auto-copy 3 times (remove this line if using feedback)
38 | smix.add(0, sig)
39 | # To get a feedback delay, use "smix.copy()" below instead of both "sig"
40 | smix.add(280 * ms, .1 * sig) # You can also try other constants
41 | smix.add(220 * ms, .1 * sig)
42 | return smix
43 | # When using the feedback delay proposed by the comments, you can use:
44 | #return smix.limit((1 + sum(dur for n, dur in notes)) * quarter_dur)
45 | # or something alike (e.g. ensuring that duration outside of this
46 | # function), helping you to avoid an endless signal.
47 |
48 |
49 | def note2snd(pitch, quarters):
50 | """
51 | Creates an audio Stream object for a single note.
52 |
53 | Parameters
54 | ----------
55 | pitch :
56 | Pitch note like ``"A4"``, as a string, or ``None`` for a rest.
57 | quarters :
58 | Duration in quarters (see ``quarter_dur``).
59 | """
60 | dur = quarters * quarter_dur
61 | if pitch is None:
62 | return zeros(dur)
63 | freq = str2freq(pitch) * Hz
64 | return synth(freq, dur)
65 |
66 |
67 | def synth(freq, dur):
68 | """
69 | Synth based on the Karplus-Strong model.
70 |
71 | Parameters
72 | ----------
73 | freq :
74 | Frequency, given in rad/sample.
75 | dur :
76 | Duration, given in samples.
77 |
78 | See Also
79 | --------
80 | sHz :
81 | Create constants ``s`` and ``Hz`` for converting "rad/sample" and
82 | "samples" to/from "seconds" and "hertz" using expressions like "440 * Hz".
83 | """
84 | return karplus_strong(freq, tau=800*ms).limit(dur)
85 |
86 |
87 | ## Uncomment these lines to use a "8-bits"-like synth
88 | #square_table = TableLookup([-1] * 512 + [1] * 512)
89 | #adsr_params = dict(a=30*ms, d=150*ms, s=.6, r=100*ms)
90 | #def synth(freq, dur, model=square_table):
91 | # """ Table-lookup synth with an ADSR envelope. """
92 | # # Why not trying "sinusoid" and "saw_table" instead of the "square_table"?
93 | # return model(freq) * adsr(dur, **adsr_params)
94 |
95 |
96 | ## Uncomment these lines to get a more "noisy" synth
97 | #sin_cube_table = sin_table ** 3
98 | #def synth(freq, dur):
99 | # env = adsr(dur, a=dur/3, d=dur/4, s=.1, r=5 * dur / 12)
100 | # env1 = env.copy() * white_noise(low=.9, high=1)
101 | # env2 = env * white_noise(low=.9, high=1)
102 | # sig1 = saw_table(gauss_noise(mu=freq, sigma=freq * .03)) * env1
103 | # sig2 = sin_cube_table(gauss_noise(mu=freq, sigma=freq * .03)) * env2
104 | # return .4 * sig1 + .6 * sig2
105 |
106 |
107 | # Musical "score"
108 | notes = [
109 | ("D4", 1), ("D4", 1), ("Eb4", 1), ("F4", 1),
110 | ("F4", 1), ("Eb4", 1), ("D4", 1), ("C4", 1),
111 | ("Bb3", 1), ("Bb3", 1), ("C4", 1), ("D4", 1),
112 | ("D4", 1.5), ("C4", .5), ("C4", 1.5), (None, .5),
113 |
114 | ("D4", 1), ("D4", 1), ("Eb4", 1), ("F4", 1),
115 | ("F4", 1), ("Eb4", 1), ("D4", 1), ("C4", 1),
116 | ("Bb3", 1), ("Bb3", 1), ("C4", 1), ("D4", 1),
117 | ("C4", 1.5), ("Bb3", .5), ("Bb3", 1.5), (None, .5),
118 | ]
119 |
120 |
121 | # Creates the music (lazily)
122 | # See play_bach_choral.py for a polyphonic (and safer) way to achieve this
123 | music = chain.from_iterable(starmap(note2snd, notes))
124 | #music = atan(15 * music) # Uncomment this to apply a simple dirtortion effect
125 | music = delay(.9 * music) # Uncomment this to apply a simple delay effect
126 |
127 |
128 | # Play it!
129 | music.append(zeros(.5 * s)) # Avoids an ending "click"
130 | api = sys.argv[1] if sys.argv[1:] else None # Choose API via command-line
131 | chunks.size = 1 if api == "jack" else 16
132 | with AudioIO(True, api=api) as player:
133 | player.play(music, rate=rate)
134 |
--------------------------------------------------------------------------------
/audiolazy/_internals.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # This file is part of AudioLazy, the signal processing Python package.
3 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
4 | #
5 | # AudioLazy is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, version 3 of the License.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 | """
17 | AudioLazy internals module
18 |
19 | The resources found here aren't DSP related nor take part of the main
20 | ``audiolazy`` namespace. Unless you're changing or trying to understand
21 | the AudioLazy internals, you probably don't need to know about this.
22 | """
23 |
24 | from functools import wraps, reduce
25 | from warnings import warn
26 | from glob import glob
27 | from operator import concat
28 | import os
29 |
30 |
31 | def deprecate(func):
32 | """ A deprecation warning emmiter as a decorator. """
33 | @wraps(func)
34 | def wrapper(*args, **kwargs):
35 | warn("Deprecated, this will be removed in the future", DeprecationWarning)
36 | return func(*args, **kwargs)
37 | wrapper.__doc__ = "Deprecated.\n" + (wrapper.__doc__ or "")
38 | return wrapper
39 |
40 |
41 | #
42 | # __init__.py importing resources
43 | #
44 |
45 | def get_module_names(package_path, pattern="lazy_*.py*"):
46 | """
47 | All names in the package directory that matches the given glob, without
48 | their extension. Repeated names should appear only once.
49 | """
50 | package_contents = glob(os.path.join(package_path[0], pattern))
51 | relative_path_names = (os.path.split(name)[1] for name in package_contents)
52 | no_ext_names = (os.path.splitext(name)[0] for name in relative_path_names)
53 | return sorted(set(no_ext_names))
54 |
55 | def get_modules(package_name, module_names):
56 | """ List of module objects from the package, keeping the name order. """
57 | def get_module(name):
58 | return __import__(".".join([package_name, name]), fromlist=[package_name])
59 | return [get_module(name) for name in module_names]
60 |
61 | def dunder_all_concat(modules):
62 | """ Single list with all ``__all__`` lists from the modules. """
63 | return reduce(concat, (getattr(m, "__all__", []) for m in modules), [])
64 |
65 |
66 | #
67 | # Resources for module/package summary tables on doctring
68 | #
69 |
70 | def summary_table(pairs, key_header, descr_header="Description", width=78):
71 | """
72 | List of one-liner strings containing a reStructuredText summary table
73 | for the given pairs ``(name, object)``.
74 | """
75 | from .lazy_text import rst_table, small_doc
76 | max_width = width - max(len(k) for k, v in pairs)
77 | table = [(k, small_doc(v, max_width=max_width)) for k, v in pairs]
78 | return rst_table(table, (key_header, descr_header))
79 |
80 | def docstring_with_summary(docstring, pairs, key_header, summary_type):
81 | """ Return a string joining the docstring with the pairs summary table. """
82 | return "\n".join(
83 | [docstring, "Summary of {}:".format(summary_type), ""] +
84 | summary_table(pairs, key_header) + [""]
85 | )
86 |
87 | def append_summary_to_module_docstring(module):
88 | """
89 | Change the ``module.__doc__`` docstring to include a summary table based
90 | on its contents as declared on ``module.__all__``.
91 | """
92 | pairs = [(name, getattr(module, name)) for name in module.__all__]
93 | kws = dict(key_header="Name", summary_type="module contents")
94 | module.__doc__ = docstring_with_summary(module.__doc__, pairs, **kws)
95 |
96 |
97 | #
98 | # Package initialization, first function to be called internally
99 | #
100 |
101 | def init_package(package_path, package_name, docstring):
102 | """
103 | Package initialization, to be called only by ``__init__.py``.
104 |
105 | - Find all module names;
106 | - Import all modules (so they're already cached on sys.modules), in
107 | the sorting order (this might make difference on cyclic imports);
108 | - Update all module docstrings (with the summary of its contents);
109 | - Build a module summary for the package docstring.
110 |
111 | Returns
112 | -------
113 | A 4-length tuple ``(modules, __all__, __doc__)``. The first one can be
114 | used by the package to import every module into the main package namespace.
115 | """
116 | module_names = get_module_names(package_path)
117 | modules = get_modules(package_name, module_names)
118 | dunder_all = dunder_all_concat(modules)
119 | for module in modules:
120 | append_summary_to_module_docstring(module)
121 | pairs = list(zip(module_names, modules))
122 | kws = dict(key_header="Module", summary_type="package modules")
123 | new_docstring = docstring_with_summary(docstring, pairs, **kws)
124 | return module_names, dunder_all, new_docstring
125 |
--------------------------------------------------------------------------------
/examples/fmbench.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | FM synthesis benchmarking
19 | """
20 |
21 | from __future__ import unicode_literals, print_function
22 | from timeit import timeit
23 | import sys
24 |
25 |
26 | # ===================
27 | # Some initialization
28 | # ===================
29 | num_tests = 30
30 | is_pypy = any(name.startswith("pypy") for name in dir(sys))
31 | if is_pypy:
32 | print("PyPy detected!")
33 | print()
34 | numpy_name = "numpypy"
35 | else:
36 | numpy_name = "numpy"
37 |
38 |
39 | # ======================
40 | # AudioLazy benchmarking
41 | # ======================
42 | kws = {}
43 | kws["setup"] = """
44 | from audiolazy import sHz, adsr, sinusoid
45 | from math import pi
46 | """
47 | kws["number"] = num_tests
48 | kws["stmt"] = """
49 | s, Hz = sHz(44100)
50 | ms = 1e-3 * s
51 | env = adsr(dur=5*s, a=20*ms, d=30*ms, s=.8, r=50*ms)
52 | sin_data = sinusoid(freq=440*Hz,
53 | phase=sinusoid(220*Hz) * pi)
54 | result = sum(env * sin_data)
55 | """
56 |
57 | print("=== AudioLazy benchmarking ===")
58 | print("Trials:", kws["number"])
59 | print()
60 | print("Setup code:")
61 | print(kws["setup"])
62 | print()
63 | print("Benchmark code (also executed once as 'setup'/'training'):")
64 | kws["setup"] += kws["stmt"] # Helpful for PyPy
65 | print(kws["stmt"])
66 | print()
67 | print("Mean time (milliseconds):")
68 | print(timeit(**kws) * 1e3 / num_tests)
69 | print("==============================")
70 | print()
71 |
72 |
73 | # ==================
74 | # Numpy benchmarking
75 | # ==================
76 | kws_np = {}
77 | kws_np["setup"] = "import {0} as np".format(numpy_name)
78 | kws_np["number"] = num_tests
79 | kws_np["stmt"] = """
80 | rate = 44100
81 | dur = 5 * rate
82 | sustain_level = .8
83 | # The np.linspace isn't in numpypy yet; it uses float64
84 | attack = np.linspace(0., 1., num=int(np.round(20e-3 * rate)), endpoint=False)
85 | decay = np.linspace(1., sustain_level, num=int(np.round(30e-3 * rate)),
86 | endpoint=False)
87 | release = np.linspace(sustain_level, 0., num=int(np.round(50e-3 * rate)),
88 | endpoint=False)
89 | sustain_dur = dur - len(attack) - len(decay) - len(release)
90 | sustain = sustain_level * np.ones(sustain_dur)
91 | env = np.hstack([attack, decay, sustain, release])
92 | freq220 = 220 * 2 * np.pi / rate
93 | freq440 = 440 * 2 * np.pi / rate
94 | phase220 = np.arange(dur, dtype=np.float64) * freq220
95 | phase440 = np.arange(dur, dtype=np.float64) * freq440
96 | sin_data = np.sin(phase440 + np.sin(phase220) * np.pi)
97 | result = np.sum(env * sin_data)
98 | """
99 |
100 | # Alternative for numpypy (since it don't have "linspace" nor "hstack")
101 | stmt_npp = """
102 | rate = 44100
103 | dur = 5 * rate
104 | sustain_level = .8
105 | len_attack = int(round(20e-3 * rate))
106 | attack = np.arange(len_attack, dtype=np.float64) / len_attack
107 | len_decay = int(round(30e-3 * rate))
108 | decay = (np.arange(len_decay - 1, -1, -1, dtype=np.float64
109 | ) / len_decay) * (1 - sustain_level) + sustain_level
110 | len_release = int(round(50e-3 * rate))
111 | release = (np.arange(len_release - 1, -1, -1, dtype=np.float64
112 | ) / len_release) * sustain_level
113 | env = np.ndarray(dur, dtype=np.float64)
114 | env[:len_attack] = attack
115 | env[len_attack:len_attack+len_decay] = decay
116 | env[len_attack+len_decay:dur-len_release] = sustain_level
117 | env[dur-len_release:dur] = release
118 | freq220 = 220 * 2 * np.pi / rate
119 | freq440 = 440 * 2 * np.pi / rate
120 | phase220 = np.arange(dur, dtype=np.float64) * freq220
121 | phase440 = np.arange(dur, dtype=np.float64) * freq440
122 | sin_data = np.sin(phase440 + np.sin(phase220) * np.pi)
123 | result = np.sum(env * sin_data)
124 | """
125 |
126 | try:
127 | if is_pypy:
128 | import numpypy as np
129 | else:
130 | import numpy as np
131 | except ImportError:
132 | print("Numpy not found. Finished benchmarking!")
133 | else:
134 | if is_pypy:
135 | kws_np["stmt"] = stmt_npp
136 |
137 | print("Numpy found!")
138 | print()
139 | print("=== Numpy benchmarking ===")
140 | print("Trials:", kws_np["number"])
141 | print()
142 | print("Setup code:")
143 | print(kws_np["setup"])
144 | print()
145 | print("Benchmark code (also executed once as 'setup'/'training'):")
146 | kws_np["setup"] += kws_np["stmt"] # Helpful for PyPy
147 | print(kws_np["stmt"])
148 | print()
149 | print("Mean time (milliseconds):")
150 | print(timeit(**kws_np) * 1e3 / num_tests)
151 | print("==========================")
152 |
--------------------------------------------------------------------------------
/audiolazy/tests/test_io.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # This file is part of AudioLazy, the signal processing Python package.
3 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
4 | #
5 | # AudioLazy is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, version 3 of the License.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 | """
17 | Testing module for the lazy_io module
18 | """
19 |
20 | import pytest
21 | p = pytest.mark.parametrize
22 |
23 | import pyaudio
24 | import _portaudio
25 | from collections import deque
26 | from time import sleep
27 | import struct
28 |
29 | # Audiolazy internal imports
30 | from ..lazy_io import AudioIO, chunks
31 | from ..lazy_synth import white_noise
32 | from ..lazy_stream import Stream
33 | from ..lazy_misc import almost_eq
34 | from ..lazy_compat import orange
35 |
36 |
37 | class WaitStream(Stream):
38 | """
39 | FIFO ControlStream-like class in which ``value`` is a deque object that
40 | waits a given duration when there's no more data available.
41 | """
42 |
43 | def __init__(self, duration=.01):
44 | """ Constructor. Duration in seconds """
45 | self.value = deque()
46 | self.active = True
47 |
48 | def data_generator():
49 | while self.active or self.value:
50 | try:
51 | yield self.value.popleft()
52 | except:
53 | sleep(duration)
54 |
55 | super(WaitStream, self).__init__(data_generator())
56 |
57 |
58 | class MockPyAudio(object):
59 | """
60 | Fake pyaudio.PyAudio I/O manager class to work with only one output.
61 | """
62 | def __init__(self):
63 | self.fake_output = Stream(0.)
64 | self._streams = set()
65 | self.terminated = False
66 |
67 | def terminate(self):
68 | assert len(self._streams) == 0
69 | self.terminated = True
70 |
71 | def open(self, **kwargs):
72 | new_pastream = pyaudio.Stream(self, **kwargs)
73 | self._streams.add(new_pastream)
74 | return new_pastream
75 |
76 |
77 | class MockStream(object):
78 | """
79 | Fake pyaudio.Stream class for testing.
80 | """
81 | def __init__(self, pa_manager, **kwargs):
82 | self._pa = pa_manager
83 | self._stream = self
84 | self.output = "output" in kwargs and kwargs["output"]
85 | if self.output:
86 | pa_manager.fake_output = WaitStream()
87 |
88 | def close(self):
89 | if self.output: # This is the only output
90 | self._pa.fake_output.active = False
91 | self._pa._streams.remove(self)
92 |
93 |
94 | def mock_write_stream(pa_stream, data, chunk_size, should_throw_exception):
95 | """
96 | Fake _portaudio.write_stream function for testing.
97 | """
98 | sdata = struct.unpack("{0}{1}".format(chunk_size, "f"), data)
99 | pa_stream._pa.fake_output.value.extend(sdata)
100 |
101 |
102 | @p("data", [orange(25), white_noise(100) + 3.])
103 | @pytest.mark.timeout(2)
104 | def test_output_only(monkeypatch, data):
105 | monkeypatch.setattr(pyaudio, "PyAudio", MockPyAudio)
106 | monkeypatch.setattr(pyaudio, "Stream", MockStream)
107 | monkeypatch.setattr(_portaudio, "write_stream", mock_write_stream)
108 |
109 | chunk_size = 16
110 | data = list(data)
111 | with AudioIO(True) as player:
112 | player.play(data, chunk_size=chunk_size)
113 |
114 | played_data = list(player._pa.fake_output)
115 | ld, lpd = len(data), len(played_data)
116 | assert all(isinstance(x, float) for x in played_data)
117 | assert lpd % chunk_size == 0
118 | assert lpd - ld == -ld % chunk_size
119 | assert all(x == 0. for x in played_data[ld - lpd:]) # Zero-pad at end
120 | assert almost_eq(played_data, data) # Data loss (64-32bits conversion)
121 |
122 | assert player._pa.terminated # Test whether "terminate" was called
123 |
124 |
125 | @p("func", chunks)
126 | class TestChunks(object):
127 |
128 | data = [17., -3.42, 5.4, 8.9, 27., 45.2, 1e-5, -3.7e-4, 7.2, .8272, -4.]
129 | ld = len(data)
130 | sizes = [1, 2, 3, 4, ld - 1, ld, ld + 1, 2 * ld, 2 * ld + 1]
131 | data_segments = (lambda d: [d[:idx] for idx, unused in enumerate(d)])(data)
132 |
133 | @p("size", sizes)
134 | @p("given_data", data_segments)
135 | def test_chunks(self, func, given_data, size):
136 | dfmt="f"
137 | padval=0.
138 | data = b"".join(func(given_data, size=size, dfmt=dfmt, padval=padval))
139 | samples_in = len(given_data)
140 | samples_out = samples_in
141 | if samples_in % size != 0:
142 | samples_out -= samples_in % -size
143 | assert samples_out > samples_in # Testing the tester...
144 | restored_data = struct.Struct(dfmt * samples_out).unpack(data)
145 | assert almost_eq(given_data,
146 | restored_data[:samples_in],
147 | ignore_type=True)
148 | assert almost_eq([padval]*(samples_out - samples_in),
149 | restored_data[samples_in:],
150 | ignore_type=True)
151 |
152 | @p("size", sizes)
153 | def test_default_size(self, func, size):
154 | dsize = chunks.size
155 | assert list(func(self.data)) == list(func(self.data, size=dsize))
156 | try:
157 | chunks.size = size
158 | assert list(func(self.data)) == list(func(self.data, size=size))
159 | finally:
160 | chunks.size = dsize
161 |
--------------------------------------------------------------------------------
/audiolazy/lazy_midi.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # This file is part of AudioLazy, the signal processing Python package.
3 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
4 | #
5 | # AudioLazy is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, version 3 of the License.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 | """
17 | MIDI representation data & note-frequency relationship
18 | """
19 |
20 | import itertools as it
21 |
22 | # Audiolazy internal imports
23 | from .lazy_misc import elementwise
24 | from .lazy_math import log2, nan, isinf, isnan
25 |
26 | __all__ = ["MIDI_A4", "FREQ_A4", "SEMITONE_RATIO", "str2freq",
27 | "str2midi", "freq2str", "freq2midi", "midi2freq", "midi2str",
28 | "octaves"]
29 |
30 | # Useful constants
31 | MIDI_A4 = 69 # MIDI Pitch number
32 | FREQ_A4 = 440. # Hz
33 | SEMITONE_RATIO = 2. ** (1. / 12.) # Ascending
34 |
35 |
36 | @elementwise("midi_number", 0)
37 | def midi2freq(midi_number):
38 | """
39 | Given a MIDI pitch number, returns its frequency in Hz.
40 | """
41 | return FREQ_A4 * 2 ** ((midi_number - MIDI_A4) * (1./12.))
42 |
43 |
44 | @elementwise("note_string", 0)
45 | def str2midi(note_string):
46 | """
47 | Given a note string name (e.g. "Bb4"), returns its MIDI pitch number.
48 | """
49 | if note_string == "?":
50 | return nan
51 | data = note_string.strip().lower()
52 | name2delta = {"c": -9, "d": -7, "e": -5, "f": -4, "g": -2, "a": 0, "b": 2}
53 | accident2delta = {"b": -1, "#": 1, "x": 2}
54 | accidents = list(it.takewhile(lambda el: el in accident2delta, data[1:]))
55 | octave_delta = int(data[len(accidents) + 1:]) - 4
56 | return (MIDI_A4 +
57 | name2delta[data[0]] + # Name
58 | sum(accident2delta[ac] for ac in accidents) + # Accident
59 | 12 * octave_delta # Octave
60 | )
61 |
62 |
63 | def str2freq(note_string):
64 | """
65 | Given a note string name (e.g. "F#2"), returns its frequency in Hz.
66 | """
67 | return midi2freq(str2midi(note_string))
68 |
69 |
70 | @elementwise("freq", 0)
71 | def freq2midi(freq):
72 | """
73 | Given a frequency in Hz, returns its MIDI pitch number.
74 | """
75 | result = 12 * (log2(freq) - log2(FREQ_A4)) + MIDI_A4
76 | return nan if isinstance(result, complex) else result
77 |
78 |
79 | @elementwise("midi_number", 0)
80 | def midi2str(midi_number, sharp=True):
81 | """
82 | Given a MIDI pitch number, returns its note string name (e.g. "C3").
83 | """
84 | if isinf(midi_number) or isnan(midi_number):
85 | return "?"
86 | num = midi_number - (MIDI_A4 - 4 * 12 - 9)
87 | note = (num + .5) % 12 - .5
88 | rnote = int(round(note))
89 | error = note - rnote
90 | octave = str(int(round((num - note) / 12.)))
91 | if sharp:
92 | names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
93 | else:
94 | names = ["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"]
95 | names = names[rnote] + octave
96 | if abs(error) < 1e-4:
97 | return names
98 | else:
99 | err_sig = "+" if error > 0 else "-"
100 | err_str = err_sig + str(round(100 * abs(error), 2)) + "%"
101 | return names + err_str
102 |
103 |
104 | def freq2str(freq):
105 | """
106 | Given a frequency in Hz, returns its note string name (e.g. "D7").
107 | """
108 | return midi2str(freq2midi(freq))
109 |
110 |
111 | def octaves(freq, fmin=20., fmax=2e4):
112 | """
113 | Given a frequency and a frequency range, returns all frequencies in that
114 | range that is an integer number of octaves related to the given frequency.
115 |
116 | Parameters
117 | ----------
118 | freq :
119 | Frequency, in any (linear) unit.
120 | fmin, fmax :
121 | Frequency range, in the same unit of ``freq``. Defaults to 20.0 and
122 | 20,000.0, respectively.
123 |
124 | Returns
125 | -------
126 | A list of frequencies, in the same unit of ``freq`` and in ascending order.
127 |
128 | Examples
129 | --------
130 | >>> from audiolazy import octaves, sHz
131 | >>> octaves(440.)
132 | [27.5, 55.0, 110.0, 220.0, 440.0, 880.0, 1760.0, 3520.0, 7040.0, 14080.0]
133 | >>> octaves(440., fmin=3000)
134 | [3520.0, 7040.0, 14080.0]
135 | >>> Hz = sHz(44100)[1] # Conversion unit from sample rate
136 | >>> freqs = octaves(440 * Hz, fmin=300 * Hz, fmax = 1000 * Hz) # rad/sample
137 | >>> len(freqs) # Number of octaves
138 | 2
139 | >>> [round(f, 6) for f in freqs] # Values in rad/sample
140 | [0.062689, 0.125379]
141 | >>> [round(f / Hz, 6) for f in freqs] # Values in Hz
142 | [440.0, 880.0]
143 |
144 | """
145 | # Input validation
146 | if any(f <= 0 for f in (freq, fmin, fmax)):
147 | raise ValueError("Frequencies have to be positive")
148 |
149 | # If freq is out of range, avoid range extension
150 | while freq < fmin:
151 | freq *= 2
152 | while freq > fmax:
153 | freq /= 2
154 | if freq < fmin: # Gone back and forth
155 | return []
156 |
157 | # Finds the range for a valid input
158 | return list(it.takewhile(lambda x: x > fmin,
159 | (freq * 2 ** harm for harm in it.count(0, -1))
160 | ))[::-1] \
161 | + list(it.takewhile(lambda x: x < fmax,
162 | (freq * 2 ** harm for harm in it.count(1))
163 | ))
164 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | AudioLazy package setup file
19 | """
20 |
21 | from setuptools import setup
22 | from setuptools.command.test import test as TestClass
23 | import os, ast
24 |
25 | class Tox(TestClass):
26 | user_options = []
27 |
28 | def finalize_options(self):
29 | TestClass.finalize_options(self)
30 | self.test_args = ["-v"] if self.verbose else []
31 | self.test_suite = True
32 |
33 | def run_tests(self):
34 | import sys, tox
35 | sys.exit(tox.cmdline(self.test_args))
36 |
37 |
38 | def locals_from_exec(code):
39 | """ Run code in a qualified exec, returning the resulting locals dict """
40 | namespace = {}
41 | exec(code, {}, namespace)
42 | return namespace
43 |
44 | def pseudo_import(fname):
45 | """ Namespace dict from assignments in the file without ``__import__`` """
46 | is_d_import = lambda n: isinstance(n, ast.Name) and n.id == "__import__"
47 | is_assign = lambda n: isinstance(n, ast.Assign)
48 | is_valid = lambda n: is_assign(n) and not any(map(is_d_import, ast.walk(n)))
49 | with open(fname, "r") as f:
50 | astree = ast.parse(f.read(), filename=fname)
51 | astree.body = [node for node in astree.body if is_valid(node)]
52 | return locals_from_exec(compile(astree, fname, mode="exec"))
53 |
54 |
55 | def read_rst_and_process(fname, line_process=lambda line: line):
56 | """
57 | The reStructuredText string in file ``fname``, without the starting ``..``
58 | comment and with ``line_process`` function applied to every line.
59 | """
60 | with open(fname, "r") as f:
61 | data = f.read().splitlines()
62 | first_idx = next(idx for idx, line in enumerate(data) if line.strip())
63 | if data[first_idx].strip() == "..":
64 | next_idx = first_idx + 1
65 | first_idx = next(idx for idx, line in enumerate(data[next_idx:], next_idx)
66 | if line.strip() and not line.startswith(" "))
67 | return "\n".join(map(line_process, data[first_idx:]))
68 |
69 | def image_path_processor_factory(path):
70 | """ Processor for concatenating the ``path`` to relative path images """
71 | def processor(line):
72 | markup = ".. image::"
73 | if line.startswith(markup):
74 | fname = line[len(markup):].strip()
75 | if not(fname.startswith("/") or "://" in fname):
76 | return "{} {}{}".format(markup, path, fname)
77 | return line
78 | return processor
79 |
80 | def read_description(readme_file, changes_file, images_url):
81 | updater = image_path_processor_factory(images_url)
82 | readme_data = read_rst_and_process(readme_file, updater)
83 | changes_data = read_rst_and_process(changes_file, updater)
84 | parts = readme_data.split("\n\n", 12)
85 | title = parts[0]
86 | pins = "\n\n".join(parts[1:-2])
87 | descr = parts[-2]
88 | sections = parts[-1].rsplit("----", 1)[0]
89 | long_descr_blocks = ["", title, "", pins, "", sections, "", changes_data]
90 | return descr, "\n".join(block.strip() for block in long_descr_blocks)
91 |
92 |
93 | path = os.path.split(__file__)[0]
94 | package_name = "audiolazy"
95 |
96 | fname_init = os.path.join(path, package_name, "__init__.py")
97 | fname_readme = os.path.join(path, "README.rst")
98 | fname_changes = os.path.join(path, "CHANGES.rst")
99 | images_url = "https://raw.github.com/danilobellini/audiolazy/master/"
100 |
101 | metadata = {k.strip("_") : v for k, v in pseudo_import(fname_init).items()}
102 | metadata["description"], metadata["long_description"] = \
103 | read_description(fname_readme, fname_changes, images_url)
104 | metadata["classifiers"] = """
105 | Development Status :: 3 - Alpha
106 | Intended Audience :: Developers
107 | Intended Audience :: Education
108 | Intended Audience :: Science/Research
109 | Intended Audience :: Other Audience
110 | License :: OSI Approved :: GNU General Public License v3 (GPLv3)
111 | Operating System :: MacOS
112 | Operating System :: Microsoft :: Windows
113 | Operating System :: POSIX :: Linux
114 | Operating System :: OS Independent
115 | Programming Language :: Python
116 | Programming Language :: Python :: 2
117 | Programming Language :: Python :: 2.7
118 | Programming Language :: Python :: 3
119 | Programming Language :: Python :: 3.2
120 | Programming Language :: Python :: 3.3
121 | Programming Language :: Python :: 3.4
122 | Programming Language :: Python :: 3.5
123 | Programming Language :: Python :: 3.6
124 | Programming Language :: Python :: Implementation :: CPython
125 | Programming Language :: Python :: Implementation :: PyPy
126 | Topic :: Artistic Software
127 | Topic :: Multimedia :: Sound/Audio
128 | Topic :: Multimedia :: Sound/Audio :: Analysis
129 | Topic :: Multimedia :: Sound/Audio :: Capture/Recording
130 | Topic :: Multimedia :: Sound/Audio :: Editors
131 | Topic :: Multimedia :: Sound/Audio :: Mixers
132 | Topic :: Multimedia :: Sound/Audio :: Players
133 | Topic :: Multimedia :: Sound/Audio :: Sound Synthesis
134 | Topic :: Multimedia :: Sound/Audio :: Speech
135 | Topic :: Scientific/Engineering
136 | Topic :: Software Development
137 | Topic :: Software Development :: Libraries
138 | Topic :: Software Development :: Libraries :: Python Modules
139 | """.strip().splitlines()
140 | metadata["license"] = "GPLv3"
141 | metadata["name"] = package_name
142 | metadata["packages"] = [package_name]
143 | metadata["tests_require"] = ["tox"]
144 | metadata["cmdclass"] = {"test": Tox}
145 | setup(**metadata)
146 |
--------------------------------------------------------------------------------
/audiolazy/tests/test_itertools.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # This file is part of AudioLazy, the signal processing Python package.
3 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
4 | #
5 | # AudioLazy is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, version 3 of the License.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 | """
17 | Testing module for the lazy_itertools module
18 | """
19 |
20 | import pytest
21 | p = pytest.mark.parametrize
22 |
23 | import operator
24 | from functools import reduce
25 |
26 | # Audiolazy internal imports
27 | from ..lazy_itertools import accumulate, chain, izip, count
28 | from ..lazy_stream import Stream
29 | from ..lazy_math import inf
30 | from ..lazy_poly import x
31 |
32 |
33 | @p("acc", accumulate)
34 | class TestAccumulate(object):
35 |
36 | @p("empty", [[], tuple(), set(), Stream([])])
37 | def test_empty_input(self, acc, empty):
38 | data = acc(empty)
39 | assert isinstance(data, Stream)
40 | assert list(data) == []
41 |
42 | def test_one_input(self, acc):
43 | for k in [1, -5, 1e3, inf, x]:
44 | data = acc([k])
45 | assert isinstance(data, Stream)
46 | assert list(data) == [k]
47 |
48 | def test_few_numbers(self, acc):
49 | data = acc(Stream([4, 7, 5, 3, -2, -3, -1, 12, 8, .5, -13]))
50 | assert isinstance(data, Stream)
51 | assert list(data) == [4, 11, 16, 19, 17, 14, 13, 25, 33, 33.5, 20.5]
52 |
53 |
54 | class TestCount(object):
55 |
56 | def test_no_input(self):
57 | data = count()
58 | assert isinstance(data, Stream)
59 | assert data.take(14) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
60 | assert data.take(3) == [14, 15, 16]
61 |
62 | @p("start", [0, -1, 7])
63 | def test_starting_value(self, start):
64 | data1 = count(start)
65 | data2 = count(start=start)
66 | assert isinstance(data1, Stream)
67 | assert isinstance(data2, Stream)
68 | expected_zero = Stream([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13])
69 | expected = (expected_zero + start).take(20)
70 | after = [14 + start, 15 + start, 16 + start]
71 | assert data1.take(14) == expected
72 | assert data2.take(13) == expected[:-1]
73 | assert data1.take(3) == after
74 | assert data2.take(4) == expected[-1:] + after
75 |
76 | @p("start", [0, -5, 1])
77 | @p("step", [1, -1, 3])
78 | def test_two_inputs(self, start, step):
79 | data1 = count(start, step)
80 | data2 = count(start=start, step=step)
81 | assert isinstance(data1, Stream)
82 | assert isinstance(data2, Stream)
83 | expected_zero = Stream([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13])
84 | expected = (expected_zero * step + start).take(20)
85 | after = list(Stream([14, 15, 16]) * step + start)
86 | assert data1.take(14) == expected
87 | assert data2.take(13) == expected[:-1]
88 | assert data1.take(3) == after
89 | assert data2.take(4) == expected[-1:] + after
90 |
91 |
92 | class TestChain(object):
93 |
94 | data = [1, 5, 3, 17, -2, 8, chain, izip, pytest, lambda x: x, 8.2]
95 | some_lists = [data, data[:5], data[3:], data[::-1], data[::2], data[1::3]]
96 |
97 | @p("blk", some_lists)
98 | def test_with_one_list_three_times(self, blk):
99 | expected = blk + blk + blk
100 | result = chain(blk, blk, blk)
101 | assert isinstance(result, Stream)
102 | assert list(result) == expected
103 | result = chain.from_iterable(3 * [blk])
104 | assert isinstance(result, Stream)
105 | assert result.take(inf) == expected
106 |
107 | def test_with_lists(self):
108 | blk = self.some_lists
109 | result = chain(*blk)
110 | assert isinstance(result, Stream)
111 | expected = list(reduce(operator.concat, blk))
112 | assert list(result) == expected
113 | result = chain.from_iterable(blk)
114 | assert isinstance(result, Stream)
115 | assert list(result) == expected
116 | result = chain.star(blk)
117 | assert isinstance(result, Stream)
118 | assert list(result) == expected
119 |
120 | def test_with_endless_stream(self):
121 | expected = [1, 2, -3, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
122 | result = chain([1, 2, -3], count())
123 | assert isinstance(result, Stream)
124 | assert result.take(len(expected)) == expected
125 | result = chain.from_iterable(([1, 2, -3], count()))
126 | assert isinstance(result, Stream)
127 | assert result.take(len(expected)) == expected
128 |
129 | def test_star_with_generator_input(self):
130 | def gen():
131 | yield [5, 5, 5]
132 | yield [2, 2]
133 | yield count(-4, 2)
134 | expected = [5, 5, 5, 2, 2, -4, -2, 0, 2, 4, 6, 8, 10, 12]
135 | result = chain.star(gen())
136 | assert isinstance(result, Stream)
137 | assert result.take(len(expected)) == expected
138 | assert chain.star is chain.from_iterable
139 |
140 | @pytest.mark.timeout(2)
141 | def test_star_with_endless_generator_input(self):
142 | def gen(): # Yields [], [1], [2, 2], [3, 3, 3], ...
143 | for c in count():
144 | yield [c] * c
145 | expected = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6]
146 | result = chain.star(gen())
147 | assert isinstance(result, Stream)
148 | assert result.take(len(expected)) == expected
149 |
150 |
151 | class TestIZip(object):
152 |
153 | def test_smallest(self):
154 | for func in [izip, izip.smallest]:
155 | result = func([1, 2, 3], [4, 5])
156 | assert isinstance(result, Stream)
157 | assert list(result) == [(1, 4), (2, 5)]
158 |
159 | def test_longest(self):
160 | result = izip.longest([1, 2, 3], [4, 5])
161 | assert isinstance(result, Stream)
162 | assert list(result) == [(1, 4), (2, 5), (3, None)]
163 |
164 | def test_longest_fillvalue(self):
165 | result = izip.longest([1, -2, 3], [4, 5], fillvalue=0)
166 | assert isinstance(result, Stream)
167 | assert list(result) == [(1, 4), (-2, 5), (3, 0)]
168 |
--------------------------------------------------------------------------------
/math/lowpass_highpass_digital.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Digital filter design for the AudioLazy lowpass and highpass filters
19 | strategies ``pole`` and ``z``, using Sympy.
20 |
21 | The single pole at R (or -R) should be real to ensure a real output.
22 | """
23 | from __future__ import division, print_function, unicode_literals
24 | from functools import reduce
25 | from sympy import (Symbol, preorder_traversal, C, init_printing, S, sympify,
26 | exp, expand_complex, cancel, trigsimp, pprint, Eq, factor,
27 | solve, I, expand, pi, sin, fraction, pretty, tan)
28 | from collections import OrderedDict
29 | init_printing(use_unicode=True)
30 |
31 |
32 | def fcompose(*funcs):
33 | return lambda data: reduce(lambda d, p: p(d), funcs, data)
34 |
35 | def has_sqrt(sympy_obj):
36 | return any(el.func is C.Pow and el.args[-1] is S.Half
37 | for el in preorder_traversal(sympy_obj))
38 |
39 |
40 | G = Symbol("G", positive=True) # Gain (linear)
41 | R = Symbol("R", real=True) # Pole "radius"
42 | w = Symbol("omega", real=True) # Frequency (rad/sample) usually in [0;pi]
43 | z = Symbol("z") # Z-Transform complex variable
44 |
45 |
46 | def design_z_filter_single_pole(filt_str, max_gain_freq):
47 | """
48 | Finds the coefficients for a simple lowpass/highpass filter.
49 |
50 | This function just prints the coefficient values, besides the given
51 | filter equation and its power gain. There's 3 constraints used to find the
52 | coefficients:
53 |
54 | 1. The G value is defined by the max gain of 1 (0 dB) imposed at a
55 | specific frequency
56 | 2. The R value is defined by the 50% power cutoff frequency given in
57 | rad/sample.
58 | 3. Filter should be stable (-1 < R < 1)
59 |
60 | Parameters
61 | ----------
62 | filt_str :
63 | Filter equation as a string using the G, R, w and z values.
64 | max_gain_freq :
65 | A value of zero (DC) or pi (Nyquist) to ensure the max gain as 1 (0 dB).
66 |
67 | Note
68 | ----
69 | The R value is evaluated only at pi/4 rad/sample to find whether -1 < R < 1,
70 | and the max gain is assumed to be either 0 or pi, using other values might
71 | fail.
72 | """
73 | print("H(z) = " + filt_str) # Avoids printing as "1/z"
74 | filt = sympify(filt_str, dict(G=G, R=R, w=w, z=z))
75 | print()
76 |
77 | # Finds the power magnitude equation for the filter
78 | freq_resp = filt.subs(z, exp(I * w))
79 | frr, fri = freq_resp.as_real_imag()
80 | power_resp = fcompose(expand_complex, cancel, trigsimp)(frr ** 2 + fri ** 2)
81 | pprint(Eq(Symbol("Power"), power_resp))
82 | print()
83 |
84 | # Finds the G value given the max gain value of 1 at the DC or Nyquist
85 | # frequency. As exp(I*pi) is -1 and exp(I*0) is 1, we can use freq_resp
86 | # (without "abs") instead of power_resp.
87 | Gsolutions = factor(solve(Eq(freq_resp.subs(w, max_gain_freq), 1), G))
88 | assert len(Gsolutions) == 1
89 | pprint(Eq(G, Gsolutions[0]))
90 | print()
91 |
92 | # Finds the unconstrained R values for a given cutoff frequency
93 | power_resp_no_G = power_resp.subs(G, Gsolutions[0])
94 | half_power_eq = Eq(power_resp_no_G, S.Half)
95 | Rsolutions = solve(half_power_eq, R)
96 |
97 | # Constraining -1 < R < 1 when w = pi/4 (although the constraint is general)
98 | Rsolutions_stable = [el for el in Rsolutions if -1 < el.subs(w, pi/4) < 1]
99 | assert len(Rsolutions_stable) == 1
100 |
101 | # Constraining w to the [0;pi] range, so |sin(w)| = sin(w)
102 | Rsolution = Rsolutions_stable[0].subs(abs(sin(w)), sin(w))
103 | pprint(Eq(R, Rsolution))
104 |
105 | # More information about the pole (or -pole)
106 | print("\n ** Alternative way to write R **\n")
107 | if has_sqrt(Rsolution):
108 | x = Symbol("x") # A helper symbol
109 | xval = sum(el for el in Rsolution.args if not has_sqrt(el))
110 | pprint(Eq(x, xval))
111 | print()
112 | pprint(Eq(R, expand(Rsolution.subs(xval, x))))
113 | else:
114 | # That's also what would be found in a bilinear transform with prewarping
115 | pprint(Eq(R, Rsolution.rewrite(tan).cancel())) # Not so nice numerically
116 |
117 | # See whether the R denominator can be zeroed
118 | for root in solve(fraction(Rsolution)[1], w):
119 | if 0 <= root <= pi:
120 | power_resp_r = fcompose(expand, cancel)(power_resp_no_G.subs(w, root))
121 | Rsolutions_r = solve(Eq(power_resp_r, S.Half), R)
122 | assert len(Rsolutions_r) == 1
123 | print("\nDenominator is zero for this value of " + pretty(w))
124 | pprint(Eq(w, root))
125 | pprint(Eq(R, Rsolutions_r[0]))
126 |
127 |
128 | filters_data = OrderedDict([
129 | ("lowpass.pole", # No zeros (constant numerator)
130 | "G / (1 - R * z ** -1)"),
131 | ("highpass.pole", # No zeros (constant numerator)
132 | "G / (1 + R * z ** -1)"),
133 | ("highpass.z", # Single zero at 1, so gain at the DC level is zero (-inf dB)
134 | "G * (1 - z ** -1) / (1 - R * z ** -1)"),
135 | ("lowpass.z", # Single zero at -1, so gain=0 (-inf dB) at the Nyquist freq
136 | "G * (1 + z ** -1) / (1 + R * z ** -1)"),
137 | ])
138 |
139 | if __name__ == "__main__":
140 | for name, filt_str in filters_data.items():
141 | ftype, fstrategy = name.split(".")
142 | descr = ("single zero and " if fstrategy == "z" else "") + "single pole"
143 | msg = "## Filter {name} ({descr} {ftype}) ##".format(**locals())
144 | msg_detail = "#" * len(msg)
145 | print(msg_detail, msg, msg_detail, "", sep="\n")
146 | max_gain_freq = 0 if ftype == "lowpass" else pi
147 | design_z_filter_single_pole(filt_str, max_gain_freq=max_gain_freq)
148 | print("\n\n" + " --//-- " * 8 + "\n\n")
149 |
--------------------------------------------------------------------------------
/math/lowpass_highpass_matched_z.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Digital filter design for the AudioLazy lowpass and highpass filters
19 | strategies ``pole_exp`` and ``z_exp`` from analog design via the
20 | matching Z-transform technique, using Sympy.
21 |
22 | This script just prints the coefficient values, the filter equations and
23 | power gain for the analog (Laplace transform), digital (Z-Transform) and
24 | mirrored digital filters. About the analog filter, there's some constraints
25 | used to find the coefficients:
26 |
27 | - The g "gain" value is found from the max gain of 1 (0 dB) imposed at a
28 | specific frequency (zero for lowpass, symbolic "infinite" for highpass).
29 | - The p value is found from the 50% power cutoff frequency given in rad/s.
30 | - The single pole at p should be real to ensure a real output.
31 | - The Laplace equation pole should be negative to ensure stability.
32 |
33 | Note that this matching procedure is an approximation. For precise values for
34 | the coefficients, you should look for a design technique that works directly
35 | with digital filters, or perhaps a numerical approach.
36 | """
37 | from __future__ import division, print_function, unicode_literals
38 | from sympy import (Symbol, init_printing, S, sympify, exp, cancel, pprint, Eq,
39 | factor, solve, I, pi, oo, limit)
40 | init_printing(use_unicode=True)
41 |
42 |
43 | # Symbols used
44 | g = Symbol("g", positive=True) # Analog gain (linear)
45 | p = Symbol("p", real=True) # Laplace pole
46 | f = Symbol("Omega", positive=True) # Frequency in rad/s (analog)
47 | s = Symbol("s") # Laplace Transform complex variable
48 |
49 | rate = Symbol("rate", positive=True) # Rate in samples/s
50 |
51 | G = Symbol("G", positive=True) # Digital gain (linear)
52 | R = Symbol("R", real=True) # Digital pole ("radius")
53 | w = Symbol("omega", real=True) # Frequency (rad/sample) usually in [0;pi]
54 | z = Symbol("z") # Z-Transform complex variable
55 |
56 |
57 | for max_gain_freq in [0, oo]: # Freq whose gain is max and equal to 1 (0 dB).
58 | has_zero = max_gain_freq != 0
59 |
60 | # Build some useful strings from the parameters
61 | if has_zero: # See the "Matching Z-Transform" comment
62 | afilt_str = "g * s / (s - p)" # for more details on the filt_str values
63 | filt_str = "G * (1 - z ** -1) / (1 - R * z ** -1)" # Single zero at exp(0)
64 | strategy = "z_exp"
65 | prefix, mprefix = ["high", "low"]
66 | else:
67 | afilt_str = "g / (s - p)"
68 | filt_str = "G / (1 - R * z ** -1)"
69 | strategy = "pole_exp"
70 | prefix, mprefix = ["low", "high"]
71 | filt_name = prefix + "pass." + strategy
72 | mfilt_name = mprefix + "pass." + strategy
73 |
74 | # Output header
75 | xtra_descr = "and single zero " if has_zero else ""
76 | msg = "## Laplace single pole {}{}pass filter ##".format(xtra_descr, prefix)
77 | msg_detail = "#" * len(msg)
78 | print(msg_detail, msg, msg_detail, sep="\n")
79 |
80 | # Creates the analog filter sympy object
81 | print("\n ** Analog design (Laplace Transform) **\n")
82 | print("H(s) = " + afilt_str)
83 | afilt = sympify(afilt_str, dict(g=g, p=p, s=s))
84 | print()
85 |
86 | # Finds the power magnitude equation for the filter
87 | freq_resp = afilt.subs(s, I * f)
88 | frr, fri = freq_resp.as_real_imag()
89 | power_resp = cancel(frr ** 2 + fri ** 2)
90 | pprint(Eq(Symbol("Power"), power_resp))
91 | print()
92 |
93 | # Finds the g value given the max gain value of 1 at the DC frequency. As
94 | # I*0 is zero and I*s cancels the imaginary unit at the limit when s -> oo
95 | # (as both numerator and denominator becomes purely complex numbers),
96 | # we can use freq_resp (without "abs") instead of power_resp.
97 | gsolutions = factor(solve(Eq(limit(freq_resp, f, max_gain_freq), 1), g))
98 | assert len(gsolutions) == 1
99 | pprint(Eq(g, gsolutions[0]))
100 | print()
101 |
102 | # Finds the p value for a given cutoff frequency, imposing stability (p < 0)
103 | power_resp_no_g = power_resp.subs(g, gsolutions[0])
104 | half_power_eq = Eq(power_resp_no_g, S.Half)
105 | psolutions_stable = [el for el in solve(half_power_eq, p) if el < 0]
106 | assert len(psolutions_stable) == 1
107 | psolution = psolutions_stable[0]
108 | pprint(Eq(p, psolution))
109 |
110 | # Creates the digital filter sympy object
111 | print("\n ** Digital design (Z-Transform) for {} **\n".format(filt_name))
112 | print("H(z) = " + filt_str)
113 | filt = sympify(filt_str, dict(G=G, R=R, z=z))
114 | print()
115 |
116 | # Matching Z-Transform
117 | # for each zero/pole in both Laplace and Z-Transform equations,
118 | # z_zp = exp(s_zp / rate)
119 | # where z_zp and s_zp are a single zero/pole for such equations
120 | Rsolution = exp(psolution / rate).subs(f, w * rate)
121 | pprint(Eq(R, Rsolution))
122 | print()
123 |
124 | # Finds the G value that fits with the Laplace filter for a single
125 | # frequency: the max_gain_freq (and its matched Z-Transform value)
126 | gain_eq = Eq(filt.subs(z, 1 if max_gain_freq == 0 else -1),
127 | limit(afilt, s, max_gain_freq).subs(g, gsolutions[0]))
128 | Gsolutions = factor(solve(gain_eq, G))
129 | assert len(Gsolutions) == 1
130 | pprint(Eq(G, Gsolutions[0]))
131 |
132 | # Mirroring the Z-Transform linear frequency response, so:
133 | # z_mirror = -z
134 | # w_mirror = pi - w
135 | print("\n ** Mirroring lowpass.pole_exp to get the {} **\n"
136 | .format(mfilt_name))
137 | mfilt_str = filt_str.replace(" - ", " + ")
138 | print("H(z) = " + mfilt_str)
139 | mfilt = sympify(mfilt_str, dict(G=G, R=R, z=z))
140 | assert filt.subs(z, -z) == mfilt
141 | print()
142 | pprint(Eq(R, Rsolution.subs(w, pi - w)))
143 | print()
144 | pprint(Eq(G, Gsolutions[0])) # This was kept
145 | print()
--------------------------------------------------------------------------------
/audiolazy/tests/test_wav.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # This file is part of AudioLazy, the signal processing Python package.
3 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
4 | #
5 | # AudioLazy is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, version 3 of the License.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 | """
17 | Testing module for the lazy_wav module
18 | """
19 |
20 | import pytest
21 | p = pytest.mark.parametrize
22 |
23 | from tempfile import NamedTemporaryFile
24 | from struct import Struct
25 | import io
26 |
27 | # Audiolazy internal imports
28 | from ..lazy_wav import WavStream
29 | from ..lazy_stream import Stream, thub
30 | from ..lazy_misc import almost_eq, DEFAULT_SAMPLE_RATE
31 |
32 |
33 | uint16pack = Struct("= min_value)
174 | assert all(ws <= max_value)
175 | assert almost_eq(result, expected)
176 |
--------------------------------------------------------------------------------
/examples/save_and_memoize_synth.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Random synthesis with saving and memoization
19 | """
20 |
21 | from __future__ import division
22 | from audiolazy import (sHz, octaves, chain, adsr, gauss_noise, sin_table, pi,
23 | sinusoid, lag2freq, Streamix, zeros, clip, lowpass,
24 | TableLookup, line, inf, xrange, thub, chunks)
25 | from random import choice, uniform, randint
26 | from functools import wraps, reduce
27 | from contextlib import closing
28 | import operator, wave
29 |
30 |
31 | #
32 | # Helper functions
33 | #
34 | def memoize(func):
35 | """
36 | Decorator for unerasable memoization based on function arguments, for
37 | functions without keyword arguments.
38 | """
39 | class Memoizer(dict):
40 | def __missing__(self, args):
41 | val = func(*args)
42 | self[args] = val
43 | return val
44 | memory = Memoizer()
45 | @wraps(func)
46 | def wrapper(*args):
47 | return memory[args]
48 | return wrapper
49 |
50 |
51 | def save_to_16bit_wave_file(fname, sig, rate):
52 | """
53 | Save a given signal ``sig`` to file ``fname`` as a 16-bit one-channel wave
54 | with the given ``rate`` sample rate.
55 | """
56 | with closing(wave.open(fname, "wb")) as wave_file:
57 | wave_file.setnchannels(1)
58 | wave_file.setsampwidth(2)
59 | wave_file.setframerate(rate)
60 | for chunk in chunks((clip(sig) * 2 ** 15).map(int), dfmt="h", padval=0):
61 | wave_file.writeframes(chunk)
62 |
63 |
64 | #
65 | # AudioLazy Initialization
66 | #
67 | rate = 44100
68 | s, Hz = sHz(rate)
69 | ms = 1e-3 * s
70 |
71 | # Frequencies (always in Hz here)
72 | freq_base = 440
73 | freq_min = 100
74 | freq_max = 8000
75 | ratios = [1/1, 8/7, 7/6, 3/2, 49/32, 7/4] # 2/1 is the next octave
76 | concat = lambda iterables: reduce(operator.concat, iterables, [])
77 | oct_partial = lambda freq: octaves(freq, fmin = freq_min, fmax = freq_max)
78 | freqs = concat(oct_partial(freq_base * ratio) for ratio in ratios)
79 |
80 |
81 | #
82 | # Audio synthesis models
83 | #
84 | def freq_gen():
85 | """
86 | Endless frequency generator (in rad/sample).
87 | """
88 | while True:
89 | yield choice(freqs) * Hz
90 |
91 |
92 | def new_note_track(env, synth):
93 | """
94 | Audio track with the frequencies.
95 |
96 | Parameters
97 | ----------
98 | env:
99 | Envelope Stream (which imposes the duration).
100 | synth:
101 | One-argument function that receives a frequency (in rad/sample) and
102 | returns a Stream instance (a synthesized note).
103 |
104 | Returns
105 | -------
106 | Endless Stream instance that joins synthesized notes.
107 |
108 | """
109 | list_env = list(env)
110 | return chain.from_iterable(synth(freq) * list_env for freq in freq_gen())
111 |
112 |
113 | @memoize
114 | def unpitched_high(dur, idx):
115 | """
116 | Non-harmonic treble/higher frequency sound as a list (due to memoization).
117 |
118 | Parameters
119 | ----------
120 | dur:
121 | Duration, in samples.
122 | idx:
123 | Zero or one (integer), for a small difference to the sound played.
124 |
125 | Returns
126 | -------
127 | A list with the synthesized note.
128 |
129 | """
130 | first_dur, a, d, r, gain = [
131 | (30 * ms, 10 * ms, 8 * ms, 10 * ms, .4),
132 | (60 * ms, 20 * ms, 8 * ms, 20 * ms, .5)
133 | ][idx]
134 | env = chain(adsr(first_dur, a=a, d=d, s=.2, r=r),
135 | adsr(dur - first_dur,
136 | a=10 * ms, d=30 * ms, s=.2, r=dur - 50 * ms))
137 | result = gauss_noise(dur) * env * gain
138 | return list(result)
139 |
140 |
141 | # Values used by the unpitched low synth
142 | harmonics = dict(enumerate([3] * 4 + [2] * 4 + [1] * 10))
143 | low_table = sin_table.harmonize(harmonics).normalize()
144 |
145 |
146 | @memoize
147 | def unpitched_low(dur, idx):
148 | """
149 | Non-harmonic bass/lower frequency sound as a list (due to memoization).
150 |
151 | Parameters
152 | ----------
153 | dur:
154 | Duration, in samples.
155 | idx:
156 | Zero or one (integer), for a small difference to the sound played.
157 |
158 | Returns
159 | -------
160 | A list with the synthesized note.
161 |
162 | """
163 | env = sinusoid(lag2freq(dur * 2)).limit(dur) ** 2
164 | freq = 40 + 20 * sinusoid(1000 * Hz, phase=uniform(-pi, pi)) # Hz
165 | result = (low_table(freq * Hz) + low_table(freq * 1.1 * Hz)) * env * .5
166 | return list(result)
167 |
168 |
169 | def geometric_delay(sig, dur, copies, pamp=.5):
170 | """
171 | Delay effect by copying data (with Streamix).
172 |
173 | Parameters
174 | ----------
175 | sig:
176 | Input signal (an iterable).
177 | dur:
178 | Duration, in samples.
179 | copies:
180 | Number of times the signal will be replayed in the given duration. The
181 | signal is played copies + 1 times.
182 | pamp:
183 | The relative remaining amplitude fraction for the next played Stream,
184 | based on the idea that total amplitude should sum to 1. Defaults to 0.5.
185 |
186 | """
187 | out = Streamix()
188 | sig = thub(sig, copies + 1)
189 | out.add(0, sig * pamp) # Original
190 | remain = 1 - pamp
191 | for unused in xrange(copies):
192 | gain = remain * pamp
193 | out.add(dur / copies, sig * gain)
194 | remain -= gain
195 | return out
196 |
197 |
198 | #
199 | # Audio mixture
200 | #
201 | tracks = 3 # besides unpitched track
202 | dur_note = 120 * ms
203 | dur_perc = 100 * ms
204 | smix = Streamix()
205 |
206 | # Pitched tracks based on a 1:2 triangular wave
207 | table = TableLookup(line(100, -1, 1).append(line(200, 1, -1)).take(inf))
208 | for track in xrange(tracks):
209 | env = adsr(dur_note, a=20 * ms, d=10 * ms, s=.8, r=30 * ms) / 1.7 / tracks
210 | smix.add(0, geometric_delay(new_note_track(env, table), 80 * ms, 2))
211 |
212 | # Unpitched tracks
213 | pfuncs = [unpitched_low] * 4 + [unpitched_high]
214 | snd = chain.from_iterable(choice(pfuncs)(dur_perc, randint(0, 1))
215 | for unused in zeros())
216 | smix.add(0, geometric_delay(snd * (1 - 1/1.7), 20 * ms, 1))
217 |
218 |
219 | #
220 | # Finishes (save in a wave file)
221 | #
222 | data = lowpass(5000 * Hz)(smix).limit(180 * s)
223 | fname = "audiolazy_save_and_memoize_synth.wav"
224 | save_to_16bit_wave_file(fname, data, rate)
225 |
--------------------------------------------------------------------------------
/examples/mcfm.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | Modulo Counter graphics with FM synthesis audio in a wxPython application
19 | """
20 |
21 | # The GUI in this example is based on the dose TDD semaphore source code
22 | # https://github.com/danilobellini/dose
23 |
24 | import wx, sys
25 | from math import pi
26 | from audiolazy import (ControlStream, modulo_counter, chunks,
27 | AudioIO, sHz, sinusoid)
28 |
29 | MIN_WIDTH = 15 # pixels
30 | MIN_HEIGHT = 15
31 | FIRST_WIDTH = 200
32 | FIRST_HEIGHT = 200
33 | MOUSE_TIMER_WATCH = 50 # ms
34 | DRAW_TIMER = 50
35 |
36 | s, Hz = sHz(44100)
37 |
38 | class McFMFrame(wx.Frame):
39 |
40 | def __init__(self, parent):
41 | frame_style = (wx.FRAME_SHAPED | # Allows wx.SetShape
42 | wx.FRAME_NO_TASKBAR |
43 | wx.STAY_ON_TOP |
44 | wx.NO_BORDER
45 | )
46 | super(McFMFrame, self).__init__(parent, style=frame_style)
47 | self.Bind(wx.EVT_ERASE_BACKGROUND, lambda evt: None)
48 | self._paint_width, self._paint_height = 0, 0 # Ensure update_sizes at
49 | # first on_paint
50 | self.ClientSize = (FIRST_WIDTH, FIRST_HEIGHT)
51 | self.Bind(wx.EVT_PAINT, self.on_paint)
52 | self._draw_timer = wx.Timer(self)
53 | self.Bind(wx.EVT_TIMER, self.on_draw_timer, self._draw_timer)
54 | self.on_draw_timer()
55 | self.angstep = ControlStream(pi/90)
56 | self.rotstream = modulo_counter(modulo=2*pi, step=self.angstep)
57 | self.rotation_data = iter(self.rotstream)
58 |
59 | def on_draw_timer(self, evt=None):
60 | self.Refresh()
61 | self._draw_timer.Start(DRAW_TIMER, True)
62 |
63 | def on_paint(self, evt):
64 | dc = wx.AutoBufferedPaintDCFactory(self)
65 | gc = wx.GraphicsContext.Create(dc) # Anti-aliasing
66 |
67 | gc.SetPen(wx.Pen("blue", width=4))
68 | gc.SetBrush(wx.Brush("black"))
69 | w, h = self.ClientSize
70 | gc.DrawRectangle(0, 0, w, h)
71 |
72 | gc.SetPen(wx.Pen("gray", width=2))
73 | w, h = w - 10, h - 10
74 | gc.Translate(5, 5)
75 | gc.DrawEllipse(0, 0, w, h)
76 | gc.SetPen(wx.Pen("red", width=1))
77 | gc.SetBrush(wx.Brush("yellow"))
78 | gc.Translate(w * .5, h * .5)
79 | gc.Scale(w, h)
80 | rot = next(self.rotation_data)
81 | gc.Rotate(-rot)
82 | gc.Translate(.5, 0)
83 | gc.Rotate(rot)
84 | gc.Scale(1./w, 1./h)
85 | gc.DrawEllipse(-5, -5, 10, 10)
86 |
87 |
88 | class InteractiveFrame(McFMFrame):
89 | def __init__(self, parent):
90 | super(InteractiveFrame, self).__init__(parent)
91 | self._timer = wx.Timer(self)
92 | self.Bind(wx.EVT_RIGHT_DOWN, self.on_right_down)
93 | self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
94 | self.Bind(wx.EVT_TIMER, self.on_timer, self._timer)
95 |
96 | @property
97 | def player(self):
98 | return self._player
99 |
100 | @player.setter
101 | def player(self, value):
102 | # Also initialize playing thread
103 | self._player = value
104 | self.volume_ctrl = ControlStream(.2)
105 | self.carrier_ctrl = ControlStream(220)
106 | self.mod_ctrl = ControlStream(440)
107 | sound = sinusoid(freq=self.carrier_ctrl * Hz,
108 | phase=sinusoid(self.mod_ctrl * Hz)
109 | ) * self.volume_ctrl
110 | self.playing_thread = player.play(sound)
111 |
112 | def on_right_down(self, evt):
113 | self.Close()
114 |
115 | def on_left_down(self, evt):
116 | self._key_state = None # Ensures initialization
117 | self.on_timer(evt)
118 |
119 | def on_timer(self, evt):
120 | """
121 | Keep watching the mouse displacement via timer
122 | Needed since EVT_MOVE doesn't happen once the mouse gets outside the
123 | frame
124 | """
125 | ctrl_is_down = wx.GetKeyState(wx.WXK_CONTROL)
126 | ms = wx.GetMouseState()
127 |
128 | # New initialization when keys pressed change
129 | if self._key_state != ctrl_is_down:
130 | self._key_state = ctrl_is_down
131 |
132 | # Keep state at click
133 | self._click_ms_x, self._click_ms_y = ms.x, ms.y
134 | self._click_frame_x, self._click_frame_y = self.Position
135 | self._click_frame_width, self._click_frame_height = self.ClientSize
136 |
137 | # Avoids refresh when there's no move (stores last mouse state)
138 | self._last_ms = ms.x, ms.y
139 |
140 | # Quadrant at click (need to know how to resize)
141 | width, height = self.ClientSize
142 | self._quad_signal_x = 1 if (self._click_ms_x -
143 | self._click_frame_x) / width > .5 else -1
144 | self._quad_signal_y = 1 if (self._click_ms_y -
145 | self._click_frame_y) / height > .5 else -1
146 |
147 | # "Polling watcher" for mouse left button while it's kept down
148 | if (wx.__version__ >= "3" and ms.leftIsDown) or \
149 | (wx.__version__ < "3" and ms.leftDown):
150 | if self._last_ms != (ms.x, ms.y): # Moved?
151 | self._last_ms = (ms.x, ms.y)
152 | delta_x = ms.x - self._click_ms_x
153 | delta_y = ms.y - self._click_ms_y
154 |
155 | # Resize
156 | if ctrl_is_down:
157 | # New size
158 | new_w = max(MIN_WIDTH, self._click_frame_width +
159 | 2 * delta_x * self._quad_signal_x
160 | )
161 | new_h = max(MIN_HEIGHT, self._click_frame_height +
162 | 2 * delta_y * self._quad_signal_y
163 | )
164 | self.ClientSize = new_w, new_h
165 | self.SendSizeEvent() # Needed for wxGTK
166 |
167 | # Center should be kept
168 | center_x = self._click_frame_x + self._click_frame_width / 2
169 | center_y = self._click_frame_y + self._click_frame_height / 2
170 | self.Position = (center_x - new_w / 2,
171 | center_y - new_h / 2)
172 |
173 | self.Refresh()
174 | self.volume_ctrl.value = (new_h * new_w) / 3e5
175 |
176 | # Move the window
177 | else:
178 | self.Position = (self._click_frame_x + delta_x,
179 | self._click_frame_y + delta_y)
180 |
181 | # Find the new center position
182 | x, y = self.Position
183 | w, h = self.ClientSize
184 | cx, cy = x + w/2, y + h/2
185 | self.mod_ctrl.value = 2.5 * cx
186 | self.carrier_ctrl.value = 2.5 * cy
187 | self.angstep.value = (cx + cy) * pi * 2e-4
188 |
189 | # Since left button is kept down, there should be another one shot
190 | # timer event again, without creating many timers like wx.CallLater
191 | self._timer.Start(MOUSE_TIMER_WATCH, True)
192 |
193 |
194 | class McFMApp(wx.App):
195 |
196 | def OnInit(self):
197 | self.SetAppName("mcfm")
198 | self.wnd = InteractiveFrame(None)
199 | self.wnd.Show()
200 | self.SetTopWindow(self.wnd)
201 | return True # Needed by wxPython
202 |
203 |
204 | if __name__ == "__main__":
205 | api = sys.argv[1] if sys.argv[1:] else None # Choose API via command-line
206 | chunks.size = 1 if api == "jack" else 16
207 | with AudioIO(api=api) as player:
208 | app = McFMApp(False, player)
209 | app.wnd.player = player
210 | app.MainLoop()
211 |
--------------------------------------------------------------------------------
/audiolazy/tests/test_synth.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # This file is part of AudioLazy, the signal processing Python package.
3 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
4 | #
5 | # AudioLazy is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, version 3 of the License.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 | """
17 | Testing module for the lazy_synth module
18 | """
19 |
20 | import pytest
21 | p = pytest.mark.parametrize
22 |
23 | import itertools as it
24 |
25 | # Audiolazy internal imports
26 | from ..lazy_synth import (modulo_counter, line, impulse, ones, zeros, zeroes,
27 | white_noise, gauss_noise, TableLookup, fadein,
28 | fadeout, sin_table, saw_table)
29 | from ..lazy_stream import Stream
30 | from ..lazy_misc import almost_eq, sHz, blocks, rint, lag2freq
31 | from ..lazy_compat import orange, xrange, xzip
32 | from ..lazy_itertools import count
33 | from ..lazy_math import pi, inf
34 |
35 |
36 | class TestLineFadeInFadeOut(object):
37 |
38 | def test_line(self):
39 | s, Hz = sHz(rate=2)
40 | L = line(4 * s, .1, .9)
41 | assert almost_eq(L, (.1 * x for x in xrange(1, 9)))
42 |
43 | def test_line_append(self):
44 | s, Hz = sHz(rate=3)
45 | L1 = line(2 * s, 2, 8)
46 | L1_should = [2, 3, 4, 5, 6, 7]
47 | L2 = line(1 * s, 8, -1)
48 | L2_should = [8, 5, 2]
49 | L3 = line(2 * s, -1, 9, finish=True)
50 | L3_should = [-1, 1, 3, 5, 7, 9]
51 | env = L1.append(L2).append(L3)
52 | env = env.map(int)
53 | env_should = L1_should + L2_should + L3_should
54 | assert list(env) == env_should
55 |
56 | def test_fade_in(self):
57 | s, Hz = sHz(rate=4)
58 | L = fadein(2.5 * s)
59 | assert almost_eq(L, (.1 * x for x in xrange(10)))
60 |
61 | def test_fade_out(self):
62 | s, Hz = sHz(rate=5)
63 | L = fadeout(2 * s)
64 | assert almost_eq(L, (.1 * x for x in xrange(10, 0, -1)))
65 |
66 |
67 | class TestModuloCounter(object):
68 |
69 | def test_ints(self):
70 | assert modulo_counter(0, 3, 2).take(8) == [0, 2, 1, 0, 2, 1, 0, 2]
71 |
72 | def test_floats(self):
73 | assert almost_eq(modulo_counter(1., 5., 3.3).take(10),
74 | [1., 4.3, 2.6, .9, 4.2, 2.5, .8, 4.1, 2.4, .7])
75 |
76 | def test_ints_modulo_one(self):
77 | assert modulo_counter(0, 1, 7).take(3) == [0, 0, 0]
78 | assert modulo_counter(0, 1, -1).take(4) == [0, 0, 0, 0]
79 | assert modulo_counter(0, 1, 0).take(5) == [0, 0, 0, 0, 0]
80 |
81 | def test_step_zero(self):
82 | assert modulo_counter(7, 5, 0).take(2) == [2] * 2
83 | assert modulo_counter(1, -2, 0).take(4) == [-1] * 4
84 | assert modulo_counter(0, 3.141592653589793, 0).take(7) == [0] * 7
85 |
86 | def test_streamed_step(self):
87 | mc = modulo_counter(5, 15, modulo_counter(0, 3, 2))
88 | assert mc.take(18) == [5, 5, 7, 8, 8, 10, 11, 11, 13, 14, 14, 1, 2, 2, 4,
89 | 5, 5, 7]
90 |
91 | def test_streamed_start(self):
92 | mc = modulo_counter(modulo_counter(2, 5, 3), 7, 1)
93 | # start = [2,0,3,1,4, 2,0,3,1,4, ...]
94 | should_mc = (Stream(2, 0, 3, 1, 4) + count()) % 7
95 | assert mc.take(29) == should_mc.take(29)
96 |
97 | @p("step", [0, 17, -17])
98 | def test_streamed_start_ignorable_step(self, step):
99 | mc = modulo_counter(it.count(), 17, step)
100 | assert mc.take(30) == (orange(17) * 2)[:30]
101 |
102 | def test_streamed_start_and_step(self):
103 | mc = modulo_counter(Stream(3, 3, 2), 17, it.count())
104 | should_step = [0, 0, 1, 3, 6, 10, 15-17, 21-17, 28-17, 36-34, 45-34,
105 | 55-51, 66-68]
106 | should_start = [3, 3, 2, 3, 3, 2, 3, 3, 2, 3, 3, 2, 3, 3, 2, 3]
107 | should_mc = [a+b for a,b in xzip(should_start, should_step)]
108 | assert mc.take(len(should_mc)) == should_mc
109 |
110 | def test_streamed_modulo(self):
111 | mc = modulo_counter(12, Stream(7, 5), 8)
112 | assert mc.take(30) == [5, 3, 4, 2, 3, 1, 2, 0, 1, 4] * 3
113 |
114 | def test_streamed_start_and_modulo(self):
115 | mc = modulo_counter(it.count(), 3 + count(), 1)
116 | expected = [0, 2, 4, 0, 2, 4, 6, 8, 10, 0, 2, 4, 6, 8, 10, 12,
117 | 14, 16, 18, 20, 22, 0, 2, 4, 6, 8, 10, 12, 14, 16]
118 | assert mc.take(len(expected)) == expected
119 |
120 | def test_all_inputs_streamed(self):
121 | mc1 = modulo_counter(it.count(), 3 + count(), Stream(0, 1))
122 | mc2 = modulo_counter(0, 3 + count(), 1 + Stream(0, 1))
123 | expected = [0, 1, 3, 4, 6, 7, 0, 1, 3, 4, 6, 7, 9, 10, 12, 13,
124 | 15, 16, 18, 19, 21, 22, 24, 25, 0, 1, 3, 4, 6, 7]
125 | assert mc1.take(len(expected)) == mc2.take(len(expected)) == expected
126 |
127 | classes = (float, Stream)
128 |
129 | @p("start", [-1e-16, -1e-100])
130 | @p("cstart", classes)
131 | @p("cmodulo", classes)
132 | @p("cstep", classes)
133 | def test_bizarre_modulo(self, start, cstart, cmodulo, cstep):
134 | # Not really a modulo counter issue, but used by modulo counter
135 | for step in xrange(2, 900):
136 | mc = modulo_counter(cstart(start),
137 | cmodulo(step),
138 | cstep(step))
139 | assert all(mc.limit(4) < step)
140 |
141 | @p(("func", "data"),
142 | [(ones, 1.0),
143 | (zeros, 0.0),
144 | (zeroes, 0.0)
145 | ])
146 | class TestOnesZerosZeroes(object):
147 |
148 | def test_no_input(self, func, data):
149 | my_stream = func()
150 | assert isinstance(my_stream, Stream)
151 | assert my_stream.take(25) == [data] * 25
152 |
153 | def test_inf_input(self, func, data):
154 | my_stream = func(inf)
155 | assert isinstance(my_stream, Stream)
156 | assert my_stream.take(30) == [data] * 30
157 |
158 | @p("dur", [-1, 0, .4, .5, 1, 2, 10])
159 | def test_finite_duration(self, func, data, dur):
160 | my_stream = func(dur)
161 | assert isinstance(my_stream, Stream)
162 | dur_int = max(rint(dur), 0)
163 | assert list(my_stream) == [data] * dur_int
164 |
165 |
166 | class TestWhiteNoise(object):
167 |
168 | def test_no_input(self):
169 | my_stream = white_noise()
170 | assert isinstance(my_stream, Stream)
171 | for el in my_stream.take(27):
172 | assert -1 <= el <= 1
173 |
174 | @p("high", [1, 0, -.042])
175 | def test_inf_input(self, high):
176 | my_stream = white_noise(inf, high=high)
177 | assert isinstance(my_stream, Stream)
178 | for el in my_stream.take(32):
179 | assert -1 <= el <= high
180 |
181 | @p("dur", [-1, 0, .4, .5, 1, 2, 10])
182 | @p("low", [0, .17])
183 | def test_finite_duration(self, dur, low):
184 | my_stream = white_noise(dur, low=low)
185 | assert isinstance(my_stream, Stream)
186 | dur_int = max(rint(dur), 0)
187 | my_list = list(my_stream)
188 | assert len(my_list) == dur_int
189 | for el in my_list:
190 | assert low <= el <= 1
191 |
192 |
193 | class TestGaussNoise(object):
194 |
195 | def test_no_input(self):
196 | my_stream = gauss_noise()
197 | assert isinstance(my_stream, Stream)
198 | assert len(my_stream.take(100)) == 100
199 |
200 | def test_inf_input(self):
201 | my_stream = gauss_noise(inf)
202 | assert isinstance(my_stream, Stream)
203 | assert len(my_stream.take(100)) == 100
204 |
205 | @p("dur", [-1, 0, .4, .5, 1, 2, 10])
206 | def test_finite_duration(self, dur):
207 | my_stream = gauss_noise(dur)
208 | assert isinstance(my_stream, Stream)
209 | dur_int = max(rint(dur), 0)
210 | my_list = list(my_stream)
211 | assert len(my_list) == dur_int
212 |
213 |
214 | class TestTableLookup(object):
215 |
216 | def test_binary_rbinary_unary(self):
217 | a = TableLookup([0, 1, 2])
218 | b = 1 - a
219 | c = b * 3
220 | assert b.table == [1, 0, -1]
221 | assert (-b).table == [-1, 0, 1]
222 | assert c.table == [3, 0, -3]
223 | assert (a + b - c).table == [-2, 1, 4]
224 |
225 | def test_sin_basics(self):
226 | assert sin_table[0] == 0
227 | assert almost_eq(sin_table(pi, phase=pi/2).take(6), [1, -1] * 3)
228 | s30 = .5 * 2 ** .5 # sin(30 degrees)
229 | assert almost_eq(sin_table(pi/2, phase=pi/4).take(12),
230 | [s30, s30, -s30, -s30] * 3)
231 | expected_pi_over_2 = [0., s30, 1., s30, 0., -s30, -1., -s30]
232 | # Assert with "diff" since it has zeros
233 | assert almost_eq.diff(sin_table(pi/4).take(32), expected_pi_over_2 * 4)
234 |
235 | def test_saw_basics(self):
236 | assert saw_table[0] == -1
237 | assert saw_table[-1] == 1
238 | assert saw_table[1] - saw_table[0] > 0
239 | data = saw_table(lag2freq(30)).take(30)
240 | first_step = data[1] - data[0]
241 | assert first_step > 0
242 | for d0, d1 in blocks(data, size=2, hop=1):
243 | assert d1 - d0 > 0 # Should be monotonically increasing
244 | assert almost_eq(d1 - d0, first_step) # Should have constant derivative
245 |
246 |
247 | class TestImpulse(object):
248 |
249 | def test_no_input(self):
250 | delta = impulse()
251 | assert isinstance(delta, Stream)
252 | assert delta.take(25) == [1.] + list(zeros(24))
253 |
254 | def test_inf_input(self):
255 | delta = impulse(inf)
256 | assert isinstance(delta, Stream)
257 | assert delta.take(17) == [1.] + list(zeros(16))
258 |
259 | def test_integer(self):
260 | delta = impulse(one=1, zero=0)
261 | assert isinstance(delta, Stream)
262 | assert delta.take(22) == [1] + [0] * 21
263 |
264 | @p("dur", [-1, 0, .4, .5, 1, 2, 10])
265 | def test_finite_duration(self, dur):
266 | delta = impulse(dur)
267 | assert isinstance(delta, Stream)
268 | dur_int = max(rint(dur), 0)
269 | if dur_int == 0:
270 | assert list(delta) == []
271 | else:
272 | assert list(delta) == [1.] + [0.] * (dur_int - 1)
273 |
--------------------------------------------------------------------------------
/docs/rst_creator.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # This file is part of AudioLazy, the signal processing Python package.
4 | # Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
5 | #
6 | # AudioLazy is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | """
18 | AudioLazy documentation reStructuredText file creator
19 |
20 | Note
21 | ----
22 | You should call make_all_docs afterwards!
23 |
24 | Warning
25 | -------
26 | Calling this OVERWRITES the RST files in the directory it's in, and don't
27 | ask for confirmation!
28 | """
29 |
30 | import os
31 | import audiolazy
32 | import re
33 | from audiolazy import iteritems
34 |
35 | # This file should be at the conf.py directory!
36 | from conf import readme_file_contents, splitter, master_doc
37 |
38 |
39 | def find_full_name(prefix, suffix="rst"):
40 | """
41 | Script path to actual path relative file name converter.
42 |
43 | Parameters
44 | ----------
45 | prefix :
46 | File name prefix (without extension), relative to the script location.
47 | suffix :
48 | File name extension (defaults to "rst").
49 |
50 | Returns
51 | -------
52 | A file name path relative to the actual location to a file
53 | inside the script location.
54 |
55 | Warning
56 | -------
57 | Calling this OVERWRITES the RST files in the directory it's in, and don't
58 | ask for confirmation!
59 |
60 | """
61 | return os.path.join(os.path.split(__file__)[0],
62 | os.path.extsep.join([prefix, suffix]))
63 |
64 |
65 | def save_to_rst(prefix, data):
66 | """
67 | Saves a RST file with the given prefix into the script file location.
68 |
69 | """
70 | with open(find_full_name(prefix), "w") as rst_file:
71 | rst_file.write(full_gpl_for_rst)
72 | rst_file.write(data)
73 |
74 |
75 | #
76 | # First chapter! Splits README.rst data into the gen_blocks iterable
77 | #
78 | readme_data = splitter(readme_file_contents)
79 | rfc_copyright = readme_data.popitem()[1] # Last block is a small license msg
80 | gen_blocks = iteritems(readme_data)
81 |
82 | # Process GPL license comment at the beginning to put it in all RST files
83 | gpl_to_add = " File auto-generated by the rst_creator.py script."
84 | full_gpl_for_rst = next(gen_blocks)[1] # It's before the first readable block
85 | full_gpl_for_rst = "\n".join(full_gpl_for_rst[:-1] + [gpl_to_add] +
86 | full_gpl_for_rst[-1:] + ["\n\n"])
87 | gpl_for_rst = "\n ".join(full_gpl_for_rst.strip()
88 | .strip(".").splitlines()[:-2])
89 |
90 | # Process the first readable block (project name and small description)
91 | rfc_name, rfc_description = next(gen_blocks) # Second block
92 | rfc_name = " ".join([rfc_name, "|version|"]) # Puts version in title ...
93 | rfc_name = [rfc_name, "=" * len(rfc_name)] # ... and the syntax of a title
94 |
95 | # Process last block to have a nice looking, and insert license file link
96 | license_file_name = "COPYING.txt"
97 | license_full_file_name = "../" + license_file_name
98 | linked_file_name = ":download:`{0} <{1}>`".format(license_file_name,
99 | license_full_file_name)
100 | rfc_copyright = ("\n ".join(el.strip() for el in rfc_copyright
101 | if el.strip() != "")
102 | .replace(license_file_name,linked_file_name)
103 | .replace("- ", "")
104 | )
105 |
106 | #
107 | # Creates a RST for each block in README.rst besides the first (small
108 | # description) and the last (license) ones, which were both already removed
109 | # from "gen_blocks"
110 | #
111 | readme_names = [] # Keep the file names
112 | for name, data in gen_blocks:
113 | fname = "".join(re.findall("[\w ]", name)).replace(" ", "_").lower()
114 |
115 | # Image location should be corrected before
116 | img_string = ".. image:: "
117 | for idx, el in enumerate(data):
118 | if el.startswith(img_string):
119 | data[idx] = el.replace(img_string, img_string + "../")
120 |
121 | save_to_rst(fname, "\n".join([name, "=" * len(name)] + data).strip())
122 | readme_names.append(fname)
123 |
124 |
125 | #
126 | # Creates the master document
127 | #
128 |
129 | # First block
130 | main_toc = """
131 | .. toctree::
132 | :maxdepth: 2
133 |
134 | intro
135 | modules
136 | """
137 | first_block = rfc_name + [""] + rfc_description + [main_toc]
138 |
139 | # Second block
140 | indices_block = """
141 | .. only:: html
142 |
143 | Indices and tables
144 | ------------------
145 |
146 | * :ref:`genindex`
147 | * :ref:`modindex`
148 | * :ref:`search`
149 | """
150 |
151 | # Saves the master document (with the TOC)
152 | index_data = "\n".join(first_block + [indices_block])
153 | save_to_rst(master_doc, index_data.strip())
154 |
155 |
156 | #
157 | # Creates the intro.rst
158 | #
159 | intro_block = """
160 | Introduction
161 | ============
162 |
163 | This is the main AudioLazy documentation, whose contents are mainly from the
164 | repository documentation and source code docstrings, tied together with
165 | `Sphinx `_. The sections below can introduce you to
166 | the AudioLazy Python DSP package.
167 |
168 | .. toctree::
169 | :maxdepth: 4
170 | {0}
171 | license
172 | """.format(("\n" + 2 * " ").join([""] + readme_names))
173 | save_to_rst("intro", intro_block.strip())
174 |
175 |
176 | #
177 | # Creates the license.rst
178 | #
179 | license_block = """
180 | License and auto-generated reST files
181 | =====================================
182 |
183 | All project files, including source and documentation, are free software,
184 | under GPLv3. This is free in the sense that the source code have to be always
185 | available to you if you ask for it, as well as forks or otherwise derivative
186 | new source codes, however stated in a far more precise way by experts in that
187 | kind of law text. That's at the same time far from the technical language from
188 | engineering, maths and computer science, and more details would be beyond the
189 | needs of this document. You should find the following information in all
190 | Python (*\*.py*) source code files and also in all reStructuredText (*\*.rst*)
191 | files:
192 |
193 | ::
194 |
195 | {0}
196 |
197 | This is so also for auto-generated reStructuredText documentation files.
198 | However, besides most reStructuredText files being generated by a script,
199 | their contents aren't auto-generated. These are spread in the source code,
200 | both in reStructuredText and Python files, organized in a way that would make
201 | the same manually written documentation be used as:
202 |
203 | + `Spyder `_ (Python IDE made for
204 | scientific purposes) *Rich Text* auto-documentation at its
205 | *Object inspector*. Docstrings were written in a reStructuredText syntax
206 | following its conventions for nice HTML rendering;
207 |
208 | + Python docstrings (besides some docstring creation like what happens in
209 | StrategyDict instances, that's really the original written data in the
210 | source);
211 |
212 | + Full documentation, thanks to `Sphinx `_, that replaces
213 | the docstring conventions to other ones for creating in the that allows
214 | automatic conversion to:
215 |
216 | - HTML
217 | - PDF (LaTeX)
218 | - ePUB
219 | - Manual pages (man)
220 | - Texinfo
221 | - Pure text files
222 |
223 | License is the same in all files that generates those documentations.
224 | Some reStructuredText files, like the README.rst that generated this whole
225 | chapter, were created manually. They're also free software as described in
226 | GPLv3. The main project repository includes a message:
227 |
228 | .. parsed-literal::
229 |
230 | {1}
231 |
232 | This should be applied to all files that belongs to the AudioLazy project.
233 | Although all the project files were up to now created and modified by a sole
234 | person, this sole person had never wanted to keep such status for so long. If
235 | you found a bug or otherwise have an issue or a patch to send, show the issue
236 | or the pull request at the
237 | `main AudioLazy repository `_,
238 | so that the bug would be fixed, or the new resource become available, not
239 | only for a few people but for everyone.
240 | """.format(gpl_for_rst, rfc_copyright)
241 | save_to_rst("license", license_block.strip())
242 |
243 |
244 | #
245 | # Second chapter! Creates the modules.rst
246 | #
247 | modules_block = """
248 | Modules Documentation
249 | =====================
250 |
251 | Below is the table of contents, with processed data from docstrings. They were
252 | made and processed in a way that would be helpful as a stand-alone
253 | documentation, but if it's your first time with this package, you should see
254 | at least the :doc:`getting_started` before these, since the
255 | full module documentation isn't written for beginners.
256 |
257 | .. toctree::
258 | :maxdepth: 4
259 | :glob:
260 |
261 | audiolazy
262 | lazy_*
263 | """
264 | save_to_rst("modules", modules_block.strip())
265 |
266 |
267 | #
268 | # Creates the RST file for the package
269 | #
270 | first_line = ":mod:`audiolazy` Package"
271 | data = [
272 | first_line,
273 | "=" * len(first_line),
274 | ".. automodule:: audiolazy",
275 | ]
276 | save_to_rst("audiolazy", "\n".join(data).strip())
277 |
278 |
279 | #
280 | # Creates the RST file for each module
281 | #
282 | for lzmodule in audiolazy.__modules__:
283 | first_line = ":mod:`{0}` Module".format(lzmodule)
284 | data = [
285 | first_line,
286 | "=" * len(first_line),
287 | ".. automodule:: audiolazy.{0}".format(lzmodule),
288 | " :members:",
289 | " :undoc-members:",
290 | " :show-inheritance:",
291 | ]
292 |
293 | # See if there's any StrategyDict in the module
294 | module_data = getattr(audiolazy, lzmodule)
295 | for memb in module_data.__all__:
296 | memb_data = getattr(module_data, memb)
297 | if isinstance(memb_data, audiolazy.StrategyDict):
298 | sline = ":obj:`{0}.{1}` StrategyDict".format(lzmodule, memb)
299 | data += [
300 | "", sline,
301 | "-" * len(sline),
302 | ".. automodule:: audiolazy.{0}.{1}".format(lzmodule, memb),
303 | " :members:",
304 | " :undoc-members:",
305 | ]
306 |
307 | save_to_rst(lzmodule, "\n".join(data).strip())
308 |
--------------------------------------------------------------------------------