├── .DS_Store
├── .gitignore
├── CQT.py
├── Constant-Q_transform_toolbox_for_music_processing.pdf
├── LICENSE
├── MATLAB
├── DEMO.m
├── cell2sparse.m
├── cqt.m
├── cqtPerfectRast.m
├── genCQTkernel.m
├── getCQT.m
├── icqt.m
├── plotCQT.m
└── sparse2cell.m
├── README.md
└── demo.dat
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iphysresearch/CQT_toolbox_python/950a923dfa7640eb67bbe044e7189cc34e3afdd9/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
--------------------------------------------------------------------------------
/CQT.py:
--------------------------------------------------------------------------------
1 | import warnings
2 |
3 | import numpy as np
4 | from decimal import Decimal, ROUND_HALF_UP
5 |
6 | from scipy.signal import butter, blackman, blackmanharris, upfirdn, filtfilt
7 | from scipy import sparse
8 | from scipy.interpolate import interp1d
9 |
10 | import matplotlib.pyplot as plt
11 |
12 |
13 |
14 | def upsample(s, n, phase=0):
15 | """Increase sampling rate by integer factor n with included offset phase.
16 | """
17 | return np.roll(np.kron(s, np.r_[1, np.zeros(n-1)]), phase)
18 |
19 |
20 | def buffer(x, n, p=0, opt=None):
21 | '''
22 | Buffer function like MATLAB. (https://www.mathworks.com/help/signal/ref/buffer.html?searchHighlight=buffer&s_tid=srchtitle)
23 |
24 | Ref: https://stackoverflow.com/a/60020929 (Your can check more buffer algorithms from this)
25 | About buffer function in Python, see also: https://github.com/aaravindravi/python-signal-segmentation/blob/master/buffer.py
26 | '''
27 | if opt not in ('nodelay', None):
28 | raise ValueError('{} not implemented'.format(opt))
29 |
30 | i = 0
31 | if opt == 'nodelay':
32 | # No zeros at array start
33 | result = x[:n]
34 | i = n
35 | else:
36 | # Start with `p` zeros
37 | result = np.hstack([np.zeros(p), x[:n-p]])
38 | i = n-p
39 | # Make 2D array, cast to list for .append()
40 | result = list(np.expand_dims(result, axis=0))
41 |
42 | while i < len(x):
43 | # Create next column, add `p` results from last col if given
44 | col = x[i:i+(n-p)]
45 | if p != 0:
46 | col = np.hstack([result[-1][-p:], col])
47 |
48 | # Append zeros if last row and not length `n`
49 | if len(col):
50 | col = np.hstack([col, np.zeros(n - len(col))])
51 |
52 | # Combine result with next row
53 | result.append(np.array(col))
54 | i += (n - p)
55 |
56 | return np.vstack(result).T
57 |
58 | def cqt(x, fmin, fmax, bins, fs, **kwargs):
59 | '''
60 | %Xcqt = cqt(x,fmin,fmax,bins,fs,varargin) calculates the constant-Q transform of the input signal x.
61 | %
62 | %INPUT:
63 | % fmin ... lowest frequency of interest
64 | % fmax ... highest frequency of interest
65 | % bins ... frequency bins per octave
66 | % fs ... sampling rate
67 | %
68 | % optional input parameters (parameter name/value pairs):
69 | %
70 | % 'atomHopFactor' ... overlap of temporal atoms in percent. Default: 0.25.
71 | %
72 | % 'q' ... the maximum value for optimal reconstruction is q=1.
73 | % For values smaller than 1 the bandwidths of the spectral
74 | % atoms (filter) are increased retaining their center
75 | % frequencies (frequency 'smearing', frequency domain redundancy
76 | % increases, time resolutin improves). Default: 1.
77 | % 'thresh' ... all values in the cqt kernel smaller than tresh are
78 | % rounded to zero. A high value for thresh yields a
79 | % very sparse kernel (fast) but introduces a bigger error.
80 | % The default value is chosen so that the error due to rounding is negligible.
81 | % 'kernel' ... if the cqt kernel structure has been precomputed
82 | % (using function 'genCQTkernel'), the computation of the kernel
83 | % will be by-passed below).
84 | % 'win' ... defines which window will be used for the CQT. Valid
85 | % values are: 'blackman','hann' and 'blackmanharris'. To
86 | % use the square root of each window use the prefix 'sqrt_'
87 | % (i.e. 'sqrt_blackman'). Default: 'sqrt_blackmanharris'
88 | % 'coeffB',
89 | % 'coeffA' ... Filter coefficients for the anti-aliasing filter, where
90 | % 'coeffB' is the numerator and 'coeffA' is the
91 | % denominator (listed in descending powers of z).
92 | %
93 | %OUTPUT:
94 | % Xcqt ... struct that comprises various fields:
95 | % spCQT: CQT coefficients in the form of a sparse matrix
96 | % (rasterized, not interpolated)
97 | % fKernel: spectral Kernel
98 | % fmin: frequency of the lowest bin
99 | % fmax: frequency of the hiqhest bin
100 | % octaveNr: number of octaves processed
101 | % bins: number of bins per octave
102 | % intParams: structure containing additional parameters for the inverse transform
103 | %He Wang, 2020/12/01 hewang@mail.bnu.edu.cn
104 | %'''
105 |
106 | # input checking
107 | if (len(x.shape) > 1) and (x.shape[0] > 1):
108 | warnings.warn('qt requires one-dimensional input!', UserWarning)
109 | if (len(x.shape) > 1):
110 | x = x.reshape(-1) # column vector
111 |
112 | # input parameters
113 | q = kwargs.get('q', 1)
114 | atomHopFactor = kwargs.get('atomHopFactor', 0.25)
115 | thresh = kwargs.get('thresh', 0.0005)
116 | cqtKernel = kwargs.get('kernel')
117 | winFlag = kwargs.get('win', 'sqrt_blackmanharris')
118 | B = kwargs.get('coeffB')
119 | A = kwargs.get('coeffA')
120 |
121 | # define
122 | octaveNr = int(np.ceil(np.log2(fmax/fmin)))
123 | fmin = (fmax/2**octaveNr) * 2**(1/bins) # set fmin to actual value
124 | xlen_init = x.size
125 |
126 |
127 | # design lowpass filter
128 | if (not B) or (not A):
129 | LPorder = 6 # order of the anti-aliasing filter
130 | cutoff = 0.5
131 | B, A = butter(LPorder, cutoff, 'low') # design f_nyquist/2-lowpass filter
132 |
133 | # design kernel for one octave
134 | if not cqtKernel:
135 | cqtKernel = genCQTkernel(fmax, bins, fs, q=q, atomHopFactor=atomHopFactor, thresh=thresh, win=winFlag)
136 |
137 | # calculate CQT
138 | cellCQT = {} #np.zeros(octaveNr)
139 | maxBlock = cqtKernel['fftLEN'] * 2**(octaveNr-1) # largest FFT Block (virtual)
140 | suffixZeros = maxBlock
141 | prefixZeros = maxBlock
142 | x = np.pad(x, (suffixZeros, prefixZeros), 'constant', constant_values=(0, 0))
143 | OVRLP = int(cqtKernel['fftLEN'] - cqtKernel['fftHOP'])
144 | K = cqtKernel['fKernel'].conj().T # %conjugate spectral kernel for cqt transformation
145 |
146 | for i in range(1, octaveNr+1):
147 |
148 | xx = buffer(x, cqtKernel['fftLEN'], OVRLP, 'nodelay') # generating FFT blocks
149 | XX = np.fft.fft(xx.T).T # applying fft to each column (each FFT frame)
150 | cellCQT[i] = np.dot(K, XX) # calculating cqt coefficients for all FFT frames for this octave
151 |
152 | if i != octaveNr:
153 | x = filtfilt(B, A, x) # anti aliasing filter
154 | x = x[::2] # drop samplerate by 2
155 |
156 | spCQT = cell2sparse(cellCQT, octaveNr, bins, cqtKernel['firstcenter'], cqtKernel['atomHOP'], cqtKernel['atomNr'])
157 |
158 | intParam = {'sufZeros':suffixZeros,'preZeros':prefixZeros,'xlen_init':xlen_init,'fftLEN':cqtKernel['fftLEN'],'fftHOP':cqtKernel['fftHOP'],
159 | 'q':q,'filtCoeffA':A,'filtCoeffB':B,'firstcenter':cqtKernel['firstcenter'],'atomHOP':cqtKernel['atomHOP'],
160 | 'atomNr':cqtKernel['atomNr'],'Nk_max':cqtKernel['Nk_max'],'Q':cqtKernel['Q'],'rast':0}
161 |
162 | Xcqt = {'spCQT':spCQT,'fKernel':cqtKernel['fKernel'],'fmax':fmax,'fmin':fmin,'octaveNr':octaveNr,'bins':cqtKernel['bins'],'intParams':intParam}
163 |
164 | return Xcqt
165 |
166 | def round_half_up(number, ndigits=0): # 精确的四舍五入
167 | '''
168 | Ref: https://cloud.tencent.com/developer/article/1426211
169 | '''
170 | num = Decimal(str(number))
171 | return float(num.quantize(Decimal('0.'+'0'*ndigits), rounding=ROUND_HALF_UP))
172 |
173 | def nextpow2(n):
174 | '''
175 | 求最接近数据长度的2的整数次方
176 | An integer equal to 2 that is closest to the length of the data
177 |
178 | Ref: https://github.com/BIDS-Apps/rsHRF/blob/669ceac0e347224fbce2ae5f7d99adbe2725d2db/rsHRF/processing/rest_filter.py#L6
179 |
180 | Eg:
181 | nextpow2(2) = 1
182 | nextpow2(2**10+1) = 11
183 | nextpow2(2**20+1) = 21
184 | '''
185 | return np.ceil(np.log2(np.abs(n))).astype('long')
186 |
187 | def hann(window_length, sflag='symmetric'):
188 | """
189 | Returns a Hann window using the window sampling specified by `sflag.
190 |
191 | Args:
192 |
193 | window_length: The number of points in the returned window.
194 | sflag: Window sampling - 'symmetric' (default) | 'periodic'
195 | Window sampling, specified as one of the following:
196 | 'symmetric' — Use this option when using windows for filter design.
197 | The Hanning window is defined as
198 | .. math::
199 | w(n) = 0.5 - 0.5\\cos\\left(\\frac{2\\pi{n}}{M-1}\\right)
200 | \\qquad 0 \\leq n \\leq M-1
201 |
202 | 'periodic' — This option is useful for spectral analysis because it
203 | enables a windowed signal to have the perfect periodic extension
204 | implicit in the discrete Fourier transform. When 'periodic' is specified,
205 | hann computes a window of length L + 1 and returns the first L points.
206 |
207 | Calculate a "periodic" Hann window.
208 |
209 | The classic Hann window is defined as a raised cosine that starts and
210 | ends on zero, and where every value appears twice, except the middle
211 | point for an odd-length window. Matlab calls this a "symmetric" window
212 | and np.hanning() returns it. However, for Fourier analysis, this
213 | actually represents just over one cycle of a period N-1 cosine, and
214 | thus is not compactly expressed on a length-N Fourier basis. Instead,
215 | it's better to use a raised cosine that ends just before the final
216 | zero value - i.e. a complete cycle of a period-N cosine. Matlab
217 | calls this a "periodic" window. This routine calculates it.
218 |
219 | Returns:
220 | A 1D np.array containing the periodic hann window.
221 |
222 | Ref: http://ddrv.cn/a/272066
223 | He Wang, 2020/12/01 hewang@mail.bnu.edu.cn
224 | """
225 | if sflag == 'symmetric':
226 | return np.hanning(window_length)
227 | elif sflag == 'periodic':
228 | return 0.5 - (0.5 * np.cos(2 * np.pi / window_length *
229 | np.arange(window_length)))
230 | else:
231 | raise
232 |
233 | def genCQTkernel(fmax, bins, fs, **kwargs):
234 | '''
235 | %Calculating the CQT Kernel for one octave. All atoms are center-stacked.
236 | %Atoms are placed so that the stacks of lower octaves are centered at the
237 | %same positions in time, however, their amount is reduced by factor two for
238 | %each octave down.
239 | %
240 | %INPUT:
241 | % fmax ... highest frequency of interest
242 | % bins ... number of bins per octave
243 | % fs ... sampling frequency
244 | %
245 | %optional input parameters (parameter name/value pairs):
246 | %
247 | % 'q' ... Q scaling factor. Default: 1.
248 | % 'atomHopFactor' ... relative hop size corresponding to the shortest
249 | % temporal atom. Default: 0.25.
250 | % 'thresh' ... values smaller than 'tresh' in the spectral kernel are rounded to
251 | % zero. Default: 0.0005.
252 | % 'win' ... defines which window will be used for the CQT. Valid
253 | % values are: 'blackman','hann' and 'blackmanharris'. To
254 | % use the square root of each window use the prefix 'sqrt_'
255 | % (i.e. 'sqrt_blackman'). Default: 'sqrt_blackmanharris'
256 | % 'perfRast' ... if set to 1 the kernel is designed in order to
257 | % enable perfect rasterization using the function
258 | % cqtPerfectRast() (Default: perRast=0). See documentation of
259 | % 'cqtPerfectRast' for further information.
260 | %
261 | %OUTPUT:
262 | % cqtKernel ... Dict that contains the spectral kernel 'fKernel'
263 | % additional design parameters used in cqt(), cqtPerfectRast() and icqt().
264 | %
265 | %He Wang, 2020/12/01 hewang@mail.bnu.edu.cn
266 | '''
267 | # input parameters
268 | q = kwargs.get('q', 1)
269 | atomHopFactor = kwargs.get('atomHopFactor', 0.25)
270 | thresh = kwargs.get('thresh', 0.0005)
271 | winFlag = kwargs.get('win', 'sqrt_blackmanharris')
272 | perfRast = kwargs.get('perfRast', 0)
273 |
274 | # define
275 | fmin = (fmax/2)*2**(1/bins)
276 | Q = 1/(2**(1/bins)-1)
277 | Q = Q*q
278 | Nk_max = Q * fs / fmin
279 | Nk_max = round_half_up(Nk_max) # length of the largest atom [samples]
280 |
281 | # Compute FFT size, FFT hop, atom hop, ...
282 | Nk_min = round_half_up( Q * fs / (fmin*2**((bins-1)/bins)) ) # length of the shortest atom [samples]
283 | atomHOP = round_half_up(Nk_min * atomHopFactor) # atom hop size
284 | first_center = np.ceil(Nk_max/2) # first possible center position within the frame
285 | first_center = atomHOP * np.ceil(first_center/atomHOP) # lock the first center to an integer multiple of the atom hop size
286 | FFTLen = 2**nextpow2(first_center+np.ceil(Nk_max/2)) # use smallest possible FFT size (increase sparsity)
287 |
288 | if perfRast:
289 | winNr = int(np.floor((FFTLen-np.ceil(Nk_max/2)-first_center)/atomHOP)) # number of temporal atoms per FFT Frame
290 | if winNr == 0 :
291 | FFTLen = FFTLen * 2
292 | winNr = int(np.floor((FFTLen-np.ceil(Nk_max/2)-first_center)/atomHOP))
293 | else:
294 | winNr = int(np.floor((FFTLen-np.ceil(Nk_max/2)-first_center)/atomHOP))+1 # number of temporal atoms per FFT Frame
295 |
296 |
297 | last_center = first_center + (winNr-1)*atomHOP
298 | fftHOP = (last_center + atomHOP) - first_center # hop size of FFT frames
299 | fftOLP = (FFTLen-fftHOP/FFTLen)*100 # overlap of FFT frames in percent ***AK:needed?
300 |
301 | # init variables
302 | tempKernel= np.zeros((1, FFTLen), dtype=complex)
303 | sparKernel= np.zeros((1, FFTLen), dtype=complex)#[]
304 |
305 | # Compute kernel
306 | atomInd = 0
307 | for k in range(bins):
308 |
309 | Nk = int(round_half_up( Q * fs / (fmin*2**(k/bins)) )) # N[k] = (fs/fk)*Q. Rounding will be omitted in future versions
310 |
311 | if winFlag == 'sqrt_blackmanharris':
312 | winFct = np.sqrt(blackmanharris(Nk))
313 | elif winFlag == 'blackmanharris':
314 | winFct = blackmanharris(Nk)
315 | elif winFlag == 'sqrt_hann':
316 | winFct = np.sqrt(hann(Nk, 'periodic'))
317 | elif winFlag == 'hann':
318 | winFct = hann(Nk, 'periodic')
319 | elif winFag == 'sqrt_blackman':
320 | winFct = np.sqrt(blackman(Nk, False))
321 | elif winFag == 'blackman':
322 | winFct = blackman(Nk, False)
323 | else:
324 | winFct = np.sqrt(blackmanharris(Nk))
325 | if k==1:
326 | warnings.warn('QT:INPUT','Non-existing window function. Default window is used!', UserWarning)
327 |
328 | fk = fmin*2**(k/bins)
329 | tempKernelBin = (winFct/Nk) * np.exp(2*np.pi*1j*fk*np.arange(Nk)/fs)
330 | atomOffset = first_center - np.ceil(Nk/2)
331 |
332 | for i in range(winNr):
333 | shift = int(atomOffset + (i * atomHOP))
334 |
335 | tempKernel[:, shift: Nk+shift] = tempKernelBin
336 |
337 | atomInd += 1
338 | specKernel= np.fft.fft(tempKernel)
339 | specKernel[abs(specKernel)<=thresh] = 0
340 | sparKernel = np.append(sparKernel, specKernel, axis=0)
341 | tempKernel = np.zeros((1, FFTLen), dtype=complex) # reset window
342 |
343 |
344 | sparKernel = (sparKernel.T/FFTLen)[:,1:]
345 |
346 | # Normalize the magnitudes of the atoms
347 | wx1=np.argmax(np.abs(sparKernel)[:,0])
348 | wx2=np.argmax(np.abs(sparKernel)[:,-1])
349 | wK=sparKernel[wx1: wx2+1,:]
350 |
351 | wK = np.diag(np.dot(wK, wK.conj().T))
352 | wK = wK[int(round_half_up(1/q)): -int(round_half_up(1/q))-2]
353 | weight = 1./np.mean(np.abs(wK))
354 | weight = weight * (fftHOP/FFTLen)
355 | weight = np.sqrt(weight) # sqrt because the same weight is applied in icqt again
356 | sparKernel = weight*sparKernel
357 |
358 | return {'fKernel': sparKernel, 'fftLEN':FFTLen,'fftHOP':fftHOP,'fftOverlap':fftOLP,'perfRast':perfRast,
359 | 'bins':bins,'firstcenter':first_center,'atomHOP':atomHOP,'atomNr':winNr,'Nk_max':Nk_max,'Q':Q,'fmin':fmin }
360 |
361 | def cell2sparse(Xcq, octaves, bins, firstcenter, atomHOP, atomNr):
362 | '''
363 | %Generates a sparse matrix containing the CQT coefficients (rasterized).
364 | %
365 | %The sparse matrix representation of the CQT coefficients contains all
366 | %computed coefficients at the corresponding time-frequency location
367 | %(similar to a spectrogram). For lower frequencies this means, that
368 | %each coefficient is followed by zeros stemming from the fact, that the time
369 | %resolution for lower frequencies decreases as the frequency resolution
370 | %increases. Due to the design of the CQT kernel, however, the coefficients
371 | %of different octaves are synchronised, meaning that for the second highest
372 | %octave each coefficient is followed by one zero, for the next octave down
373 | %two zeros are inserted, for the next octave four zeros are inserted and so
374 | %on.
375 | %
376 | %INPUT:
377 | % Xcq ... Dict array consisting of all coefficients for all octaves
378 | % octaves ... Number of octaves processed
379 | % bins ... Number of bins per octave
380 | % firstcenter ... Location of the leftmost atom-stack in the temporal
381 | % kernel
382 | % atomHOP ... Spacing of two consecutive atom stacks
383 | % atomNr ... Number of atoms per bin within the kernel
384 | %
385 | %%He Wang, 2020/12/01 hewang@mail.bnu.edu.cn
386 | '''
387 |
388 |
389 |
390 | # this version uses less memory but is noticable slower
391 | emptyHops = firstcenter/atomHOP
392 | drops = emptyHops*np.power(2, octaves - np.arange(1, octaves + 1)) - emptyHops
393 | Len = int(np.max((np.asarray([atomNr*c.shape[1] for _,c in Xcq.items()]) - drops) * np.power(2, np.arange(octaves)))) # number of columns of output matrix
394 | spCQT = np.empty((0,Len)).astype(np.complex)
395 |
396 | for i in range(1, octaves+1)[::-1]:
397 | drop = int(emptyHops*2**(octaves-i)-emptyHops) # first coefficients of all octaves have to be in synchrony
398 | X = Xcq[i]
399 |
400 | if atomNr > 1: # more than one atom per bin --> reshape
401 | Xoct = np.zeros((bins, atomNr*X.shape[1] - drop)).astype(np.complex)
402 | for u in range(bins): # reshape to continous windows for each bin (for the case of several wins per frame)
403 | octX_bin = X[u*atomNr:(u+1)*atomNr,:]
404 | Xcont = octX_bin.T.reshape(-1)
405 | Xoct[u,:] = Xcont[drop:]
406 | X = Xoct
407 |
408 | else:
409 | X = X[:,drop:]
410 |
411 | X = np.pad(upfirdn([1], X.T, 2**(i-1), axis=0), [[0,2**(i-1)-1],[0,0]], mode='constant').T # upfirdn: upsampling with zeros insertion
412 | X = np.append(X, np.zeros((bins, Len-X.shape[1])), axis=1)
413 |
414 | spCQT = np.append(spCQT, X, axis=0)
415 |
416 | return spCQT
417 |
418 | def getCQT(Xcqt, fSlice, tSlice, iFlag):
419 | '''
420 | %outCQ = getCQT(Xcqt,fSlice,tSlice,iFlag) computes a rasterized representation of
421 | %the amplitudes of the calculated CQT coefficients for the frequency bins definded in vector fSlice and the
422 | %points in time (time frames) defined in vector tSlice using the interpolation method defined in iFlag.
423 | %Valid values for iFlag are:
424 | %
425 | %'linear' ... linear interpolation (default)
426 | %'spline' ... spline interpolation
427 | %'nearest' ... nearest neighbor interpolation
428 | %'cubic' ... piecewise cubic interpolation
429 | %
430 | %If the entire CQT representation should be rasterized, set fSlice and
431 | %tSlice to 'all'.
432 | %The input parameter Xcqt is the structure gained using cqt(...).
433 | %The output parameter 'intCQT' is the same size as Xcqt.spCQT but is no
434 | %longer sparse since the zeros between two coefficients are replaced by
435 | %the interpolated values. The coefficients stored in 'intCQT' are now
436 | %real-valued since only the absolute values of the coefficients are
437 | %interpolated. If a spectrogram-like (rasterized) version of the CQT
438 | %coefficients including phase information is required, use the function
439 | %cqtPerfectRast() (see documentation for further information)
440 | %
441 | %%He Wang, 2020/12/01 hewang@mail.bnu.edu.cn
442 | '''
443 | if type(fSlice) == type(''):
444 | fSlice = np.arange(Xcqt['bins'] * Xcqt['octaveNr'])
445 | if type(tSlice) == type(''):
446 | lastEnt = Xcqt['spCQT'][0,:].nonzero()[0][-1]
447 | tSlice = range(lastEnt)
448 |
449 |
450 | intCQT = np.zeros((len(fSlice),len(tSlice)))
451 | bins = Xcqt['bins']
452 | spCQT = Xcqt['spCQT']
453 | octaveNr = Xcqt['octaveNr']
454 | spCQT = spCQT.T
455 |
456 | for k in range(len(fSlice)):
457 | Oct = octaveNr-np.floor((fSlice[k]-0.1)/bins)
458 | stepVec = range(0, spCQT.shape[0], int(2**(Oct-1)))
459 | Xbin = spCQT[stepVec, fSlice[k]]
460 | intCQT[k,:] = np.interp(tSlice, stepVec, abs(Xbin))
461 | return intCQT
462 |
463 | def plotCQT(Xcqt, fs, fcomp):
464 | '''
465 | %plotCQT(Xcqt,fs,fcomp,method) plots the magnitudes of the CQT
466 | %coefficients similar to a spectrogram using linear interpolation
467 | %between the calculated coefficients. For better illustration, the
468 | %magnitude values can be compressed using fcomp < 1 (Xmag^fcomp).
469 | %
470 | %%He Wang, 2020/12/13 hewang@mail.bnu.edu.cn
471 | '''
472 | if Xcqt['intParams']['rast']:
473 | absCQT = np.abs(Xcqt['spCQT'])
474 | else:
475 | absCQT = getCQT(Xcqt, 'all', 'all', 'linear')
476 |
477 |
478 | emptyHops = Xcqt['intParams']['firstcenter'] / Xcqt['intParams']['atomHOP']
479 | maxDrop = emptyHops * 2**(Xcqt['octaveNr']-1)-emptyHops
480 | droppedSamples = (maxDrop-1) * Xcqt['intParams']['atomHOP'] + Xcqt['intParams']['firstcenter']
481 | outputTimeVec = np.arange(1, absCQT.shape[1]+1) * Xcqt['intParams']['atomHOP']-Xcqt['intParams']['preZeros']+droppedSamples
482 |
483 | xout = outputTimeVec / fs
484 | ytickarray = np.arange(1, Xcqt['octaveNr']*Xcqt['bins'], Xcqt['bins']/2)
485 | yout = Xcqt['fmin'] * 2**((ytickarray-1)/Xcqt['bins'])
486 | yTickLabel = Xcqt['fmin']*2**((ytickarray-1)/Xcqt['bins'])
487 |
488 | X_cq_rast = absCQT**fcomp # compress magnitudes
489 | fig, ax = plt.subplots(1, 1, sharex=True)
490 | ax.imshow(abs(X_cq_rast), origin='lower', vmin=np.min(X_cq_rast), vmax=np.max(X_cq_rast),)
491 | ax.axis('tight')
492 | ax.set_xlabel('time [sec]')
493 | ax.set_ylabel('frequency [Hz]')
494 | ax.set_title('Constant Q transform', fontdict={'fontsize':12})
495 | plt.yticks(ytickarray, [int(round_half_up(i)) for i in yTickLabel])
496 | fsp = int(1/(xout[1] - xout[0]))
497 | plt.xticks(np.arange(xout.size)[fsp//2::fsp], np.round(xout[fsp//2::fsp],1))
498 | return (ytickarray, yout), xout
499 |
500 | def sparse2cell(spCQT,bins,octaveNr,atomNr,firstcenter,atomHOP):
501 | '''
502 | % Maps the sparse matrix respresentation of the CQT coefficients back to
503 | % the cell representation for inverse transform
504 | %
505 | %%He Wang, 2020/12/13 hewang@mail.bnu.edu.cn
506 | '''
507 | emptyHops = firstcenter/atomHOP # void atom hopsizes in the beginning of the temporal kernel
508 | cellCQT = {}
509 |
510 | for i in range(1, octaveNr+1):
511 | dropped = int(emptyHops*2**(octaveNr-i)-emptyHops)
512 | X = spCQT[bins*octaveNr-i*bins:bins*octaveNr-(i-1)*bins,::2**(i-1)]
513 | X = np.concatenate([np.zeros((bins,dropped)), X], axis=-1)
514 | X = np.concatenate([X, np.zeros((bins,int(np.ceil(X.shape[1]/atomNr))*atomNr-X.shape[1]))], axis=-1)
515 | if atomNr > 1: # reshape
516 | Xcell = np.zeros((bins*atomNr,int(np.ceil(X.shape[1]/atomNr)))).astype(np.complex)
517 | for u in range(1,bins+1):
518 | Xbin = np.reshape(X[u-1,:], (atomNr,int(len(X[u-1,:])/atomNr)), order='F')
519 | Xcell[(u-1)*atomNr:u*atomNr, :] = Xbin
520 | cellCQT[i] = Xcell
521 | else:
522 | cellCQT[i] = X
523 |
524 | return cellCQT
525 |
526 |
527 |
528 | def icqt(Xcqt):
529 | '''
530 | %y = icqt(Xcqt) computes the inverse CQT of the CQT coefficients in Xcqt.spCQT
531 | %
532 | %The input structue Xcqt is the structure gained by cqt() and cqtPerfectRast(), respectively.
533 | %If the CQT coefficients in Xcqt.spCQT are not changed, the output y is the
534 | %reconstructed (near-perfect) time-domain signal of the input signal x
535 | %(cqt(x,...)) withing the frequency range [fmin fmax].
536 | %
537 | %%He Wang, 2020/12/13 hewang@mail.bnu.edu.cn
538 | '''
539 | cellCQT = sparse2cell(Xcqt['spCQT'],Xcqt['bins'],Xcqt['octaveNr'],Xcqt['intParams']['atomNr'],
540 | Xcqt['intParams']['firstcenter'],Xcqt['intParams']['atomHOP'])
541 |
542 | FFTLen = Xcqt['intParams']['fftLEN']
543 | octaveNr = Xcqt['octaveNr']
544 | HOPSZ = int(Xcqt['intParams']['fftHOP'])
545 |
546 | # Kernel for inverse transform
547 | Kinv = Xcqt['fKernel']
548 |
549 | # inverse transform
550 | y = np.array([])
551 |
552 | for i in np.arange(1,octaveNr+1,1)[::-1]:
553 | cellCQT_oct = cellCQT[i]
554 | Y = np.dot(Kinv, cellCQT_oct) # compute spectrum of reconstructed signal for all coefficients in this octave
555 | y_oct_temp = np.fft.ifft(Y.T).T
556 | y_oct = 2*np.real(y_oct_temp) # Y contains no negative frequencies -> keep only real part*2 to
557 | # reconstruct real valued time signal
558 | NBLOCKS = Y.shape[1]
559 | siglen = int(FFTLen + (NBLOCKS-1)*HOPSZ)
560 | y = np.append(y, np.zeros((siglen-len(y),))) if siglen-len(y)>0 else y
561 |
562 | for n in range(NBLOCKS):
563 | y[n*HOPSZ:(n*HOPSZ)+FFTLen] = y_oct[:,n] + y[n*HOPSZ:(n*HOPSZ)+FFTLen] # overlap-add
564 |
565 | if i != 1: # upsampling by factor two
566 | #y = sum(map(lambda x: [x, 0], y), []) # insert one zero between each sample
567 | y = upsample(y, 2) # insert one zero between each sample
568 | y = filtfilt(Xcqt['intParams']['filtCoeffB'], Xcqt['intParams']['filtCoeffA'],y)
569 | y *= 2
570 | return y
571 |
--------------------------------------------------------------------------------
/Constant-Q_transform_toolbox_for_music_processing.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iphysresearch/CQT_toolbox_python/950a923dfa7640eb67bbe044e7189cc34e3afdd9/Constant-Q_transform_toolbox_for_music_processing.pdf
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 He Wang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MATLAB/DEMO.m:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | /DEMO.m - Constant-Q Transform Toolbox - Sound Software .ac.uk
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
41 |
42 |
73 |
74 |
75 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
To check out this repository please hg clone
the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
86 |
87 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
统计
107 |
108 |
Download as Zip
109 |
110 |
119 |
120 |
121 |
122 |
123 |
root
124 | / DEMO.m
125 |
126 |
127 |
128 |
129 |
130 |
131 | 历史记录 |
132 | 查看 |
133 | 追溯 |
134 | 下载
135 | (1.898 KB)
136 |
137 |
138 |
139 |
140 |
587 |
588 |
589 |
590 |
591 |
592 |
593 |
594 |
595 |
596 |
载入中...
597 |
598 |
599 |
604 |
605 |
606 |
607 |
608 |
609 |
--------------------------------------------------------------------------------
/MATLAB/cell2sparse.m:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | /cell2sparse.m - Constant-Q Transform Toolbox - Sound Software .ac.uk
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
41 |
42 |
73 |
74 |
75 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
To check out this repository please hg clone
the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
86 |
87 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
统计
107 |
108 |
Download as Zip
109 |
110 |
119 |
120 |
121 |
122 |
123 |
128 |
129 |
130 |
131 | 历史记录 |
132 | 查看 |
133 | 追溯 |
134 | 下载
135 | (3.512 KB)
136 |
137 |
138 |
139 |
140 |
848 |
849 |
850 |
851 |
852 |
853 |
854 |
855 |
856 |
857 |
载入中...
858 |
859 |
860 |
865 |
866 |
867 |
868 |
869 |
870 |
--------------------------------------------------------------------------------
/MATLAB/cqt.m:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | /cqt.m - Constant-Q Transform Toolbox - Sound Software .ac.uk
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
41 |
42 |
73 |
74 |
75 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
To check out this repository please hg clone
the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
86 |
87 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
统计
107 |
108 |
Download as Zip
109 |
110 |
119 |
120 |
121 |
122 |
123 |
root
124 | / cqt.m
125 |
126 |
127 |
128 |
129 |
130 |
131 | 历史记录 |
132 | 查看 |
133 | 追溯 |
134 | 下载
135 | (5.275 KB)
136 |
137 |
138 |
139 |
140 |
1190 |
1191 |
1192 |
1193 |
1194 |
1195 |
1196 |
1197 |
1198 |
1199 |
载入中...
1200 |
1201 |
1202 |
1207 |
1208 |
1209 |
1210 |
1211 |
1212 |
--------------------------------------------------------------------------------
/MATLAB/genCQTkernel.m:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | /genCQTkernel.m - Constant-Q Transform Toolbox - Sound Software .ac.uk
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
41 |
42 |
73 |
74 |
75 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
To check out this repository please hg clone
the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
86 |
87 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
统计
107 |
108 |
Download as Zip
109 |
110 |
119 |
120 |
121 |
122 |
123 |
128 |
129 |
130 |
131 | 历史记录 |
132 | 查看 |
133 | 追溯 |
134 | 下载
135 | (5.476 KB)
136 |
137 |
138 |
139 |
140 |
1361 |
1362 |
1363 |
1364 |
1365 |
1366 |
1367 |
1368 |
1369 |
1370 |
载入中...
1371 |
1372 |
1373 |
1378 |
1379 |
1380 |
1381 |
1382 |
1383 |
--------------------------------------------------------------------------------
/MATLAB/getCQT.m:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | /getCQT.m - Constant-Q Transform Toolbox - Sound Software .ac.uk
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
41 |
42 |
73 |
74 |
75 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
To check out this repository please hg clone
the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
86 |
87 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
统计
107 |
108 |
Download as Zip
109 |
110 |
119 |
120 |
121 |
122 |
123 |
root
124 | / getCQT.m
125 |
126 |
127 |
128 |
129 |
130 |
131 | 历史记录 |
132 | 查看 |
133 | 追溯 |
134 | 下载
135 | (1.741 KB)
136 |
137 |
138 |
139 |
140 |
569 |
570 |
571 |
572 |
573 |
574 |
575 |
576 |
577 |
578 |
载入中...
579 |
580 |
581 |
586 |
587 |
588 |
589 |
590 |
591 |
--------------------------------------------------------------------------------
/MATLAB/icqt.m:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | /icqt.m - Constant-Q Transform Toolbox - Sound Software .ac.uk
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
41 |
42 |
73 |
74 |
75 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
To check out this repository please hg clone
the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
86 |
87 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
统计
107 |
108 |
Download as Zip
109 |
110 |
119 |
120 |
121 |
122 |
123 |
root
124 | / icqt.m
125 |
126 |
127 |
128 |
129 |
130 |
131 | 历史记录 |
132 | 查看 |
133 | 追溯 |
134 | 下载
135 | (1.735 KB)
136 |
137 |
138 |
139 |
140 |
542 |
543 |
544 |
545 |
546 |
547 |
548 |
549 |
550 |
551 |
载入中...
552 |
553 |
554 |
559 |
560 |
561 |
562 |
563 |
564 |
--------------------------------------------------------------------------------
/MATLAB/plotCQT.m:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | /plotCQT.m - Constant-Q Transform Toolbox - Sound Software .ac.uk
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
41 |
42 |
73 |
74 |
75 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
To check out this repository please hg clone
the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
86 |
87 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
统计
107 |
108 |
Download as Zip
109 |
110 |
119 |
120 |
121 |
122 |
123 |
root
124 | / plotCQT.m
125 |
126 |
127 |
128 |
129 |
130 |
131 | 历史记录 |
132 | 查看 |
133 | 追溯 |
134 | 下载
135 | (1.77 KB)
136 |
137 |
138 |
139 |
140 |
550 |
551 |
552 |
553 |
554 |
555 |
556 |
557 |
558 |
559 |
载入中...
560 |
561 |
562 |
567 |
568 |
569 |
570 |
571 |
572 |
--------------------------------------------------------------------------------
/MATLAB/sparse2cell.m:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | /sparse2cell.m - Constant-Q Transform Toolbox - Sound Software .ac.uk
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
41 |
42 |
73 |
74 |
75 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
To check out this repository please hg clone
the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
86 |
87 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
统计
107 |
108 |
Download as Zip
109 |
110 |
119 |
120 |
121 |
122 |
123 |
128 |
129 |
130 |
131 | 历史记录 |
132 | 查看 |
133 | 追溯 |
134 | 下载
135 | (979 Bytes)
136 |
137 |
138 |
139 |
140 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
载入中...
399 |
400 |
401 |
406 |
407 |
408 |
409 |
410 |
411 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Constant-Q Transform Toolbox for Python/MATLAB
2 |
3 | ## Introduction
4 |
5 | A Python/MATLAB reference implementation of a computationally efficient method for computing the constant-Q transform (CQT) of a time-domain signal.
6 |
7 | Note: I just translate the core original MATLAB codes (`/MATLAB/*.m`) to Python version (`/CQT.py`) with following functions:
8 |
9 | - Core:
10 | - `cqt`
11 | - `icqt`
12 | - `genCQTkernel`
13 | - `getCQT`
14 | - `cell2sparse`
15 | - `sparse2cell`
16 | - `plotCQT`
17 |
18 | - Extra bonus:
19 | - `buffer`
20 | - `upsample`
21 | - `round_half_up`
22 | - `nextpow2`
23 | - `hann`
24 |
25 | See the authors' homepage for more information and MATLAB packaged downloads:
26 |
27 | - https://code.soundsoftware.ac.uk/projects/constant-q-toolbox
28 |
29 | Or you can read my blog post (Chinese) for inspriation:
30 |
31 | - [恒 Q 变换 (Constant-Q transform)](https://iphysresearch.github.io/blog/post/signal_processing/cqt/)
32 |
33 | ## Related publications
34 |
35 | > C. Schörkhuber and A. Klapuri, “Constant-Q transform toolbox for music processing,” in Proceedings of the 7th Sound and Music Computing Conference, Barcelona, Spain, 2010. [PDF](https://www.researchgate.net/publication/228523955) or [Constant-Q_transform_toolbox_for_music_processing.pdf](./Constant-Q_transform_toolbox_for_music_processing.pdf)
36 |
37 |
38 | ## Requirements
39 |
40 | - Python 3.6+
41 | - Numpy
42 | - Scipy
43 | - Matplotlib
44 |
45 | ## Demo
46 |
47 | Note: It might not be as efficient than the original MATLAB version, partly because the sparse property have yet to be fully utilised in this Python version.
48 |
49 | ```python
50 | from CQT import *
51 | fname = './demo.dat'
52 | data = np.loadtxt(fname)
53 | t, hp, hc = data[:,0], data[:,1], data[:,2]
54 |
55 | fs = 1/(t[1]-t[0])
56 | print('fs =', fs)
57 |
58 | bins_per_octave = 24
59 | fmax = 400
60 | fmin = 20
61 |
62 | Xcqt = cqt(hp, fmin, fmax, bins_per_octave, fs,)
63 | _ = plotCQT(Xcqt, fs, 0.6)
64 |
65 | y = icqt(Xcqt)
66 | ```
67 |
68 | 
69 |
70 |
71 | ## License
72 |
73 | MIT
--------------------------------------------------------------------------------