analyzeAudio 0.0.15__tar.gz → 0.0.17__tar.gz
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-0.0.15 → analyzeaudio-0.0.17}/PKG-INFO +16 -5
- {analyzeaudio-0.0.15 → analyzeaudio-0.0.17}/README.md +9 -0
- {analyzeaudio-0.0.15 → analyzeaudio-0.0.17}/analyzeAudio/analyzersUseFilename.py +32 -23
- {analyzeaudio-0.0.15 → analyzeaudio-0.0.17}/analyzeAudio/analyzersUseSpectrogram.py +14 -13
- {analyzeaudio-0.0.15 → analyzeaudio-0.0.17}/analyzeAudio/analyzersUseTensor.py +3 -2
- analyzeaudio-0.0.17/analyzeAudio/analyzersUseWaveform.py +27 -0
- {analyzeaudio-0.0.15 → analyzeaudio-0.0.17}/analyzeAudio/audioAspectsRegistry.py +73 -55
- {analyzeaudio-0.0.15 → analyzeaudio-0.0.17}/analyzeAudio/pythonator.py +14 -11
- {analyzeaudio-0.0.15 → analyzeaudio-0.0.17}/analyzeAudio.egg-info/PKG-INFO +16 -5
- {analyzeaudio-0.0.15 → analyzeaudio-0.0.17}/analyzeAudio.egg-info/requires.txt +3 -2
- analyzeaudio-0.0.17/pyproject.toml +103 -0
- analyzeaudio-0.0.17/tests/test_other.py +11 -0
- analyzeaudio-0.0.15/analyzeAudio/analyzersUseWaveform.py +0 -26
- analyzeaudio-0.0.15/pyproject.toml +0 -81
- analyzeaudio-0.0.15/tests/test_other.py +0 -10
- {analyzeaudio-0.0.15 → analyzeaudio-0.0.17}/LICENSE +0 -0
- {analyzeaudio-0.0.15 → analyzeaudio-0.0.17}/analyzeAudio/__init__.py +0 -0
- {analyzeaudio-0.0.15 → analyzeaudio-0.0.17}/analyzeAudio/py.typed +0 -0
- {analyzeaudio-0.0.15 → analyzeaudio-0.0.17}/analyzeAudio.egg-info/SOURCES.txt +0 -0
- {analyzeaudio-0.0.15 → analyzeaudio-0.0.17}/analyzeAudio.egg-info/dependency_links.txt +0 -0
- {analyzeaudio-0.0.15 → analyzeaudio-0.0.17}/analyzeAudio.egg-info/entry_points.txt +0 -0
- {analyzeaudio-0.0.15 → analyzeaudio-0.0.17}/analyzeAudio.egg-info/top_level.txt +0 -0
- {analyzeaudio-0.0.15 → analyzeaudio-0.0.17}/setup.cfg +0 -0
- {analyzeaudio-0.0.15 → analyzeaudio-0.0.17}/tests/conftest.py +0 -0
- {analyzeaudio-0.0.15 → analyzeaudio-0.0.17}/tests/test_audioAspectsRegistry.py +0 -0
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: analyzeAudio
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.17
|
|
4
4
|
Summary: Measure one or more aspects of one or more audio files.
|
|
5
5
|
Author-email: Hunter Hogan <HunterHogan@pm.me>
|
|
6
6
|
License: CC-BY-NC-4.0
|
|
7
7
|
Project-URL: Donate, https://www.patreon.com/integrated
|
|
8
8
|
Project-URL: Homepage, https://github.com/hunterhogan/analyzeAudio
|
|
9
|
+
Project-URL: Issues, https://github.com/hunterhogan/
|
|
9
10
|
Project-URL: Repository, https://github.com/hunterhogan/analyzeAudio.git
|
|
10
|
-
Keywords:
|
|
11
|
+
Keywords: FFmpeg,FFprobe,LUFS,RMS,SRMR,analysis,audio,audio-analysis,loudness,measurement,metrics,signal-processing,spectral,spectrum,torch,waveform
|
|
11
12
|
Classifier: Development Status :: 3 - Alpha
|
|
12
13
|
Classifier: Environment :: Console
|
|
13
14
|
Classifier: Intended Audience :: Developers
|
|
14
15
|
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
-
Classifier: Intended Audience :: Science/Research
|
|
16
16
|
Classifier: Intended Audience :: Information Technology
|
|
17
17
|
Classifier: Intended Audience :: Other Audience
|
|
18
|
+
Classifier: Intended Audience :: Science/Research
|
|
18
19
|
Classifier: Natural Language :: English
|
|
19
20
|
Classifier: Operating System :: OS Independent
|
|
20
21
|
Classifier: Programming Language :: Python
|
|
@@ -32,16 +33,17 @@ Classifier: Typing :: Typed
|
|
|
32
33
|
Requires-Python: >=3.10
|
|
33
34
|
Description-Content-Type: text/markdown
|
|
34
35
|
License-File: LICENSE
|
|
36
|
+
Requires-Dist: Z0Z_tools
|
|
35
37
|
Requires-Dist: cachetools
|
|
38
|
+
Requires-Dist: hunterMakesPy
|
|
36
39
|
Requires-Dist: librosa
|
|
37
40
|
Requires-Dist: numpy
|
|
38
|
-
Requires-Dist: optype[numpy]
|
|
39
41
|
Requires-Dist: standard-aifc; python_version >= "3.13"
|
|
40
42
|
Requires-Dist: standard-sunau; python_version >= "3.13"
|
|
41
43
|
Requires-Dist: torch
|
|
42
44
|
Requires-Dist: torchmetrics[audio]
|
|
43
45
|
Requires-Dist: tqdm
|
|
44
|
-
Requires-Dist:
|
|
46
|
+
Requires-Dist: typing_extensions
|
|
45
47
|
Provides-Extra: testing
|
|
46
48
|
Requires-Dist: pytest; extra == "testing"
|
|
47
49
|
Requires-Dist: pytest-cov; extra == "testing"
|
|
@@ -213,4 +215,13 @@ pip install analyzeAudio
|
|
|
213
215
|
[](https://HunterThinks.com/support)
|
|
214
216
|
[](https://www.youtube.com/@HunterHogan)
|
|
215
217
|
|
|
218
|
+
## How to code
|
|
219
|
+
|
|
220
|
+
Coding One Step at a Time:
|
|
221
|
+
|
|
222
|
+
0. WRITE CODE.
|
|
223
|
+
1. Don't write stupid code that's hard to revise.
|
|
224
|
+
2. Write good code.
|
|
225
|
+
3. When revising, write better code.
|
|
226
|
+
|
|
216
227
|
[](https://creativecommons.org/licenses/by-nc/4.0/)
|
|
@@ -162,4 +162,13 @@ pip install analyzeAudio
|
|
|
162
162
|
[](https://HunterThinks.com/support)
|
|
163
163
|
[](https://www.youtube.com/@HunterHogan)
|
|
164
164
|
|
|
165
|
+
## How to code
|
|
166
|
+
|
|
167
|
+
Coding One Step at a Time:
|
|
168
|
+
|
|
169
|
+
0. WRITE CODE.
|
|
170
|
+
1. Don't write stupid code that's hard to revise.
|
|
171
|
+
2. Write good code.
|
|
172
|
+
3. When revising, write better code.
|
|
173
|
+
|
|
165
174
|
[](https://creativecommons.org/licenses/by-nc/4.0/)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"""Analyzers that use the filename of an audio file to analyze its audio data."""
|
|
2
|
+
# ruff: noqa: D103
|
|
3
|
+
from analyzeAudio import cacheAudioAnalyzers, registrationAudioAspect
|
|
4
|
+
from analyzeAudio.pythonator import pythonizeFFprobe
|
|
3
5
|
from os import PathLike
|
|
4
6
|
from statistics import mean
|
|
5
7
|
from typing import Any, cast
|
|
@@ -11,22 +13,31 @@ import subprocess
|
|
|
11
13
|
|
|
12
14
|
@registrationAudioAspect('SI-SDR mean')
|
|
13
15
|
def getSI_SDRmean(pathFilenameAlpha: str | PathLike[Any], pathFilenameBeta: str | PathLike[Any]) -> float | None:
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
"""Calculate the mean Scale-Invariant Signal-to-Distortion Ratio (SI-SDR) between two audio files.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
pathFilenameAlpha : str | PathLike[Any]
|
|
21
|
+
Path to the first audio file.
|
|
22
|
+
pathFilenameBeta : str | PathLike[Any]
|
|
23
|
+
Path to the second audio file.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
SI_SDRmean : float | None
|
|
28
|
+
The mean SI-SDR value in decibels (dB).
|
|
29
|
+
|
|
30
|
+
Raises
|
|
31
|
+
------
|
|
32
|
+
subprocess.CalledProcessError
|
|
33
|
+
If the FFmpeg command fails.
|
|
34
|
+
ValueError
|
|
35
|
+
If no SI-SDR values are found in the FFmpeg output.
|
|
36
|
+
|
|
26
37
|
"""
|
|
27
38
|
commandLineFFmpeg = [
|
|
28
39
|
'ffmpeg', '-hide_banner', '-loglevel', '32',
|
|
29
|
-
'-i', f'{str(pathlib.Path(pathFilenameAlpha))}', '-i', f'{str(pathlib.Path(pathFilenameBeta))}',
|
|
40
|
+
'-i', f'{str(pathlib.Path(pathFilenameAlpha))}', '-i', f'{str(pathlib.Path(pathFilenameBeta))}', # noqa: RUF010
|
|
30
41
|
'-filter_complex', '[0][1]asisdr', '-f', 'null', '-'
|
|
31
42
|
]
|
|
32
43
|
systemProcessFFmpeg = subprocess.run(commandLineFFmpeg, check=True, stderr=subprocess.PIPE)
|
|
@@ -36,8 +47,7 @@ def getSI_SDRmean(pathFilenameAlpha: str | PathLike[Any], pathFilenameBeta: str
|
|
|
36
47
|
regexSI_SDR = regex.compile(r"^\[Parsed_asisdr_.* (.*) dB", regex.MULTILINE)
|
|
37
48
|
|
|
38
49
|
listMatchesSI_SDR = regexSI_SDR.findall(stderrFFmpeg)
|
|
39
|
-
|
|
40
|
-
return SI_SDRmean
|
|
50
|
+
return mean(float(match) for match in listMatchesSI_SDR)
|
|
41
51
|
|
|
42
52
|
@cachetools.cached(cache=cacheAudioAnalyzers)
|
|
43
53
|
def ffprobeShotgunAndCache(pathFilename: str | PathLike[Any]) -> dict[str, float]:
|
|
@@ -66,16 +76,16 @@ def ffprobeShotgunAndCache(pathFilename: str | PathLike[Any]) -> dict[str, float
|
|
|
66
76
|
dictionaryAspectsAnalyzed: dict[str, float] = {}
|
|
67
77
|
if 'aspectralstats' in FFprobeStructured:
|
|
68
78
|
for keyName in FFprobeStructured['aspectralstats']:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
"""No matter how many channels, each keyName is `numpy.ndarray[tuple[int, int], numpy.dtype[numpy.float64]]`
|
|
80
|
+
where `tuple[int, int]` is (channel, frame)
|
|
81
|
+
NOTE (as of this writing) `registrar` can only understand the generic class `numpy.ndarray` and not more specific typing
|
|
82
|
+
dictionaryAspectsAnalyzed[keyName] = FFprobeStructured['aspectralstats'][keyName]"""
|
|
73
83
|
dictionaryAspectsAnalyzed[keyName] = numpy.mean(FFprobeStructured['aspectralstats'][keyName]).astype(float)
|
|
74
84
|
if 'r128' in FFprobeStructured:
|
|
75
85
|
for keyName in FFprobeStructured['r128']:
|
|
76
86
|
dictionaryAspectsAnalyzed[keyName] = FFprobeStructured['r128'][keyName][-1]
|
|
77
87
|
if 'astats' in FFprobeStructured:
|
|
78
|
-
for keyName, arrayFeatureValues in cast(dict[str, numpy.ndarray[Any, Any]], FFprobeStructured['astats']).items():
|
|
88
|
+
for keyName, arrayFeatureValues in cast('dict[str, numpy.ndarray[Any, Any]]', FFprobeStructured['astats']).items():
|
|
79
89
|
dictionaryAspectsAnalyzed[keyName.split('.')[-1]] = numpy.mean(arrayFeatureValues[..., -1:None]).astype(float)
|
|
80
90
|
|
|
81
91
|
return dictionaryAspectsAnalyzed
|
|
@@ -186,7 +196,6 @@ def analyzeRolloff(pathFilename: str | PathLike[Any]) -> float | None:
|
|
|
186
196
|
|
|
187
197
|
@registrationAudioAspect('Abs_Peak_count')
|
|
188
198
|
def analyzeAbs_Peak_count(pathFilename: str | PathLike[Any]) -> float | None:
|
|
189
|
-
print('Abs_Peak_count', pathFilename)
|
|
190
199
|
return ffprobeShotgunAndCache(pathFilename).get('Abs_Peak_count')
|
|
191
200
|
|
|
192
201
|
@registrationAudioAspect('Bit_depth')
|
|
@@ -1,30 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
"""Analyzers that use the spectrogram to analyze audio data."""
|
|
2
|
+
# ruff: noqa: D103
|
|
3
|
+
from analyzeAudio import audioAspects, cacheAudioAnalyzers, registrationAudioAspect
|
|
4
|
+
from numpy import dtype, floating
|
|
2
5
|
from typing import Any
|
|
3
6
|
import cachetools
|
|
4
7
|
import librosa
|
|
5
8
|
import numpy
|
|
6
|
-
from optype.numpy import AnyFloatingDType, ToArray3D, ToFloat3D
|
|
7
|
-
from numpy import dtype, floating
|
|
8
9
|
|
|
9
10
|
@registrationAudioAspect('Chromagram')
|
|
10
|
-
def analyzeChromagram(spectrogramPower: numpy.ndarray[Any, dtype[floating[Any]]], sampleRate: int, **keywordArguments: Any) -> numpy.ndarray:
|
|
11
|
-
return librosa.feature.chroma_stft(S=spectrogramPower, sr=sampleRate, **keywordArguments)
|
|
11
|
+
def analyzeChromagram(spectrogramPower: numpy.ndarray[Any, dtype[floating[Any]]], sampleRate: int, **keywordArguments: Any) -> numpy.ndarray: # pyright: ignore [reportMissingTypeArgument, reportUnknownParameterType]
|
|
12
|
+
return librosa.feature.chroma_stft(S=spectrogramPower, sr=sampleRate, **keywordArguments) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
|
|
12
13
|
|
|
13
14
|
@registrationAudioAspect('Spectral Contrast')
|
|
14
|
-
def analyzeSpectralContrast(spectrogramMagnitude: numpy.ndarray[Any, dtype[floating[Any]]], **keywordArguments: Any) -> numpy.ndarray:
|
|
15
|
-
return librosa.feature.spectral_contrast(S=spectrogramMagnitude, **keywordArguments)
|
|
15
|
+
def analyzeSpectralContrast(spectrogramMagnitude: numpy.ndarray[Any, dtype[floating[Any]]], **keywordArguments: Any) -> numpy.ndarray: # pyright: ignore [reportMissingTypeArgument, reportUnknownParameterType]
|
|
16
|
+
return librosa.feature.spectral_contrast(S=spectrogramMagnitude, **keywordArguments) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
|
|
16
17
|
|
|
17
18
|
@registrationAudioAspect('Spectral Bandwidth')
|
|
18
|
-
def analyzeSpectralBandwidth(spectrogramMagnitude: numpy.ndarray[Any, dtype[floating[Any]]], **keywordArguments: Any) -> numpy.ndarray:
|
|
19
|
+
def analyzeSpectralBandwidth(spectrogramMagnitude: numpy.ndarray[Any, dtype[floating[Any]]], **keywordArguments: Any) -> numpy.ndarray: # pyright: ignore [reportMissingTypeArgument, reportUnknownParameterType]
|
|
19
20
|
centroid = audioAspects['Spectral Centroid']['analyzer'](spectrogramMagnitude)
|
|
20
|
-
return librosa.feature.spectral_bandwidth(S=spectrogramMagnitude, centroid=centroid, **keywordArguments)
|
|
21
|
+
return librosa.feature.spectral_bandwidth(S=spectrogramMagnitude, centroid=centroid, **keywordArguments) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
|
|
21
22
|
|
|
22
23
|
@cachetools.cached(cache=cacheAudioAnalyzers)
|
|
23
24
|
@registrationAudioAspect('Spectral Centroid')
|
|
24
|
-
def analyzeSpectralCentroid(spectrogramMagnitude: numpy.ndarray[Any, dtype[floating[Any]]], **keywordArguments: Any) -> numpy.ndarray:
|
|
25
|
-
return librosa.feature.spectral_centroid(S=spectrogramMagnitude, **keywordArguments)
|
|
25
|
+
def analyzeSpectralCentroid(spectrogramMagnitude: numpy.ndarray[Any, dtype[floating[Any]]], **keywordArguments: Any) -> numpy.ndarray: # pyright: ignore [reportMissingTypeArgument, reportUnknownParameterType]
|
|
26
|
+
return librosa.feature.spectral_centroid(S=spectrogramMagnitude, **keywordArguments) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
|
|
26
27
|
|
|
27
28
|
@registrationAudioAspect('Spectral Flatness')
|
|
28
|
-
def analyzeSpectralFlatness(spectrogramMagnitude: numpy.ndarray[Any, dtype[floating[Any]]], **keywordArguments: Any) -> numpy.ndarray:
|
|
29
|
-
spectralFlatness = librosa.feature.spectral_flatness(S=spectrogramMagnitude, **keywordArguments)
|
|
29
|
+
def analyzeSpectralFlatness(spectrogramMagnitude: numpy.ndarray[Any, dtype[floating[Any]]], **keywordArguments: Any) -> numpy.ndarray: # pyright: ignore [reportMissingTypeArgument, reportUnknownParameterType]
|
|
30
|
+
spectralFlatness: numpy.ndarray[tuple[int, ...], numpy.dtype[numpy.floating[Any]]] = librosa.feature.spectral_flatness(S=spectrogramMagnitude, **keywordArguments) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
|
|
30
31
|
return 20 * numpy.log10(spectralFlatness, where=(spectralFlatness != 0)) # dB
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"""Analyzers that use the tensor to analyze audio data."""
|
|
1
2
|
from analyzeAudio import registrationAudioAspect
|
|
2
3
|
from torchmetrics.functional.audio.srmr import speech_reverberation_modulation_energy_ratio
|
|
3
4
|
from typing import Any
|
|
@@ -5,6 +6,6 @@ import numpy
|
|
|
5
6
|
import torch
|
|
6
7
|
|
|
7
8
|
@registrationAudioAspect('SRMR')
|
|
8
|
-
def analyzeSRMR(tensorAudio: torch.Tensor, sampleRate: int, pytorchOnCPU: bool | None, **keywordArguments: Any) -> numpy.ndarray:
|
|
9
|
+
def analyzeSRMR(tensorAudio: torch.Tensor, sampleRate: int, pytorchOnCPU: bool | None, **keywordArguments: Any) -> numpy.ndarray: # pyright: ignore [reportMissingTypeArgument, reportUnknownParameterType] # noqa: D103, FBT001
|
|
9
10
|
keywordArguments['fast'] = keywordArguments.get('fast') or pytorchOnCPU or None
|
|
10
|
-
return torch.Tensor.numpy(speech_reverberation_modulation_energy_ratio(tensorAudio, sampleRate, **keywordArguments))
|
|
11
|
+
return torch.Tensor.numpy(speech_reverberation_modulation_energy_ratio(tensorAudio, sampleRate, **keywordArguments)) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Analyzers that use the waveform of audio data."""
|
|
2
|
+
# ruff: noqa: D103
|
|
3
|
+
from analyzeAudio import audioAspects, cacheAudioAnalyzers, registrationAudioAspect
|
|
4
|
+
from typing import Any
|
|
5
|
+
import cachetools
|
|
6
|
+
import librosa
|
|
7
|
+
import numpy
|
|
8
|
+
|
|
9
|
+
@cachetools.cached(cache=cacheAudioAnalyzers)
|
|
10
|
+
@registrationAudioAspect('Tempogram')
|
|
11
|
+
def analyzeTempogram(waveform: numpy.ndarray[tuple[int, ...], numpy.dtype[numpy.floating[Any]]], sampleRate: int, **keywordArguments: Any) -> numpy.ndarray: # pyright: ignore [reportMissingTypeArgument, reportUnknownParameterType]
|
|
12
|
+
return librosa.feature.tempogram(y=waveform, sr=sampleRate, **keywordArguments) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
|
|
13
|
+
|
|
14
|
+
# "RMS value from audio samples is faster ... However, ... spectrogram ... more accurate ... because ... windowed"
|
|
15
|
+
@registrationAudioAspect('RMS from waveform')
|
|
16
|
+
def analyzeRMS(waveform: numpy.ndarray[tuple[int, ...], numpy.dtype[numpy.floating[Any]]], **keywordArguments: Any) -> numpy.ndarray: # pyright: ignore [reportMissingTypeArgument, reportUnknownParameterType]
|
|
17
|
+
arrayRMS: numpy.ndarray[tuple[int, ...], numpy.dtype[numpy.floating[Any]]] = librosa.feature.rms(y=waveform, **keywordArguments) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
|
|
18
|
+
return 20 * numpy.log10(arrayRMS, where=(arrayRMS != 0)) # dB
|
|
19
|
+
|
|
20
|
+
@registrationAudioAspect('Tempo')
|
|
21
|
+
def analyzeTempo(waveform: numpy.ndarray[tuple[int, ...], numpy.dtype[numpy.floating[Any]]], sampleRate: int, **keywordArguments: Any) -> numpy.ndarray: # pyright: ignore [reportMissingTypeArgument, reportUnknownParameterType]
|
|
22
|
+
tempogram = audioAspects['Tempogram']['analyzer'](waveform, sampleRate)
|
|
23
|
+
return librosa.feature.tempo(y=waveform, sr=sampleRate, tg=tempogram, **keywordArguments) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
|
|
24
|
+
|
|
25
|
+
@registrationAudioAspect('Zero-crossing rate') # This is distinct from 'Zero-crossings rate'
|
|
26
|
+
def analyzeZeroCrossingRate(waveform: numpy.ndarray[tuple[int, ...], numpy.dtype[numpy.floating[Any]]], **keywordArguments: Any) -> numpy.ndarray: # pyright: ignore [reportMissingTypeArgument, reportUnknownParameterType]
|
|
27
|
+
return librosa.feature.zero_crossing_rate(y=waveform, **keywordArguments) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
from collections.abc import Callable, Sequence
|
|
2
|
-
from concurrent.futures import
|
|
1
|
+
from collections.abc import Callable, Sequence # noqa: D100
|
|
2
|
+
from concurrent.futures import as_completed, ProcessPoolExecutor
|
|
3
|
+
from hunterMakesPy import defineConcurrencyLimit, oopsieKwargsie
|
|
4
|
+
from multiprocessing import set_start_method as multiprocessing_set_start_method
|
|
3
5
|
from numpy.typing import NDArray
|
|
4
6
|
from os import PathLike
|
|
5
|
-
from typing import Any, cast, ParamSpec, TypeAlias,
|
|
6
|
-
from
|
|
7
|
+
from typing import Any, cast, ParamSpec, TypeAlias, TypeVar
|
|
8
|
+
from typing_extensions import TypedDict
|
|
9
|
+
from Z0Z_tools import Spectrogram, stft
|
|
7
10
|
import cachetools
|
|
11
|
+
import contextlib
|
|
8
12
|
import inspect
|
|
9
13
|
import numpy
|
|
10
14
|
import pathlib
|
|
@@ -12,16 +16,8 @@ import soundfile
|
|
|
12
16
|
import torch
|
|
13
17
|
import warnings
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
from typing import TypedDict
|
|
17
|
-
else:
|
|
18
|
-
TypedDict = dict
|
|
19
|
-
|
|
20
|
-
from multiprocessing import set_start_method as multiprocessing_set_start_method
|
|
21
|
-
try:
|
|
19
|
+
with contextlib.suppress(RuntimeError):
|
|
22
20
|
multiprocessing_set_start_method('spawn')
|
|
23
|
-
except RuntimeError:
|
|
24
|
-
pass
|
|
25
21
|
|
|
26
22
|
warnings.filterwarnings('ignore', category=UserWarning, module='torchmetrics', message='.*fast=True.*')
|
|
27
23
|
|
|
@@ -29,48 +25,57 @@ parameterSpecifications = ParamSpec('parameterSpecifications')
|
|
|
29
25
|
typeReturned = TypeVar('typeReturned')
|
|
30
26
|
|
|
31
27
|
audioAspect: TypeAlias = str
|
|
32
|
-
|
|
28
|
+
|
|
29
|
+
class analyzersAudioAspects(TypedDict): # noqa: D101
|
|
33
30
|
analyzer: Callable[..., Any]
|
|
34
31
|
analyzerParameters: list[str]
|
|
35
32
|
|
|
33
|
+
|
|
36
34
|
audioAspects: dict[audioAspect, analyzersAudioAspects] = {}
|
|
37
35
|
"""A register of 1) measurable aspects of audio data, 2) analyzer functions to measure audio aspects, 3) and parameters of analyzer functions."""
|
|
38
36
|
|
|
39
37
|
def registrationAudioAspect(aspectName: str) -> Callable[[Callable[parameterSpecifications, typeReturned]], Callable[parameterSpecifications, typeReturned]]:
|
|
40
|
-
"""
|
|
41
|
-
|
|
38
|
+
"""'Decorate' a registrant-analyzer function and the aspect of audio data it can analyze.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
aspectName : str
|
|
43
|
+
The audio aspect that the registrar will enter into the register, `audioAspects`.
|
|
42
44
|
|
|
43
|
-
Parameters:
|
|
44
|
-
aspectName: The audio aspect that the registrar will enter into the register, `audioAspects`.
|
|
45
45
|
"""
|
|
46
46
|
|
|
47
47
|
def registrar(registrant: Callable[parameterSpecifications, typeReturned]) -> Callable[parameterSpecifications, typeReturned]:
|
|
48
48
|
"""
|
|
49
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
50
|
|
|
51
|
-
Parameters
|
|
52
|
-
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
registrant : Callable
|
|
54
|
+
The function that analyzes an aspect of audio data.
|
|
55
|
+
|
|
56
|
+
Note
|
|
57
|
+
----
|
|
58
|
+
`registrar` does not change the behavior of `registrant`, the analyzer function.
|
|
53
59
|
|
|
54
|
-
Note:
|
|
55
|
-
`registrar` does not change the behavior of `registrant`, the analyzer function.
|
|
56
60
|
"""
|
|
57
61
|
audioAspects[aspectName] = {
|
|
58
62
|
'analyzer': registrant,
|
|
59
63
|
'analyzerParameters': inspect.getfullargspec(registrant).args
|
|
60
64
|
}
|
|
61
65
|
|
|
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
66
|
if isinstance(registrant.__annotations__.get('return', type(None)), type) and issubclass(registrant.__annotations__.get('return', type(None)), numpy.ndarray): # maybe someday I will understand what all of this statement means
|
|
65
67
|
def registrationAudioAspectMean(*arguments: parameterSpecifications.args, **keywordArguments: parameterSpecifications.kwargs) -> numpy.floating[Any]:
|
|
66
68
|
"""
|
|
67
69
|
`registrar` updates the registry with a new analyzer function that calculates the mean of the analyzer's numpy.ndarray result.
|
|
68
70
|
|
|
69
|
-
Returns
|
|
70
|
-
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
mean : float
|
|
74
|
+
Mean value of the analyzer's numpy.ndarray result.
|
|
75
|
+
|
|
71
76
|
"""
|
|
72
77
|
aspectValue = registrant(*arguments, **keywordArguments)
|
|
73
|
-
return numpy.mean(cast(NDArray[Any], aspectValue))
|
|
78
|
+
return numpy.mean(cast('NDArray[Any]', aspectValue))
|
|
74
79
|
# return aspectValue.mean()
|
|
75
80
|
audioAspects[f"{aspectName} mean"] = {
|
|
76
81
|
'analyzer': registrationAudioAspectMean,
|
|
@@ -83,15 +88,21 @@ def analyzeAudioFile(pathFilename: str | PathLike[Any], listAspectNames: list[st
|
|
|
83
88
|
"""
|
|
84
89
|
Analyzes an audio file for specified aspects and returns the results.
|
|
85
90
|
|
|
86
|
-
Parameters
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
pathFilename : str or PathLike
|
|
94
|
+
The path to the audio file to be analyzed.
|
|
95
|
+
listAspectNames : list of str
|
|
96
|
+
A list of aspect names to analyze in the audio file.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
listAspectValues : list of (str or float or NDArray)
|
|
101
|
+
A list of analyzed values in the same order as `listAspectNames`.
|
|
89
102
|
|
|
90
|
-
Returns:
|
|
91
|
-
listAspectValues: A list of analyzed values in the same order as `listAspectNames`.
|
|
92
103
|
"""
|
|
93
104
|
pathlib.Path(pathFilename).stat() # raises FileNotFoundError if the file does not exist
|
|
94
|
-
dictionaryAspectsAnalyzed: dict[str, str | float | NDArray[Any]] =
|
|
105
|
+
dictionaryAspectsAnalyzed: dict[str, str | float | NDArray[Any]] = dict.fromkeys(listAspectNames, 'not found')
|
|
95
106
|
"""Despite returning a list, use a dictionary to preserve the order of the listAspectNames.
|
|
96
107
|
Similarly, 'not found' ensures the returned list length == len(listAspectNames)"""
|
|
97
108
|
|
|
@@ -102,22 +113,22 @@ def analyzeAudioFile(pathFilename: str | PathLike[Any], listAspectNames: list[st
|
|
|
102
113
|
|
|
103
114
|
# I need "lazy" loading
|
|
104
115
|
tryAgain = True
|
|
105
|
-
while tryAgain:
|
|
116
|
+
while tryAgain:
|
|
106
117
|
try:
|
|
107
|
-
tensorAudio = torch.from_numpy(waveform) # memory-sharing
|
|
118
|
+
tensorAudio = torch.from_numpy(waveform) # pyright: ignore[reportUnknownMemberType] # memory-sharing # noqa: F841
|
|
108
119
|
tryAgain = False
|
|
109
|
-
except RuntimeError as ERRORmessage:
|
|
120
|
+
except RuntimeError as ERRORmessage: # noqa: PERF203
|
|
110
121
|
if 'negative stride' in str(ERRORmessage):
|
|
111
122
|
waveform = waveform.copy() # not memory-sharing
|
|
112
123
|
tryAgain = True
|
|
113
124
|
else:
|
|
114
|
-
raise ERRORmessage
|
|
125
|
+
raise ERRORmessage # noqa: TRY201
|
|
115
126
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
spectrogram = stft(waveform, sampleRate=sampleRate)
|
|
128
|
+
spectrogramMagnitude = numpy.absolute(spectrogram)
|
|
129
|
+
spectrogramPower = spectrogramMagnitude ** 2 # noqa: F841
|
|
119
130
|
|
|
120
|
-
pytorchOnCPU = not torch.cuda.is_available() # False if GPU available, True if not
|
|
131
|
+
pytorchOnCPU = not torch.cuda.is_available() # False if GPU available, True if not # noqa: F841
|
|
121
132
|
|
|
122
133
|
for aspectName in listAspectNames:
|
|
123
134
|
if aspectName in audioAspects:
|
|
@@ -127,18 +138,23 @@ def analyzeAudioFile(pathFilename: str | PathLike[Any], listAspectNames: list[st
|
|
|
127
138
|
|
|
128
139
|
return [dictionaryAspectsAnalyzed[aspectName] for aspectName in listAspectNames]
|
|
129
140
|
|
|
130
|
-
def analyzeAudioListPathFilenames(listPathFilenames: Sequence[str] | Sequence[PathLike[Any]], listAspectNames: list[str], CPUlimit: int | float | bool | None = None) -> list[list[str | float | NDArray[Any]]]:
|
|
141
|
+
def analyzeAudioListPathFilenames(listPathFilenames: Sequence[str] | Sequence[PathLike[Any]], listAspectNames: list[str], CPUlimit: int | float | bool | None = None) -> list[list[str | float | NDArray[Any]]]: # noqa: FBT001, PYI041
|
|
131
142
|
"""
|
|
132
143
|
Analyzes a list of audio files for specified aspects of the individual files and returns the results.
|
|
133
144
|
|
|
134
|
-
Parameters
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
145
|
+
Parameters
|
|
146
|
+
----------
|
|
147
|
+
listPathFilenames : Sequence of str or PathLike
|
|
148
|
+
A list of paths to the audio files to be analyzed.
|
|
149
|
+
listAspectNames : list of str
|
|
150
|
+
A list of aspect names to analyze in each audio file.
|
|
151
|
+
CPUlimit : int, float, bool, or None, default=None
|
|
152
|
+
Whether and how to limit the CPU usage. See notes for details.
|
|
138
153
|
|
|
139
|
-
Returns
|
|
140
|
-
|
|
141
|
-
|
|
154
|
+
Returns
|
|
155
|
+
-------
|
|
156
|
+
rowsListFilenameAspectValues : list of list of (str or float or NDArray)
|
|
157
|
+
A list of lists, where each inner list contains the filename and analyzed values corresponding to the specified aspects, which are in the same order as `listAspectNames`.
|
|
142
158
|
|
|
143
159
|
You can save the data with `Z0Z_tools.dataTabularTOpathFilenameDelimited()`.
|
|
144
160
|
For example,
|
|
@@ -166,7 +182,7 @@ def analyzeAudioListPathFilenames(listPathFilenames: Sequence[str] | Sequence[Pa
|
|
|
166
182
|
|
|
167
183
|
if not (CPUlimit is None or isinstance(CPUlimit, (bool, int, float))):
|
|
168
184
|
CPUlimit = oopsieKwargsie(CPUlimit)
|
|
169
|
-
max_workers = defineConcurrencyLimit(CPUlimit)
|
|
185
|
+
max_workers = defineConcurrencyLimit(limit=CPUlimit)
|
|
170
186
|
|
|
171
187
|
with ProcessPoolExecutor(max_workers=max_workers) as concurrencyManager:
|
|
172
188
|
dictionaryConcurrency = {concurrencyManager.submit(analyzeAudioFile, pathFilename, listAspectNames)
|
|
@@ -177,18 +193,20 @@ def analyzeAudioListPathFilenames(listPathFilenames: Sequence[str] | Sequence[Pa
|
|
|
177
193
|
cacheAudioAnalyzers.pop(dictionaryConcurrency[claimTicket], None)
|
|
178
194
|
listAspectValues = claimTicket.result()
|
|
179
195
|
rowsListFilenameAspectValues.append(
|
|
180
|
-
[str(pathlib.PurePath(dictionaryConcurrency[claimTicket]).as_posix())]
|
|
196
|
+
[str(pathlib.PurePath(dictionaryConcurrency[claimTicket]).as_posix())] # noqa: RUF005
|
|
181
197
|
+ listAspectValues)
|
|
182
198
|
|
|
183
199
|
return rowsListFilenameAspectValues
|
|
184
200
|
|
|
185
201
|
def getListAvailableAudioAspects() -> list[str]:
|
|
186
202
|
"""
|
|
187
|
-
|
|
188
|
-
|
|
203
|
+
Return a sorted list of audio aspect names. All valid values for the parameter `listAspectNames`, for example, are returned by this function.
|
|
204
|
+
|
|
205
|
+
Returns
|
|
206
|
+
-------
|
|
207
|
+
listAvailableAudioAspects : list of str
|
|
208
|
+
The list of aspect names registered in `audioAspects`.
|
|
189
209
|
|
|
190
|
-
Returns:
|
|
191
|
-
listAvailableAudioAspects: The list of aspect names registered in `audioAspects`.
|
|
192
210
|
"""
|
|
193
211
|
return sorted(audioAspects.keys())
|
|
194
212
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Convert FFprobe output to a standardized Python object."""
|
|
2
|
+
# ruff: noqa: D103
|
|
1
3
|
from collections import defaultdict
|
|
2
4
|
from typing import Any, cast, NamedTuple
|
|
3
5
|
import json
|
|
@@ -11,11 +13,11 @@ import numpy
|
|
|
11
13
|
# NOTE You changed the code because a static type checker was mad at you. Ask yourself,
|
|
12
14
|
# "Are you the tool or is the type checker the tool?"
|
|
13
15
|
|
|
14
|
-
class Blackdetect(NamedTuple):
|
|
16
|
+
class Blackdetect(NamedTuple): # noqa: D101
|
|
15
17
|
black_start: float | None = None
|
|
16
18
|
black_end: float | None = None
|
|
17
19
|
|
|
18
|
-
def pythonizeFFprobe(FFprobeJSON_utf8: str):
|
|
20
|
+
def pythonizeFFprobe(FFprobeJSON_utf8: str) -> tuple[defaultdict[str, Any] | dict[str, Any], dict[str, numpy.ndarray[Any, Any] | dict[str, numpy.ndarray[Any, Any]]]]: # noqa: C901, PLR0912, PLR0915
|
|
19
21
|
FFroot: dict[str, Any] = json.loads(FFprobeJSON_utf8)
|
|
20
22
|
Z0Z_dictionaries: dict[str, numpy.ndarray[Any, Any] | dict[str, numpy.ndarray[Any, Any]]] = {}
|
|
21
23
|
if 'packets_and_frames' in FFroot: # Divide into 'packets' and 'frames'
|
|
@@ -25,7 +27,8 @@ def pythonizeFFprobe(FFprobeJSON_utf8: str):
|
|
|
25
27
|
FFroot[section := packetOrFrame['type'] + 's'].append(packetOrFrame)
|
|
26
28
|
del FFroot[section][-1]['type']
|
|
27
29
|
else:
|
|
28
|
-
|
|
30
|
+
msg = "'packets_and_frames' for the win!"
|
|
31
|
+
raise ValueError(msg)
|
|
29
32
|
del FFroot['packets_and_frames']
|
|
30
33
|
|
|
31
34
|
Z0Z_register = [
|
|
@@ -37,16 +40,16 @@ def pythonizeFFprobe(FFprobeJSON_utf8: str):
|
|
|
37
40
|
leftCrumbs = False
|
|
38
41
|
if 'frames' in FFroot:
|
|
39
42
|
leftCrumbs = False
|
|
40
|
-
# listTuplesBlackdetect = [] # uncommentToFixBlackdetect
|
|
43
|
+
# listTuplesBlackdetect = [] # uncommentToFixBlackdetect # noqa: ERA001
|
|
41
44
|
listTuplesBlackdetect: list[Blackdetect] = []
|
|
42
45
|
for indexFrame, FFframe in enumerate(FFroot['frames']):
|
|
43
46
|
if 'tags' in FFframe:
|
|
44
47
|
if 'lavfi.black_start' in FFframe['tags']:
|
|
45
|
-
# listTuplesBlackdetect.append(float(FFframe['tags']['lavfi.black_start'])) # uncommentToFixBlackdetect
|
|
48
|
+
# listTuplesBlackdetect.append(float(FFframe['tags']['lavfi.black_start'])) # uncommentToFixBlackdetect # noqa: ERA001
|
|
46
49
|
listTuplesBlackdetect.append(Blackdetect(black_start=float(FFframe['tags']['lavfi.black_start'])))
|
|
47
50
|
del FFframe['tags']['lavfi.black_start']
|
|
48
51
|
if 'lavfi.black_end' in FFframe['tags']:
|
|
49
|
-
# listTuplesBlackdetect[-1] = (listTuplesBlackdetect[-1], float(FFframe['tags']['lavfi.black_end'])) # uncommentToFixBlackdetect
|
|
52
|
+
# listTuplesBlackdetect[-1] = (listTuplesBlackdetect[-1], float(FFframe['tags']['lavfi.black_end'])) # uncommentToFixBlackdetect # noqa: ERA001
|
|
50
53
|
tupleBlackdetectLast = listTuplesBlackdetect.pop() if listTuplesBlackdetect else Blackdetect()
|
|
51
54
|
match tupleBlackdetectLast.black_end:
|
|
52
55
|
case None:
|
|
@@ -88,15 +91,15 @@ def pythonizeFFprobe(FFprobeJSON_utf8: str):
|
|
|
88
91
|
if registrant not in Z0Z_dictionaries:
|
|
89
92
|
Z0Z_dictionaries[registrant] = {}
|
|
90
93
|
elif statistic not in Z0Z_dictionaries[registrant]:
|
|
91
|
-
# NOTE (as of this writing) `registrar` can only understand the generic class `numpy.ndarray` and not more specific typing
|
|
92
|
-
valueSherpa = cast(numpy.ndarray, numpy.zeros((channel, len(FFroot['frames']))))
|
|
94
|
+
# NOTE (as of this writing) `registrar` can only understand the generic class `numpy.ndarray` and not more specific typing # noqa: ERA001
|
|
95
|
+
valueSherpa = cast('numpy.ndarray', numpy.zeros((channel, len(FFroot['frames'])))) # pyright: ignore[reportMissingTypeArgument, reportUnknownVariableType]
|
|
93
96
|
Z0Z_dictionaries[registrant][statistic] = valueSherpa
|
|
94
97
|
else:
|
|
95
98
|
raise # Re-raise the exception
|
|
96
99
|
except IndexError:
|
|
97
100
|
if channel > Z0Z_dictionaries[registrant][statistic].shape[0]:
|
|
98
101
|
Z0Z_dictionaries[registrant][statistic] = numpy.resize(Z0Z_dictionaries[registrant][statistic], (channel, len(FFroot['frames'])))
|
|
99
|
-
# Z0Z_dictionaries[registrant][statistic].resize((channel, len(FFroot['frames'])))
|
|
102
|
+
# Z0Z_dictionaries[registrant][statistic].resize((channel, len(FFroot['frames']))) # noqa: ERA001
|
|
100
103
|
else:
|
|
101
104
|
raise # Re-raise the exception
|
|
102
105
|
|
|
@@ -105,7 +108,7 @@ def pythonizeFFprobe(FFprobeJSON_utf8: str):
|
|
|
105
108
|
if FFframe:
|
|
106
109
|
leftCrumbs = True
|
|
107
110
|
if listTuplesBlackdetect:
|
|
108
|
-
# 2025-03-06
|
|
111
|
+
# 2025-03-06 I am _shocked_ that I was able to create a numpy structured array whenever it was when I originally wrote this code.
|
|
109
112
|
arrayBlackdetect = numpy.array(
|
|
110
113
|
[(
|
|
111
114
|
-1.0 if detect.black_start is None else detect.black_start,
|
|
@@ -115,7 +118,7 @@ def pythonizeFFprobe(FFprobeJSON_utf8: str):
|
|
|
115
118
|
copy=False
|
|
116
119
|
)
|
|
117
120
|
Z0Z_dictionaries['blackdetect'] = arrayBlackdetect
|
|
118
|
-
# Z0Z_dictionaries['blackdetect'] = numpy.array(listTuplesBlackdetect, dtype=[('black_start', numpy.float32), ('black_end', numpy.float32)], copy=False) # uncommentToFixBlackdetect
|
|
121
|
+
# Z0Z_dictionaries['blackdetect'] = numpy.array(listTuplesBlackdetect, dtype=[('black_start', numpy.float32), ('black_end', numpy.float32)], copy=False) # uncommentToFixBlackdetect # noqa: ERA001
|
|
119
122
|
if not leftCrumbs:
|
|
120
123
|
del FFroot['frames']
|
|
121
124
|
return FFroot, Z0Z_dictionaries
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: analyzeAudio
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.17
|
|
4
4
|
Summary: Measure one or more aspects of one or more audio files.
|
|
5
5
|
Author-email: Hunter Hogan <HunterHogan@pm.me>
|
|
6
6
|
License: CC-BY-NC-4.0
|
|
7
7
|
Project-URL: Donate, https://www.patreon.com/integrated
|
|
8
8
|
Project-URL: Homepage, https://github.com/hunterhogan/analyzeAudio
|
|
9
|
+
Project-URL: Issues, https://github.com/hunterhogan/
|
|
9
10
|
Project-URL: Repository, https://github.com/hunterhogan/analyzeAudio.git
|
|
10
|
-
Keywords:
|
|
11
|
+
Keywords: FFmpeg,FFprobe,LUFS,RMS,SRMR,analysis,audio,audio-analysis,loudness,measurement,metrics,signal-processing,spectral,spectrum,torch,waveform
|
|
11
12
|
Classifier: Development Status :: 3 - Alpha
|
|
12
13
|
Classifier: Environment :: Console
|
|
13
14
|
Classifier: Intended Audience :: Developers
|
|
14
15
|
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
-
Classifier: Intended Audience :: Science/Research
|
|
16
16
|
Classifier: Intended Audience :: Information Technology
|
|
17
17
|
Classifier: Intended Audience :: Other Audience
|
|
18
|
+
Classifier: Intended Audience :: Science/Research
|
|
18
19
|
Classifier: Natural Language :: English
|
|
19
20
|
Classifier: Operating System :: OS Independent
|
|
20
21
|
Classifier: Programming Language :: Python
|
|
@@ -32,16 +33,17 @@ Classifier: Typing :: Typed
|
|
|
32
33
|
Requires-Python: >=3.10
|
|
33
34
|
Description-Content-Type: text/markdown
|
|
34
35
|
License-File: LICENSE
|
|
36
|
+
Requires-Dist: Z0Z_tools
|
|
35
37
|
Requires-Dist: cachetools
|
|
38
|
+
Requires-Dist: hunterMakesPy
|
|
36
39
|
Requires-Dist: librosa
|
|
37
40
|
Requires-Dist: numpy
|
|
38
|
-
Requires-Dist: optype[numpy]
|
|
39
41
|
Requires-Dist: standard-aifc; python_version >= "3.13"
|
|
40
42
|
Requires-Dist: standard-sunau; python_version >= "3.13"
|
|
41
43
|
Requires-Dist: torch
|
|
42
44
|
Requires-Dist: torchmetrics[audio]
|
|
43
45
|
Requires-Dist: tqdm
|
|
44
|
-
Requires-Dist:
|
|
46
|
+
Requires-Dist: typing_extensions
|
|
45
47
|
Provides-Extra: testing
|
|
46
48
|
Requires-Dist: pytest; extra == "testing"
|
|
47
49
|
Requires-Dist: pytest-cov; extra == "testing"
|
|
@@ -213,4 +215,13 @@ pip install analyzeAudio
|
|
|
213
215
|
[](https://HunterThinks.com/support)
|
|
214
216
|
[](https://www.youtube.com/@HunterHogan)
|
|
215
217
|
|
|
218
|
+
## How to code
|
|
219
|
+
|
|
220
|
+
Coding One Step at a Time:
|
|
221
|
+
|
|
222
|
+
0. WRITE CODE.
|
|
223
|
+
1. Don't write stupid code that's hard to revise.
|
|
224
|
+
2. Write good code.
|
|
225
|
+
3. When revising, write better code.
|
|
226
|
+
|
|
216
227
|
[](https://creativecommons.org/licenses/by-nc/4.0/)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "analyzeAudio"
|
|
3
|
+
version = "0.0.17"
|
|
4
|
+
description = "Measure one or more aspects of one or more audio files."
|
|
5
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { 'text' = "CC-BY-NC-4.0" }
|
|
8
|
+
authors = [{ name = "Hunter Hogan", email = "HunterHogan@pm.me" }]
|
|
9
|
+
keywords = [
|
|
10
|
+
"FFmpeg",
|
|
11
|
+
"FFprobe",
|
|
12
|
+
"LUFS",
|
|
13
|
+
"RMS",
|
|
14
|
+
"SRMR",
|
|
15
|
+
"analysis",
|
|
16
|
+
"audio",
|
|
17
|
+
"audio-analysis",
|
|
18
|
+
"loudness",
|
|
19
|
+
"measurement",
|
|
20
|
+
"metrics",
|
|
21
|
+
"signal-processing",
|
|
22
|
+
"spectral",
|
|
23
|
+
"spectrum",
|
|
24
|
+
"torch",
|
|
25
|
+
"waveform",
|
|
26
|
+
]
|
|
27
|
+
classifiers = [
|
|
28
|
+
"Development Status :: 3 - Alpha",
|
|
29
|
+
"Environment :: Console",
|
|
30
|
+
"Intended Audience :: Developers",
|
|
31
|
+
"Intended Audience :: End Users/Desktop",
|
|
32
|
+
"Intended Audience :: Information Technology",
|
|
33
|
+
"Intended Audience :: Other Audience",
|
|
34
|
+
"Intended Audience :: Science/Research",
|
|
35
|
+
"Natural Language :: English",
|
|
36
|
+
"Operating System :: OS Independent",
|
|
37
|
+
"Programming Language :: Python",
|
|
38
|
+
"Programming Language :: Python :: 3",
|
|
39
|
+
"Programming Language :: Python :: 3.10",
|
|
40
|
+
"Programming Language :: Python :: 3.11",
|
|
41
|
+
"Programming Language :: Python :: 3.12",
|
|
42
|
+
"Programming Language :: Python :: 3.13",
|
|
43
|
+
"Topic :: Multimedia :: Sound/Audio",
|
|
44
|
+
"Topic :: Multimedia :: Sound/Audio :: Analysis",
|
|
45
|
+
"Topic :: Multimedia :: Sound/Audio :: Conversion",
|
|
46
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
47
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
48
|
+
"Typing :: Typed",
|
|
49
|
+
]
|
|
50
|
+
urls = { Donate = "https://www.patreon.com/integrated", Homepage = "https://github.com/hunterhogan/analyzeAudio", Issues = "https://github.com/hunterhogan/", Repository = "https://github.com/hunterhogan/analyzeAudio.git" }
|
|
51
|
+
dependencies = [
|
|
52
|
+
"Z0Z_tools",
|
|
53
|
+
"cachetools",
|
|
54
|
+
"hunterMakesPy",
|
|
55
|
+
"librosa",
|
|
56
|
+
"numpy",
|
|
57
|
+
"standard-aifc;python_version>='3.13'",
|
|
58
|
+
"standard-sunau;python_version>='3.13'",
|
|
59
|
+
"torch",
|
|
60
|
+
"torchmetrics[audio]",
|
|
61
|
+
"tqdm",
|
|
62
|
+
"typing_extensions",
|
|
63
|
+
]
|
|
64
|
+
optional-dependencies = { testing = [
|
|
65
|
+
"pytest",
|
|
66
|
+
"pytest-cov",
|
|
67
|
+
"pytest-xdist",
|
|
68
|
+
"pyupgrade",
|
|
69
|
+
] }
|
|
70
|
+
|
|
71
|
+
[project.scripts]
|
|
72
|
+
whatMeasurements = "analyzeAudio.audioAspectsRegistry:getListAvailableAudioAspects"
|
|
73
|
+
|
|
74
|
+
[build-system]
|
|
75
|
+
requires = ["setuptools"]
|
|
76
|
+
build-backend = "setuptools.build_meta"
|
|
77
|
+
|
|
78
|
+
[tool.coverage]
|
|
79
|
+
report = { exclude_lines = [
|
|
80
|
+
"if TYPE_CHECKING:",
|
|
81
|
+
"if __name__ == \"__main__\":",
|
|
82
|
+
], ignore_errors = false }
|
|
83
|
+
run = { branch = true, concurrency = [
|
|
84
|
+
"multiprocessing",
|
|
85
|
+
], data_file = "tests/coverage/.coverage", omit = [
|
|
86
|
+
"tests/*",
|
|
87
|
+
], parallel = true, source = [
|
|
88
|
+
".",
|
|
89
|
+
] }
|
|
90
|
+
xml = { output = "tests/coverage/coverage.xml" }
|
|
91
|
+
|
|
92
|
+
[tool.pytest.ini_options]
|
|
93
|
+
addopts = ["--color=auto", "-n 4"]
|
|
94
|
+
log_auto_indent = true
|
|
95
|
+
testpaths = ["tests"]
|
|
96
|
+
|
|
97
|
+
[tool.setuptools]
|
|
98
|
+
package-data = { "*" = ["py.typed"] }
|
|
99
|
+
packages = { find = { } }
|
|
100
|
+
|
|
101
|
+
[tool.updateCitation]
|
|
102
|
+
filenameCitationDOTcff = "CITATION.cff"
|
|
103
|
+
pathFilenameCitationSSOT = "citations/CITATION.cff"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from hunterMakesPy.pytestForYourUse import PytestFor_defineConcurrencyLimit, PytestFor_oopsieKwargsie
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
@pytest.mark.parametrize("nameOfTest,callablePytest", PytestFor_defineConcurrencyLimit())
|
|
6
|
+
def testConcurrencyLimit(nameOfTest: str, callablePytest: Callable[[], None]) -> None:
|
|
7
|
+
callablePytest()
|
|
8
|
+
|
|
9
|
+
@pytest.mark.parametrize("nameOfTest,callablePytest", PytestFor_oopsieKwargsie())
|
|
10
|
+
def testOopsieKwargsie(nameOfTest: str, callablePytest: Callable[[], None]) -> None:
|
|
11
|
+
callablePytest()
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
from analyzeAudio import registrationAudioAspect, audioAspects, cacheAudioAnalyzers
|
|
2
|
-
from typing import Any
|
|
3
|
-
import librosa
|
|
4
|
-
import numpy
|
|
5
|
-
from optype.numpy import ToArray2D, AnyFloatingDType
|
|
6
|
-
import cachetools
|
|
7
|
-
|
|
8
|
-
@cachetools.cached(cache=cacheAudioAnalyzers)
|
|
9
|
-
@registrationAudioAspect('Tempogram')
|
|
10
|
-
def analyzeTempogram(waveform: ToArray2D[AnyFloatingDType], sampleRate: int, **keywordArguments: Any) -> numpy.ndarray:
|
|
11
|
-
return librosa.feature.tempogram(y=waveform, sr=sampleRate, **keywordArguments)
|
|
12
|
-
|
|
13
|
-
# "RMS value from audio samples is faster ... However, ... spectrogram ... more accurate ... because ... windowed"
|
|
14
|
-
@registrationAudioAspect('RMS from waveform')
|
|
15
|
-
def analyzeRMS(waveform: ToArray2D[AnyFloatingDType], **keywordArguments: Any) -> numpy.ndarray:
|
|
16
|
-
arrayRMS = librosa.feature.rms(y=waveform, **keywordArguments)
|
|
17
|
-
return 20 * numpy.log10(arrayRMS, where=(arrayRMS != 0)) # dB
|
|
18
|
-
|
|
19
|
-
@registrationAudioAspect('Tempo')
|
|
20
|
-
def analyzeTempo(waveform: ToArray2D[AnyFloatingDType], sampleRate: int, **keywordArguments: Any) -> numpy.ndarray:
|
|
21
|
-
tempogram = audioAspects['Tempogram']['analyzer'](waveform, sampleRate)
|
|
22
|
-
return librosa.feature.tempo(y=waveform, sr=sampleRate, tg=tempogram, **keywordArguments)
|
|
23
|
-
|
|
24
|
-
@registrationAudioAspect('Zero-crossing rate') # This is distinct from 'Zero-crossings rate'
|
|
25
|
-
def analyzeZeroCrossingRate(waveform: ToArray2D[AnyFloatingDType], **keywordArguments: Any) -> numpy.ndarray:
|
|
26
|
-
return librosa.feature.zero_crossing_rate(y=waveform, **keywordArguments)
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
authors = [{ name = "Hunter Hogan", email = "HunterHogan@pm.me" }]
|
|
7
|
-
classifiers = [
|
|
8
|
-
"Development Status :: 3 - Alpha",
|
|
9
|
-
"Environment :: Console",
|
|
10
|
-
"Intended Audience :: Developers",
|
|
11
|
-
"Intended Audience :: End Users/Desktop",
|
|
12
|
-
"Intended Audience :: Science/Research",
|
|
13
|
-
"Intended Audience :: Information Technology",
|
|
14
|
-
"Intended Audience :: Other Audience",
|
|
15
|
-
"Natural Language :: English",
|
|
16
|
-
"Operating System :: OS Independent",
|
|
17
|
-
"Programming Language :: Python",
|
|
18
|
-
"Programming Language :: Python :: 3",
|
|
19
|
-
"Programming Language :: Python :: 3.10",
|
|
20
|
-
"Programming Language :: Python :: 3.11",
|
|
21
|
-
"Programming Language :: Python :: 3.12",
|
|
22
|
-
"Programming Language :: Python :: 3.13",
|
|
23
|
-
"Topic :: Multimedia :: Sound/Audio",
|
|
24
|
-
"Topic :: Multimedia :: Sound/Audio :: Analysis",
|
|
25
|
-
"Topic :: Multimedia :: Sound/Audio :: Conversion",
|
|
26
|
-
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
27
|
-
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
-
"Typing :: Typed",]
|
|
29
|
-
dependencies = [
|
|
30
|
-
"cachetools",
|
|
31
|
-
"librosa",
|
|
32
|
-
"numpy",
|
|
33
|
-
"optype[numpy]",
|
|
34
|
-
"standard-aifc;python_version>='3.13'",
|
|
35
|
-
"standard-sunau;python_version>='3.13'",
|
|
36
|
-
"torch",
|
|
37
|
-
"torchmetrics[audio]",
|
|
38
|
-
"tqdm",
|
|
39
|
-
"Z0Z_tools",]
|
|
40
|
-
description = "Measure one or more aspects of one or more audio files."
|
|
41
|
-
keywords = ["audio", "analysis", "measurement", "metrics", "torch", "spectrum", "spectral", "waveform", "loudness", "LUFS", "RMS", "FFmpeg", "FFprobe", "SRMR", "audio-analysis", "signal-processing"]
|
|
42
|
-
license = { 'text' = "CC-BY-NC-4.0" }
|
|
43
|
-
name = "analyzeAudio"
|
|
44
|
-
optional-dependencies = { testing = [
|
|
45
|
-
"pytest",
|
|
46
|
-
"pytest-cov",
|
|
47
|
-
"pytest-xdist",
|
|
48
|
-
"pyupgrade",] }
|
|
49
|
-
readme = { file = "README.md", content-type = "text/markdown" }
|
|
50
|
-
requires-python = ">=3.10"
|
|
51
|
-
urls = { Donate = "https://www.patreon.com/integrated", Homepage = "https://github.com/hunterhogan/analyzeAudio", Repository = "https://github.com/hunterhogan/analyzeAudio.git" }
|
|
52
|
-
version = "0.0.15"
|
|
53
|
-
|
|
54
|
-
[project.scripts]
|
|
55
|
-
whatMeasurements = "analyzeAudio.audioAspectsRegistry:getListAvailableAudioAspects"
|
|
56
|
-
|
|
57
|
-
[tool.coverage]
|
|
58
|
-
report = { exclude_lines = [
|
|
59
|
-
"if TYPE_CHECKING:",
|
|
60
|
-
"if __name__ == \"__main__\":",
|
|
61
|
-
], ignore_errors = false }
|
|
62
|
-
run = { branch = true, concurrency = [
|
|
63
|
-
"multiprocessing",
|
|
64
|
-
], data_file = "tests/coverage/.coverage", omit = [
|
|
65
|
-
"tests/*",
|
|
66
|
-
], parallel = true, source = [
|
|
67
|
-
".",] }
|
|
68
|
-
xml = { output = "tests/coverage/coverage.xml" }
|
|
69
|
-
|
|
70
|
-
[tool.pytest.ini_options]
|
|
71
|
-
log_auto_indent = true
|
|
72
|
-
addopts = ["--color=yes", "-n 4"]
|
|
73
|
-
testpaths = ["tests"]
|
|
74
|
-
|
|
75
|
-
[tool.setuptools]
|
|
76
|
-
package-data = { "*" = ["py.typed"] }
|
|
77
|
-
packages = { find = {} }
|
|
78
|
-
|
|
79
|
-
[tool.updateCitation]
|
|
80
|
-
filenameCitationDOTcff = 'CITATION.cff'
|
|
81
|
-
pathFilenameCitationSSOT = "citations/CITATION.cff"
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from Z0Z_tools.pytestForYourUse import PytestFor_defineConcurrencyLimit, PytestFor_oopsieKwargsie
|
|
3
|
-
|
|
4
|
-
@pytest.mark.parametrize("nameOfTest,callablePytest", PytestFor_defineConcurrencyLimit())
|
|
5
|
-
def testConcurrencyLimit(nameOfTest, callablePytest):
|
|
6
|
-
callablePytest()
|
|
7
|
-
|
|
8
|
-
@pytest.mark.parametrize("nameOfTest,callablePytest", PytestFor_oopsieKwargsie())
|
|
9
|
-
def testOopsieKwargsie(nameOfTest, callablePytest):
|
|
10
|
-
callablePytest()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|