├── 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 | --------------------------------------------------------------------------------