analyzeAudio 0.0.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- analyzeAudio/__init__.py +14 -0
- analyzeAudio/analyzersUseFilename.py +234 -0
- analyzeAudio/analyzersUseSpectrogram.py +28 -0
- analyzeAudio/analyzersUseTensor.py +10 -0
- analyzeAudio/analyzersUseWaveform.py +25 -0
- analyzeAudio/audioAspectsRegistry.py +195 -0
- analyzeAudio/pythonator.py +85 -0
- analyzeAudio-0.0.11.dist-info/METADATA +108 -0
- analyzeAudio-0.0.11.dist-info/RECORD +14 -0
- analyzeAudio-0.0.11.dist-info/WHEEL +5 -0
- analyzeAudio-0.0.11.dist-info/top_level.txt +2 -0
- tests/conftest.py +16 -0
- tests/test_audioAspectsRegistry.py +3 -0
- tests/test_other.py +13 -0
analyzeAudio/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .audioAspectsRegistry import registrationAudioAspect, cacheAudioAnalyzers, analyzeAudioFile, \
|
|
2
|
+
analyzeAudioListPathFilenames, getListAvailableAudioAspects, audioAspects
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
'analyzeAudioFile',
|
|
6
|
+
'analyzeAudioListPathFilenames',
|
|
7
|
+
'audioAspects',
|
|
8
|
+
'getListAvailableAudioAspects',
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
from . import analyzersUseFilename
|
|
12
|
+
from . import analyzersUseSpectrogram
|
|
13
|
+
from . import analyzersUseTensor
|
|
14
|
+
from . import analyzersUseWaveform
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
from .pythonator import pythonizeFFprobe
|
|
2
|
+
from analyzeAudio import registrationAudioAspect, cacheAudioAnalyzers
|
|
3
|
+
from statistics import mean
|
|
4
|
+
from typing import Any, Dict, List, Optional, Union, cast
|
|
5
|
+
import cachetools
|
|
6
|
+
import numpy
|
|
7
|
+
import os
|
|
8
|
+
import pathlib
|
|
9
|
+
import re as regex
|
|
10
|
+
import subprocess
|
|
11
|
+
|
|
12
|
+
@registrationAudioAspect('SI-SDR mean')
|
|
13
|
+
def getSI_SDRmean(pathFilenameAlpha: Union[str, os.PathLike[Any]], pathFilenameBeta: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
14
|
+
"""
|
|
15
|
+
Calculate the mean Scale-Invariant Signal-to-Distortion Ratio (SI-SDR) between two audio files.
|
|
16
|
+
This function uses FFmpeg to compute the SI-SDR between two audio files specified by their paths.
|
|
17
|
+
The SI-SDR values are extracted from the FFmpeg output and their mean is calculated.
|
|
18
|
+
Parameters:
|
|
19
|
+
pathFilenameAlpha: Path to the first audio file.
|
|
20
|
+
pathFilenameBeta: Path to the second audio file.
|
|
21
|
+
Returns:
|
|
22
|
+
SI_SDRmean: The mean SI-SDR value in decibels (dB).
|
|
23
|
+
Raises:
|
|
24
|
+
subprocess.CalledProcessError: If the FFmpeg command fails.
|
|
25
|
+
ValueError: If no SI-SDR values are found in the FFmpeg output.
|
|
26
|
+
"""
|
|
27
|
+
commandLineFFmpeg = [
|
|
28
|
+
'ffmpeg', '-hide_banner', '-loglevel', '32',
|
|
29
|
+
'-i', f'{str(pathlib.Path(pathFilenameAlpha))}', '-i', f'{str(pathlib.Path(pathFilenameBeta))}',
|
|
30
|
+
'-filter_complex', '[0][1]asisdr', '-f', 'null', '-'
|
|
31
|
+
]
|
|
32
|
+
systemProcessFFmpeg = subprocess.run(commandLineFFmpeg, check=True, stderr=subprocess.PIPE)
|
|
33
|
+
|
|
34
|
+
stderrFFmpeg = systemProcessFFmpeg.stderr.decode()
|
|
35
|
+
|
|
36
|
+
regexSI_SDR = regex.compile(r"^\[Parsed_asisdr_.* (.*) dB", regex.MULTILINE)
|
|
37
|
+
|
|
38
|
+
listMatchesSI_SDR = regexSI_SDR.findall(stderrFFmpeg)
|
|
39
|
+
SI_SDRmean = mean(float(match) for match in listMatchesSI_SDR)
|
|
40
|
+
return SI_SDRmean
|
|
41
|
+
|
|
42
|
+
@cachetools.cached(cache=cacheAudioAnalyzers)
|
|
43
|
+
def ffprobeShotgunAndCache(pathFilename: Union[str, os.PathLike[Any]]) -> Dict[str, float]:
|
|
44
|
+
# for lavfi amovie/movie, the colons after driveLetter letters need to be escaped twice.
|
|
45
|
+
pFn = pathlib.PureWindowsPath(pathFilename)
|
|
46
|
+
lavfiPathFilename = pFn.drive.replace(":", "\\\\:")+pathlib.PureWindowsPath(pFn.root,pFn.relative_to(pFn.anchor)).as_posix()
|
|
47
|
+
|
|
48
|
+
filterChain: List[str] = []
|
|
49
|
+
filterChain += ["astats=metadata=1:measure_perchannel=Crest_factor+Zero_crossings_rate+Dynamic_range:measure_overall=all"]
|
|
50
|
+
filterChain += ["aspectralstats"]
|
|
51
|
+
filterChain += ["ebur128=metadata=1:framelog=quiet"]
|
|
52
|
+
|
|
53
|
+
entriesFFprobe = ["frame_tags"]
|
|
54
|
+
|
|
55
|
+
commandLineFFprobe = [
|
|
56
|
+
"ffprobe", "-hide_banner",
|
|
57
|
+
"-f", "lavfi", f"amovie={lavfiPathFilename},{','.join(filterChain)}",
|
|
58
|
+
"-show_entries", ':'.join(entriesFFprobe),
|
|
59
|
+
"-output_format", "json=compact=1",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
systemProcessFFprobe = subprocess.Popen(commandLineFFprobe, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
63
|
+
stdoutFFprobe, DISCARDstderr = systemProcessFFprobe.communicate()
|
|
64
|
+
FFprobeStructured = pythonizeFFprobe(stdoutFFprobe.decode('utf-8'))[-1]
|
|
65
|
+
|
|
66
|
+
dictionaryAspectsAnalyzed = {}
|
|
67
|
+
if 'aspectralstats' in FFprobeStructured:
|
|
68
|
+
for keyName in FFprobeStructured['aspectralstats']:
|
|
69
|
+
dictionaryAspectsAnalyzed[keyName] = numpy.mean(FFprobeStructured['aspectralstats'][keyName])
|
|
70
|
+
if 'r128' in FFprobeStructured:
|
|
71
|
+
for keyName in FFprobeStructured['r128']:
|
|
72
|
+
dictionaryAspectsAnalyzed[keyName] = FFprobeStructured['r128'][keyName][-1]
|
|
73
|
+
if 'astats' in FFprobeStructured:
|
|
74
|
+
for keyName, arrayFeatureValues in cast(dict, FFprobeStructured['astats']).items():
|
|
75
|
+
dictionaryAspectsAnalyzed[keyName.split('.')[-1]] = numpy.mean(arrayFeatureValues[..., -1:])
|
|
76
|
+
|
|
77
|
+
return dictionaryAspectsAnalyzed
|
|
78
|
+
|
|
79
|
+
@registrationAudioAspect('Zero-crossings rate')
|
|
80
|
+
def analyzeZero_crossings_rate(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
81
|
+
return ffprobeShotgunAndCache(pathFilename).get('Zero_crossings_rate')
|
|
82
|
+
|
|
83
|
+
@registrationAudioAspect('DC offset')
|
|
84
|
+
def analyzeDCoffset(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
85
|
+
return ffprobeShotgunAndCache(pathFilename).get('DC_offset')
|
|
86
|
+
|
|
87
|
+
@registrationAudioAspect('Dynamic range')
|
|
88
|
+
def analyzeDynamicRange(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
89
|
+
return ffprobeShotgunAndCache(pathFilename).get('Dynamic_range')
|
|
90
|
+
|
|
91
|
+
@registrationAudioAspect('Signal entropy')
|
|
92
|
+
def analyzeSignalEntropy(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
93
|
+
return ffprobeShotgunAndCache(pathFilename).get('Entropy')
|
|
94
|
+
|
|
95
|
+
@registrationAudioAspect('Duration-samples')
|
|
96
|
+
def analyzeNumber_of_samples(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
97
|
+
return ffprobeShotgunAndCache(pathFilename).get('Number_of_samples')
|
|
98
|
+
|
|
99
|
+
@registrationAudioAspect('Peak dB')
|
|
100
|
+
def analyzePeak_level(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
101
|
+
return ffprobeShotgunAndCache(pathFilename).get('Peak_level')
|
|
102
|
+
|
|
103
|
+
@registrationAudioAspect('RMS total')
|
|
104
|
+
def analyzeRMS_level(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
105
|
+
return ffprobeShotgunAndCache(pathFilename).get('RMS_level')
|
|
106
|
+
|
|
107
|
+
@registrationAudioAspect('Crest factor')
|
|
108
|
+
def analyzeCrest_factor(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
109
|
+
return ffprobeShotgunAndCache(pathFilename).get('Crest_factor')
|
|
110
|
+
|
|
111
|
+
@registrationAudioAspect('RMS peak')
|
|
112
|
+
def analyzeRMS_peak(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
113
|
+
return ffprobeShotgunAndCache(pathFilename).get('RMS_peak')
|
|
114
|
+
|
|
115
|
+
@registrationAudioAspect('LUFS integrated')
|
|
116
|
+
def analyzeLUFSintegrated(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
117
|
+
return ffprobeShotgunAndCache(pathFilename).get('I')
|
|
118
|
+
|
|
119
|
+
@registrationAudioAspect('LUFS loudness range')
|
|
120
|
+
def analyzeLRA(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
121
|
+
return ffprobeShotgunAndCache(pathFilename).get('LRA')
|
|
122
|
+
|
|
123
|
+
@registrationAudioAspect('LUFS low')
|
|
124
|
+
def analyzeLUFSlow(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
125
|
+
return ffprobeShotgunAndCache(pathFilename).get('LRA.low')
|
|
126
|
+
|
|
127
|
+
@registrationAudioAspect('LUFS high')
|
|
128
|
+
def analyzeLUFShigh(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
129
|
+
return ffprobeShotgunAndCache(pathFilename).get('LRA.high')
|
|
130
|
+
|
|
131
|
+
@registrationAudioAspect('Spectral mean')
|
|
132
|
+
def analyzeMean(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
133
|
+
return ffprobeShotgunAndCache(pathFilename).get('mean')
|
|
134
|
+
|
|
135
|
+
@registrationAudioAspect('Spectral variance')
|
|
136
|
+
def analyzeVariance(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
137
|
+
return ffprobeShotgunAndCache(pathFilename).get('variance')
|
|
138
|
+
|
|
139
|
+
@registrationAudioAspect('Spectral centroid')
|
|
140
|
+
def analyzeCentroid(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
141
|
+
return ffprobeShotgunAndCache(pathFilename).get('centroid')
|
|
142
|
+
|
|
143
|
+
@registrationAudioAspect('Spectral spread')
|
|
144
|
+
def analyzeSpread(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
145
|
+
return ffprobeShotgunAndCache(pathFilename).get('spread')
|
|
146
|
+
|
|
147
|
+
@registrationAudioAspect('Spectral skewness')
|
|
148
|
+
def analyzeSkewness(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
149
|
+
return ffprobeShotgunAndCache(pathFilename).get('skewness')
|
|
150
|
+
|
|
151
|
+
@registrationAudioAspect('Spectral kurtosis')
|
|
152
|
+
def analyzeKurtosis(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
153
|
+
return ffprobeShotgunAndCache(pathFilename).get('kurtosis')
|
|
154
|
+
|
|
155
|
+
@registrationAudioAspect('Spectral entropy')
|
|
156
|
+
def analyzeSpectralEntropy(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
157
|
+
return ffprobeShotgunAndCache(pathFilename).get('entropy')
|
|
158
|
+
|
|
159
|
+
@registrationAudioAspect('Spectral flatness')
|
|
160
|
+
def analyzeFlatness(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
161
|
+
return ffprobeShotgunAndCache(pathFilename).get('flatness')
|
|
162
|
+
|
|
163
|
+
@registrationAudioAspect('Spectral crest')
|
|
164
|
+
def analyzeCrest(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
165
|
+
return ffprobeShotgunAndCache(pathFilename).get('crest')
|
|
166
|
+
|
|
167
|
+
@registrationAudioAspect('Spectral flux')
|
|
168
|
+
def analyzeFlux(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
169
|
+
return ffprobeShotgunAndCache(pathFilename).get('flux')
|
|
170
|
+
|
|
171
|
+
@registrationAudioAspect('Spectral slope')
|
|
172
|
+
def analyzeSlope(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
173
|
+
return ffprobeShotgunAndCache(pathFilename).get('slope')
|
|
174
|
+
|
|
175
|
+
@registrationAudioAspect('Spectral decrease')
|
|
176
|
+
def analyzeDecrease(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
177
|
+
return ffprobeShotgunAndCache(pathFilename).get('decrease')
|
|
178
|
+
|
|
179
|
+
@registrationAudioAspect('Spectral rolloff')
|
|
180
|
+
def analyzeRolloff(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
181
|
+
return ffprobeShotgunAndCache(pathFilename).get('rolloff')
|
|
182
|
+
|
|
183
|
+
@registrationAudioAspect('Abs_Peak_count')
|
|
184
|
+
def analyzeAbs_Peak_count(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
185
|
+
print('Abs_Peak_count', pathFilename)
|
|
186
|
+
return ffprobeShotgunAndCache(pathFilename).get('Abs_Peak_count')
|
|
187
|
+
|
|
188
|
+
@registrationAudioAspect('Bit_depth')
|
|
189
|
+
def analyzeBit_depth(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
190
|
+
return ffprobeShotgunAndCache(pathFilename).get('Bit_depth')
|
|
191
|
+
|
|
192
|
+
@registrationAudioAspect('Flat_factor')
|
|
193
|
+
def analyzeFlat_factor(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
194
|
+
return ffprobeShotgunAndCache(pathFilename).get('Flat_factor')
|
|
195
|
+
|
|
196
|
+
@registrationAudioAspect('Max_difference')
|
|
197
|
+
def analyzeMax_difference(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
198
|
+
return ffprobeShotgunAndCache(pathFilename).get('Max_difference')
|
|
199
|
+
|
|
200
|
+
@registrationAudioAspect('Max_level')
|
|
201
|
+
def analyzeMax_level(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
202
|
+
return ffprobeShotgunAndCache(pathFilename).get('Max_level')
|
|
203
|
+
|
|
204
|
+
@registrationAudioAspect('Mean_difference')
|
|
205
|
+
def analyzeMean_difference(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
206
|
+
return ffprobeShotgunAndCache(pathFilename).get('Mean_difference')
|
|
207
|
+
|
|
208
|
+
@registrationAudioAspect('Min_difference')
|
|
209
|
+
def analyzeMin_difference(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
210
|
+
return ffprobeShotgunAndCache(pathFilename).get('Min_difference')
|
|
211
|
+
|
|
212
|
+
@registrationAudioAspect('Min_level')
|
|
213
|
+
def analyzeMin_level(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
214
|
+
return ffprobeShotgunAndCache(pathFilename).get('Min_level')
|
|
215
|
+
|
|
216
|
+
@registrationAudioAspect('Noise_floor')
|
|
217
|
+
def analyzeNoise_floor(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
218
|
+
return ffprobeShotgunAndCache(pathFilename).get('Noise_floor')
|
|
219
|
+
|
|
220
|
+
@registrationAudioAspect('Noise_floor_count')
|
|
221
|
+
def analyzeNoise_floor_count(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
222
|
+
return ffprobeShotgunAndCache(pathFilename).get('Noise_floor_count')
|
|
223
|
+
|
|
224
|
+
@registrationAudioAspect('Peak_count')
|
|
225
|
+
def analyzePeak_count(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
226
|
+
return ffprobeShotgunAndCache(pathFilename).get('Peak_count')
|
|
227
|
+
|
|
228
|
+
@registrationAudioAspect('RMS_difference')
|
|
229
|
+
def analyzeRMS_difference(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
230
|
+
return ffprobeShotgunAndCache(pathFilename).get('RMS_difference')
|
|
231
|
+
|
|
232
|
+
@registrationAudioAspect('RMS_trough')
|
|
233
|
+
def analyzeRMS_trough(pathFilename: Union[str, os.PathLike[Any]]) -> Optional[float]:
|
|
234
|
+
return ffprobeShotgunAndCache(pathFilename).get('RMS_trough')
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from analyzeAudio import registrationAudioAspect, audioAspects, cacheAudioAnalyzers
|
|
2
|
+
from typing import Any
|
|
3
|
+
import cachetools
|
|
4
|
+
import librosa
|
|
5
|
+
import numpy
|
|
6
|
+
|
|
7
|
+
@registrationAudioAspect('Chromagram')
|
|
8
|
+
def analyzeChromagram(spectrogramPower: numpy.ndarray, sampleRate: int, **keywordArguments: Any) -> numpy.ndarray:
|
|
9
|
+
return librosa.feature.chroma_stft(S=spectrogramPower, sr=sampleRate, **keywordArguments)
|
|
10
|
+
|
|
11
|
+
@registrationAudioAspect('Spectral Contrast')
|
|
12
|
+
def analyzeSpectralContrast(spectrogramMagnitude: numpy.ndarray, **keywordArguments: Any) -> numpy.ndarray:
|
|
13
|
+
return librosa.feature.spectral_contrast(S=spectrogramMagnitude, **keywordArguments)
|
|
14
|
+
|
|
15
|
+
@registrationAudioAspect('Spectral Bandwidth')
|
|
16
|
+
def analyzeSpectralBandwidth(spectrogramMagnitude: numpy.ndarray, **keywordArguments: Any) -> numpy.ndarray:
|
|
17
|
+
centroid = audioAspects['Spectral Centroid']['analyzer'](spectrogramMagnitude)
|
|
18
|
+
return librosa.feature.spectral_bandwidth(S=spectrogramMagnitude, centroid=centroid, **keywordArguments)
|
|
19
|
+
|
|
20
|
+
@cachetools.cached(cache=cacheAudioAnalyzers)
|
|
21
|
+
@registrationAudioAspect('Spectral Centroid')
|
|
22
|
+
def analyzeSpectralCentroid(spectrogramMagnitude: numpy.ndarray, **keywordArguments: Any) -> numpy.ndarray:
|
|
23
|
+
return librosa.feature.spectral_centroid(S=spectrogramMagnitude, **keywordArguments)
|
|
24
|
+
|
|
25
|
+
@registrationAudioAspect('Spectral Flatness')
|
|
26
|
+
def analyzeSpectralFlatness(spectrogramMagnitude: numpy.ndarray, **keywordArguments: Any) -> numpy.ndarray:
|
|
27
|
+
spectralFlatness = librosa.feature.spectral_flatness(S=spectrogramMagnitude, **keywordArguments)
|
|
28
|
+
return 20 * numpy.log10(spectralFlatness, where=(spectralFlatness != 0)) # dB
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from analyzeAudio import registrationAudioAspect
|
|
2
|
+
from torchmetrics.functional.audio.srmr import speech_reverberation_modulation_energy_ratio
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
import numpy
|
|
5
|
+
import torch
|
|
6
|
+
|
|
7
|
+
@registrationAudioAspect('SRMR')
|
|
8
|
+
def analyzeSRMR(tensorAudio: torch.Tensor, sampleRate: int, pytorchOnCPU: Optional[bool], **keywordArguments: Any) -> numpy.ndarray:
|
|
9
|
+
keywordArguments['fast'] = keywordArguments.get('fast') or pytorchOnCPU or None
|
|
10
|
+
return torch.Tensor.numpy(speech_reverberation_modulation_energy_ratio(tensorAudio, sampleRate, **keywordArguments))
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from analyzeAudio import registrationAudioAspect, audioAspects, cacheAudioAnalyzers
|
|
2
|
+
from typing import Any
|
|
3
|
+
import librosa
|
|
4
|
+
import numpy
|
|
5
|
+
import cachetools
|
|
6
|
+
|
|
7
|
+
@cachetools.cached(cache=cacheAudioAnalyzers)
|
|
8
|
+
@registrationAudioAspect('Tempogram')
|
|
9
|
+
def analyzeTempogram(waveform: numpy.ndarray, sampleRate: int, **keywordArguments: Any) -> numpy.ndarray:
|
|
10
|
+
return librosa.feature.tempogram(y=waveform, sr=sampleRate, **keywordArguments)
|
|
11
|
+
|
|
12
|
+
# "RMS value from audio samples is faster ... However, ... spectrogram ... more accurate ... because ... windowed"
|
|
13
|
+
@registrationAudioAspect('RMS from waveform')
|
|
14
|
+
def analyzeRMS(waveform: numpy.ndarray, **keywordArguments: Any) -> numpy.ndarray:
|
|
15
|
+
arrayRMS = librosa.feature.rms(y=waveform, **keywordArguments)
|
|
16
|
+
return 20 * numpy.log10(arrayRMS, where=(arrayRMS != 0)) # dB
|
|
17
|
+
|
|
18
|
+
@registrationAudioAspect('Tempo')
|
|
19
|
+
def analyzeTempo(waveform: numpy.ndarray, sampleRate: int, **keywordArguments: Any) -> numpy.ndarray:
|
|
20
|
+
tempogram = audioAspects['Tempogram']['analyzer'](waveform, sampleRate)
|
|
21
|
+
return librosa.feature.tempo(y=waveform, sr=sampleRate, tg=tempogram, **keywordArguments)
|
|
22
|
+
|
|
23
|
+
@registrationAudioAspect('Zero-crossing rate') # This is distinct from 'Zero-crossings rate'
|
|
24
|
+
def analyzeZeroCrossingRate(waveform: numpy.ndarray, **keywordArguments: Any) -> numpy.ndarray:
|
|
25
|
+
return librosa.feature.zero_crossing_rate(y=waveform, **keywordArguments)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from Z0Z_tools import defineConcurrencyLimit, oopsieKwargsie
|
|
2
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
3
|
+
from numpy.typing import NDArray
|
|
4
|
+
from tqdm.auto import tqdm
|
|
5
|
+
from typing import Any, Callable, cast, Dict, List, Optional, ParamSpec, Sequence, TypeAlias, TYPE_CHECKING, TypeVar, Union
|
|
6
|
+
import cachetools
|
|
7
|
+
import inspect
|
|
8
|
+
import librosa
|
|
9
|
+
import multiprocessing
|
|
10
|
+
import numpy
|
|
11
|
+
import os
|
|
12
|
+
import pathlib
|
|
13
|
+
import soundfile
|
|
14
|
+
import torch
|
|
15
|
+
import warnings
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from typing import TypedDict
|
|
19
|
+
else:
|
|
20
|
+
TypedDict = dict
|
|
21
|
+
|
|
22
|
+
if __name__ == '__main__':
|
|
23
|
+
multiprocessing.set_start_method('spawn')
|
|
24
|
+
|
|
25
|
+
warnings.filterwarnings('ignore', category=UserWarning, module='torchmetrics', message='.*fast=True.*')
|
|
26
|
+
|
|
27
|
+
parameterSpecifications = ParamSpec('parameterSpecifications')
|
|
28
|
+
typeReturned = TypeVar('typeReturned')
|
|
29
|
+
|
|
30
|
+
subclassTarget: TypeAlias = numpy.ndarray
|
|
31
|
+
audioAspect: TypeAlias = str
|
|
32
|
+
class analyzersAudioAspects(TypedDict):
|
|
33
|
+
analyzer: Callable[..., Any]
|
|
34
|
+
analyzerParameters: List[str]
|
|
35
|
+
|
|
36
|
+
audioAspects: Dict[audioAspect, analyzersAudioAspects] = {}
|
|
37
|
+
"""A register of 1) measurable aspects of audio data, 2) analyzer functions to measure audio aspects, 3) and parameters of analyzer functions."""
|
|
38
|
+
|
|
39
|
+
def registrationAudioAspect(aspectName: str) -> Callable[[Callable[parameterSpecifications, typeReturned]], Callable[parameterSpecifications, typeReturned]]:
|
|
40
|
+
"""
|
|
41
|
+
A function to "decorate" a registrant-analyzer function and the aspect of audio data it can analyze.
|
|
42
|
+
|
|
43
|
+
Parameters:
|
|
44
|
+
aspectName: The audio aspect that the registrar will enter into the register, `audioAspects`.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def registrar(registrant: Callable[parameterSpecifications, typeReturned]) -> Callable[parameterSpecifications, typeReturned]:
|
|
48
|
+
"""
|
|
49
|
+
`registrar` updates the registry, `audioAspects`, with 1) the analyzer function, `registrant`, 2) the analyzer function's parameters, and 3) the aspect of audio data that the analyzer function measures.
|
|
50
|
+
|
|
51
|
+
Parameters:
|
|
52
|
+
registrant: The function that analyzes an aspect of audio data.
|
|
53
|
+
|
|
54
|
+
Note:
|
|
55
|
+
`registrar` does not change the behavior of `registrant`, the analyzer function.
|
|
56
|
+
"""
|
|
57
|
+
audioAspects[aspectName] = {
|
|
58
|
+
'analyzer': registrant,
|
|
59
|
+
'analyzerParameters': inspect.getfullargspec(registrant).args
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# if registrant.__annotations__.get('return') is not None and issubclass(registrant.__annotations__['return'], subclassTarget): # maybe someday I will understand why this doesn't work
|
|
63
|
+
# if registrant.__annotations__.get('return') is not None and issubclass(registrant.__annotations__.get('return', type(None)), subclassTarget): # maybe someday I will understand why this doesn't work
|
|
64
|
+
if isinstance(registrant.__annotations__.get('return', type(None)), type) and issubclass(registrant.__annotations__.get('return', type(None)), subclassTarget): # maybe someday I will understand what all of this statement means
|
|
65
|
+
def registrationAudioAspectMean(*arguments: parameterSpecifications.args, **keywordArguments: parameterSpecifications.kwargs) -> numpy.floating[Any]:
|
|
66
|
+
"""
|
|
67
|
+
`registrar` updates the registry with a new analyzer function that calculates the mean of the analyzer's numpy.ndarray result.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
mean: Mean value of the analyzer's numpy.ndarray result.
|
|
71
|
+
"""
|
|
72
|
+
aspectValue = registrant(*arguments, **keywordArguments)
|
|
73
|
+
return numpy.mean(cast(subclassTarget, aspectValue))
|
|
74
|
+
# return aspectValue.mean()
|
|
75
|
+
audioAspects[f"{aspectName} mean"] = {
|
|
76
|
+
'analyzer': registrationAudioAspectMean,
|
|
77
|
+
'analyzerParameters': inspect.getfullargspec(registrant).args
|
|
78
|
+
}
|
|
79
|
+
return registrant
|
|
80
|
+
return registrar
|
|
81
|
+
|
|
82
|
+
def analyzeAudioFile(pathFilename: Union[str, os.PathLike[Any]], listAspectNames: List[str]) -> List[Union[str, float, NDArray[Any]]]:
|
|
83
|
+
"""
|
|
84
|
+
Analyzes an audio file for specified aspects and returns the results.
|
|
85
|
+
|
|
86
|
+
Parameters:
|
|
87
|
+
pathFilename: The path to the audio file to be analyzed.
|
|
88
|
+
listAspectNames: A list of aspect names to analyze in the audio file.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
listAspectValues: A list of analyzed values in the same order as `listAspectNames`.
|
|
92
|
+
"""
|
|
93
|
+
pathlib.Path(pathFilename).stat() # raises FileNotFoundError if the file does not exist
|
|
94
|
+
dictionaryAspectsAnalyzed: Dict[str, Union[str, float, NDArray[Any]]] = {aspectName: 'not found' for aspectName in listAspectNames}
|
|
95
|
+
"""Despite returning a list, use a dictionary to preserve the order of the listAspectNames.
|
|
96
|
+
Similarly, 'not found' ensures the returned list length == len(listAspectNames)"""
|
|
97
|
+
|
|
98
|
+
# waveform, sampleRate = librosa.load(path=str(pathFilename), sr=None, mono=False)
|
|
99
|
+
with soundfile.SoundFile(pathFilename) as readSoundFile:
|
|
100
|
+
sampleRate: int = readSoundFile.samplerate
|
|
101
|
+
waveform = readSoundFile.read()
|
|
102
|
+
waveform = waveform.T
|
|
103
|
+
|
|
104
|
+
# I need "lazy" loading
|
|
105
|
+
tryAgain = True
|
|
106
|
+
while tryAgain: # `tenacity`?
|
|
107
|
+
try:
|
|
108
|
+
tensorAudio = torch.from_numpy(waveform) # memory-sharing
|
|
109
|
+
tryAgain = False
|
|
110
|
+
except RuntimeError as ERRORmessage:
|
|
111
|
+
if 'negative stride' in str(ERRORmessage):
|
|
112
|
+
waveform = waveform.copy() # not memory-sharing
|
|
113
|
+
tryAgain = True
|
|
114
|
+
else:
|
|
115
|
+
raise ERRORmessage
|
|
116
|
+
|
|
117
|
+
spectrogram = librosa.stft(y=waveform)
|
|
118
|
+
spectrogramMagnitude, DISCARDEDphase = librosa.magphase(D=spectrogram)
|
|
119
|
+
spectrogramPower = numpy.absolute(spectrogram) ** 2
|
|
120
|
+
|
|
121
|
+
pytorchOnCPU = not torch.cuda.is_available() # False if GPU available, True if not
|
|
122
|
+
|
|
123
|
+
for aspectName in listAspectNames:
|
|
124
|
+
if aspectName in audioAspects:
|
|
125
|
+
analyzer = audioAspects[aspectName]['analyzer']
|
|
126
|
+
analyzerParameters = audioAspects[aspectName]['analyzerParameters']
|
|
127
|
+
dictionaryAspectsAnalyzed[aspectName] = analyzer(*map(vars().get, analyzerParameters))
|
|
128
|
+
|
|
129
|
+
return [dictionaryAspectsAnalyzed[aspectName] for aspectName in listAspectNames]
|
|
130
|
+
|
|
131
|
+
def analyzeAudioListPathFilenames(listPathFilenames: Union[Sequence[str], Sequence[os.PathLike[Any]]], listAspectNames: List[str], CPUlimit: Optional[Union[int, float, bool]] = None) -> List[List[Union[str, float, NDArray[Any]]]]:
|
|
132
|
+
"""
|
|
133
|
+
Analyzes a list of audio files for specified aspects of the individual files and returns the results.
|
|
134
|
+
|
|
135
|
+
Parameters:
|
|
136
|
+
listPathFilenames: A list of paths to the audio files to be analyzed.
|
|
137
|
+
listAspectNames: A list of aspect names to analyze in each audio file.
|
|
138
|
+
CPUlimit (gluttonous resource usage): whether and how to limit the CPU usage. See notes for details.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
rowsListFilenameAspectValues: A list of lists, where each inner list contains the filename and
|
|
142
|
+
analyzed values corresponding to the specified aspects, which are in the same order as `listAspectNames`.
|
|
143
|
+
|
|
144
|
+
Limits on CPU usage CPUlimit:
|
|
145
|
+
False, None, or 0: No limits on CPU usage; uses all available CPUs. All other values will potentially limit CPU usage.
|
|
146
|
+
True: Yes, limit the CPU usage; limits to 1 CPU.
|
|
147
|
+
Integer >= 1: Limits usage to the specified number of CPUs.
|
|
148
|
+
Decimal value (float) between 0 and 1: Fraction of total CPUs to use.
|
|
149
|
+
Decimal value (float) between -1 and 0: Fraction of CPUs to *not* use.
|
|
150
|
+
Integer <= -1: Subtract the absolute value from total CPUs.
|
|
151
|
+
|
|
152
|
+
You can save the data with `Z0Z_tools.dataTabularTOpathFilenameDelimited()`.
|
|
153
|
+
For example,
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
dataTabularTOpathFilenameDelimited(
|
|
157
|
+
pathFilename = pathFilename,
|
|
158
|
+
tableRows = rowsListFilenameAspectValues, # The return of this function
|
|
159
|
+
tableColumns = ['File'] + listAspectNames # A parameter of this function
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Nevertheless, I aspire to improve `analyzeAudioListPathFilenames` by radically improving the structure of the returned data.
|
|
164
|
+
"""
|
|
165
|
+
rowsListFilenameAspectValues = []
|
|
166
|
+
|
|
167
|
+
if not (CPUlimit is None or isinstance(CPUlimit, (bool, int, float))):
|
|
168
|
+
CPUlimit = oopsieKwargsie(CPUlimit)
|
|
169
|
+
max_workers = defineConcurrencyLimit(CPUlimit)
|
|
170
|
+
|
|
171
|
+
with ProcessPoolExecutor(max_workers=max_workers) as concurrencyManager:
|
|
172
|
+
dictionaryConcurrency = {concurrencyManager.submit(analyzeAudioFile, pathFilename, listAspectNames)
|
|
173
|
+
: pathFilename
|
|
174
|
+
for pathFilename in listPathFilenames}
|
|
175
|
+
|
|
176
|
+
for claimTicket in as_completed(dictionaryConcurrency):
|
|
177
|
+
cacheAudioAnalyzers.pop(dictionaryConcurrency[claimTicket], None)
|
|
178
|
+
listAspectValues = claimTicket.result()
|
|
179
|
+
rowsListFilenameAspectValues.append(
|
|
180
|
+
[str(pathlib.PurePath(dictionaryConcurrency[claimTicket]).as_posix())]
|
|
181
|
+
+ listAspectValues)
|
|
182
|
+
|
|
183
|
+
return rowsListFilenameAspectValues
|
|
184
|
+
|
|
185
|
+
def getListAvailableAudioAspects() -> List[str]:
|
|
186
|
+
"""
|
|
187
|
+
Returns a sorted list of audio aspect names. All valid values for the parameter `listAspectNames`, for example,
|
|
188
|
+
are returned by this function.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
listAvailableAudioAspects: The list of aspect names registered in `audioAspects`.
|
|
192
|
+
"""
|
|
193
|
+
return sorted(audioAspects.keys())
|
|
194
|
+
|
|
195
|
+
cacheAudioAnalyzers = cachetools.LRUCache(maxsize=256)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from typing import Dict, Any, Tuple, Union
|
|
3
|
+
import json
|
|
4
|
+
import numpy
|
|
5
|
+
|
|
6
|
+
def pythonizeFFprobe(FFprobeJSON_utf8: str):
|
|
7
|
+
FFroot: Dict[str, Any] = json.loads(FFprobeJSON_utf8)
|
|
8
|
+
Z0Z_dictionaries: Dict[str, Union[numpy.ndarray, Dict[str, numpy.ndarray]]] = {}
|
|
9
|
+
if 'packets_and_frames' in FFroot: # Divide into 'packets' and 'frames'
|
|
10
|
+
FFroot = defaultdict(list, FFroot)
|
|
11
|
+
for packetOrFrame in FFroot['packets_and_frames']:
|
|
12
|
+
if 'type' in packetOrFrame:
|
|
13
|
+
FFroot[section := packetOrFrame['type'] + 's'].append(packetOrFrame)
|
|
14
|
+
del FFroot[section][-1]['type']
|
|
15
|
+
else:
|
|
16
|
+
raise ValueError("'packets_and_frames' for the win!")
|
|
17
|
+
del FFroot['packets_and_frames']
|
|
18
|
+
|
|
19
|
+
Z0Z_register = [
|
|
20
|
+
'aspectralstats',
|
|
21
|
+
'astats',
|
|
22
|
+
'r128',
|
|
23
|
+
'signalstats',
|
|
24
|
+
]
|
|
25
|
+
if 'frames' in FFroot:
|
|
26
|
+
leftCrumbs = False
|
|
27
|
+
listTuplesBlackdetect = []
|
|
28
|
+
for indexFrame, FFframe in enumerate(FFroot['frames']):
|
|
29
|
+
if 'tags' in FFframe:
|
|
30
|
+
if 'lavfi.black_start' in FFframe['tags']:
|
|
31
|
+
listTuplesBlackdetect.append(float(FFframe['tags']['lavfi.black_start']))
|
|
32
|
+
del FFframe['tags']['lavfi.black_start']
|
|
33
|
+
if 'lavfi.black_end' in FFframe['tags']:
|
|
34
|
+
listTuplesBlackdetect[-1] = (listTuplesBlackdetect[-1], float(FFframe['tags']['lavfi.black_end']))
|
|
35
|
+
del FFframe['tags']['lavfi.black_end']
|
|
36
|
+
|
|
37
|
+
# This is not the way to do it
|
|
38
|
+
for keyName, keyValue in FFframe['tags'].items():
|
|
39
|
+
if 'lavfi' in (keyNameDeconstructed := keyName.split('.'))[0]:
|
|
40
|
+
channel = None
|
|
41
|
+
if (registrant := keyNameDeconstructed[1]) in Z0Z_register:
|
|
42
|
+
keyNameDeconstructed = keyNameDeconstructed[2:]
|
|
43
|
+
if keyNameDeconstructed[0].isdigit():
|
|
44
|
+
channel = int(keyNameDeconstructed[0])
|
|
45
|
+
keyNameDeconstructed = keyNameDeconstructed[1:]
|
|
46
|
+
statistic = '.'.join(keyNameDeconstructed)
|
|
47
|
+
if channel is None:
|
|
48
|
+
while True:
|
|
49
|
+
try:
|
|
50
|
+
Z0Z_dictionaries[registrant][statistic][indexFrame] = float(keyValue)
|
|
51
|
+
break # If successful, exit the loop
|
|
52
|
+
except KeyError:
|
|
53
|
+
if registrant not in Z0Z_dictionaries:
|
|
54
|
+
Z0Z_dictionaries[registrant] = {}
|
|
55
|
+
elif statistic not in Z0Z_dictionaries[registrant]:
|
|
56
|
+
Z0Z_dictionaries[registrant][statistic] = numpy.zeros((len(FFroot['frames'])))
|
|
57
|
+
else:
|
|
58
|
+
raise # Re-raise the exception
|
|
59
|
+
else:
|
|
60
|
+
while True:
|
|
61
|
+
try:
|
|
62
|
+
Z0Z_dictionaries[registrant][statistic][channel - 1, indexFrame] = float(keyValue)
|
|
63
|
+
break # If successful, exit the loop
|
|
64
|
+
except KeyError:
|
|
65
|
+
if registrant not in Z0Z_dictionaries:
|
|
66
|
+
Z0Z_dictionaries[registrant] = {}
|
|
67
|
+
elif statistic not in Z0Z_dictionaries[registrant]:
|
|
68
|
+
Z0Z_dictionaries[registrant][statistic] = numpy.zeros((channel, len(FFroot['frames'])))
|
|
69
|
+
else:
|
|
70
|
+
raise # Re-raise the exception
|
|
71
|
+
except IndexError:
|
|
72
|
+
if channel > Z0Z_dictionaries[registrant][statistic].shape[0]:
|
|
73
|
+
Z0Z_dictionaries[registrant][statistic].resize((channel, len(FFroot['frames'])))
|
|
74
|
+
else:
|
|
75
|
+
raise # Re-raise the exception
|
|
76
|
+
|
|
77
|
+
if not FFframe['tags']: # empty = False
|
|
78
|
+
del FFframe['tags']
|
|
79
|
+
if FFframe:
|
|
80
|
+
leftCrumbs = True
|
|
81
|
+
if listTuplesBlackdetect:
|
|
82
|
+
Z0Z_dictionaries['blackdetect'] = numpy.array(listTuplesBlackdetect, dtype=[('black_start', numpy.float32), ('black_end', numpy.float32)], copy=False)
|
|
83
|
+
if not leftCrumbs:
|
|
84
|
+
del FFroot['frames']
|
|
85
|
+
return FFroot, Z0Z_dictionaries
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: analyzeAudio
|
|
3
|
+
Version: 0.0.11
|
|
4
|
+
Summary: Measure one or more aspects of one or more audio files.
|
|
5
|
+
Author-email: Hunter Hogan <HunterHogan@pm.me>
|
|
6
|
+
License: CC-BY-NC-4.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/hunterhogan/analyzeAudio
|
|
8
|
+
Project-URL: Donate, https://www.patreon.com/integrated
|
|
9
|
+
Keywords: audio,analysis,measurement,metrics,torch
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: Intended Audience :: Other Audience
|
|
15
|
+
Classifier: Natural Language :: English
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: cachetools
|
|
23
|
+
Requires-Dist: librosa
|
|
24
|
+
Requires-Dist: numpy
|
|
25
|
+
Requires-Dist: torch
|
|
26
|
+
Requires-Dist: torchmetrics[audio]
|
|
27
|
+
Requires-Dist: tqdm
|
|
28
|
+
Requires-Dist: Z0Z-tools
|
|
29
|
+
Provides-Extra: testing
|
|
30
|
+
Requires-Dist: pytest; extra == "testing"
|
|
31
|
+
Requires-Dist: pytest-cov; extra == "testing"
|
|
32
|
+
Requires-Dist: pytest-env; extra == "testing"
|
|
33
|
+
|
|
34
|
+
# analyzeAudio
|
|
35
|
+
|
|
36
|
+
Measure one or more aspects of one or more audio files.
|
|
37
|
+
|
|
38
|
+
## Note well: FFmpeg & FFprobe binaries must be in PATH
|
|
39
|
+
|
|
40
|
+
Some options to [download FFmpeg and FFprobe](https://www.ffmpeg.org/download.html) at ffmpeg.org.
|
|
41
|
+
|
|
42
|
+
## Some ways to use this package
|
|
43
|
+
|
|
44
|
+
### Use `analyzeAudioFile` to measure one or more aspects of a single audio file
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from analyzeAudio import analyzeAudioFile
|
|
48
|
+
listAspectNames = ['LUFS integrated',
|
|
49
|
+
'RMS peak',
|
|
50
|
+
'SRMR mean',
|
|
51
|
+
'Spectral Flatness mean']
|
|
52
|
+
listMeasurements = analyzeAudioFile(pathFilename, listAspectNames)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Use `getListAvailableAudioAspects` to get a crude list of aspects this package can measure
|
|
56
|
+
|
|
57
|
+
The aspect names are accurate, but the lack of additional documentation can make things challenging. 'Zero-crossing rate', 'Zero-crossing rate mean', and 'Zero-crossings rate', for example, are different from each other. ("... lack of additional documentation ...")
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import analyzeAudio
|
|
61
|
+
analyzeAudio.getListAvailableAudioAspects()
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Use `analyzeAudioListPathFilenames` to measure one or more aspects of individual file in a list of audio files
|
|
65
|
+
|
|
66
|
+
### Use `audioAspects` to call an analyzer function by using the name of the aspect you wish to measure
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from analyzeAudio import audioAspects
|
|
70
|
+
SI_SDR_channelsMean = audioAspects['SI-SDR mean']['analyzer'](pathFilenameAudioFile, pathFilenameDifferentAudioFile)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Retrieve the names of the parameters for an analyzer function with the `['analyzerParameters']` key-name.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from analyzeAudio import audioAspects
|
|
77
|
+
print(audioAspects['Chromagram']['analyzerParameters'])
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Install this package
|
|
81
|
+
|
|
82
|
+
### From Github
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
pip install analyzeAudio@git+https://github.com/hunterhogan/analyzeAudio.git
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### From a local directory
|
|
89
|
+
|
|
90
|
+
#### Windows
|
|
91
|
+
|
|
92
|
+
```powershell
|
|
93
|
+
git clone https://github.com/hunterhogan/analyzeAudio.git \path\to\analyzeAudio
|
|
94
|
+
pip install analyzeAudio@file:\path\to\analyzeAudio
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### POSIX
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
git clone https://github.com/hunterhogan/analyzeAudio.git /path/to/analyzeAudio
|
|
101
|
+
pip install analyzeAudio@file:/path/to/analyzeAudio
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Install updates
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
pip install --upgrade analyzeAudio@git+https://github.com/hunterhogan/analyzeAudio.git
|
|
108
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
analyzeAudio/__init__.py,sha256=WT2LnWdO-7r8ARjS9fVqjedu-TwUDj-gStQMJouplc4,452
|
|
2
|
+
analyzeAudio/analyzersUseFilename.py,sha256=RMoIKlLzHZW-ks-J-CxgrpD8IgifTl2B514VeW0J2W4,11316
|
|
3
|
+
analyzeAudio/analyzersUseSpectrogram.py,sha256=bY06q3iacB0ciyjAlfde6rxLRXCvrg7CY3Yp-elv9b4,1623
|
|
4
|
+
analyzeAudio/analyzersUseTensor.py,sha256=n5jOwtam5pDErYp_Ja_VGVR_Q4c0MS3KPLIQnaRfDgs,572
|
|
5
|
+
analyzeAudio/analyzersUseWaveform.py,sha256=4HCWb8LCqZAeXGxkBOpfViXDv3ffFQiTtTyIpLoVZ24,1381
|
|
6
|
+
analyzeAudio/audioAspectsRegistry.py,sha256=abIbrXRqDfx8DRE9HKSv7qQPfUzxU8IhLbB6n5JJAuU,9365
|
|
7
|
+
analyzeAudio/pythonator.py,sha256=jTsQapE537hP_Wav16EsvETdwsw7FzDhQnbX6tISC8g,4835
|
|
8
|
+
tests/conftest.py,sha256=BhZswOjkl_u-qiS4Zy38d2fETdWAtZiigeuXYBK8l0k,397
|
|
9
|
+
tests/test_audioAspectsRegistry.py,sha256=bZb6TQQwRedaCKzH5hqPpJkn7cHl_cHpj4OHtPonawo,28
|
|
10
|
+
tests/test_other.py,sha256=mJxNqTjs5U_qdmNIVN599h3XzoDtRMvM8koE9-wI664,549
|
|
11
|
+
analyzeAudio-0.0.11.dist-info/METADATA,sha256=VEDc6M2hh2eNHAJJVJWME3dYpuDaGZ9GoaRiIrttrdY,3462
|
|
12
|
+
analyzeAudio-0.0.11.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
13
|
+
analyzeAudio-0.0.11.dist-info/top_level.txt,sha256=QV8LQ0r_1LIQuewxDcEzODpykv5qRYG3I70piOUSVRg,19
|
|
14
|
+
analyzeAudio-0.0.11.dist-info/RECORD,,
|
tests/conftest.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
pathDataSamples = pathlib.Path(__file__).parent / "dataSamples"
|
|
5
|
+
|
|
6
|
+
listOfFiles = ["pink-20RMS60sec.wav",
|
|
7
|
+
"pink-40RMS60sec.wav",
|
|
8
|
+
"pink-60RMS60sec.wav",
|
|
9
|
+
"testParkMono96kHz32float12.1sec.wav",
|
|
10
|
+
"testPink2ch7.1sec.wav",
|
|
11
|
+
"testSine2ch5sec.wav",
|
|
12
|
+
"testSine2ch5secCopy1.wav",
|
|
13
|
+
"testTrain2ch48kHz6.3sec.wav",
|
|
14
|
+
"testVideo11sec.mkv",
|
|
15
|
+
"testWooWooMono16kHz32integerClipping9sec.wav",
|
|
16
|
+
]
|
tests/test_other.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from Z0Z_tools.pytest_parseParameters import makeTestSuiteConcurrencyLimit, makeTestSuiteOopsieKwargsie
|
|
3
|
+
from Z0Z_tools import defineConcurrencyLimit, oopsieKwargsie
|
|
4
|
+
|
|
5
|
+
def test_oopsieKwargsie():
|
|
6
|
+
dictionaryTests = makeTestSuiteOopsieKwargsie(oopsieKwargsie)
|
|
7
|
+
for testName, testFunction in dictionaryTests.items():
|
|
8
|
+
testFunction()
|
|
9
|
+
|
|
10
|
+
def test_defineConcurrencyLimit():
|
|
11
|
+
dictionaryTests = makeTestSuiteConcurrencyLimit(defineConcurrencyLimit)
|
|
12
|
+
for testName, testFunction in dictionaryTests.items():
|
|
13
|
+
testFunction()
|