hunterHearsPy 1.0.3__tar.gz → 1.2.0__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.
Files changed (41) hide show
  1. {hunterhearspy-1.0.3 → hunterhearspy-1.2.0}/PKG-INFO +96 -16
  2. hunterhearspy-1.2.0/README.md +150 -0
  3. {hunterhearspy-1.0.3 → hunterhearspy-1.2.0}/pyproject.toml +28 -10
  4. hunterhearspy-1.2.0/setup.cfg +4 -0
  5. hunterhearspy-1.2.0/src/hunterHearsPy/__init__.py +52 -0
  6. hunterhearspy-1.2.0/src/hunterHearsPy/_arrays.py +206 -0
  7. hunterhearspy-1.2.0/src/hunterHearsPy/_fft.py +129 -0
  8. hunterhearspy-1.2.0/src/hunterHearsPy/_io.py +139 -0
  9. hunterhearspy-1.2.0/src/hunterHearsPy/_resample.py +43 -0
  10. hunterhearspy-1.2.0/src/hunterHearsPy/amplitude.py +273 -0
  11. {hunterhearspy-1.0.3 → hunterhearspy-1.2.0}/src/hunterHearsPy/autoRevert.py +5 -5
  12. {hunterhearspy-1.0.3 → hunterhearspy-1.2.0}/src/hunterHearsPy/clippingArrays.py +34 -21
  13. hunterhearspy-1.2.0/src/hunterHearsPy/dataBaskets.py +34 -0
  14. hunterhearspy-1.2.0/src/hunterHearsPy/theSSOT.py +113 -0
  15. hunterhearspy-1.2.0/src/hunterHearsPy/theTypes.py +121 -0
  16. hunterhearspy-1.2.0/src/hunterHearsPy/windowingFunctions.py +166 -0
  17. hunterhearspy-1.2.0/src/hunterHearsPy/windowingFunctionsTensor.py +132 -0
  18. hunterhearspy-1.2.0/src/hunterHearsPy.egg-info/PKG-INFO +195 -0
  19. hunterhearspy-1.2.0/src/hunterHearsPy.egg-info/SOURCES.txt +32 -0
  20. hunterhearspy-1.2.0/src/hunterHearsPy.egg-info/dependency_links.txt +1 -0
  21. hunterhearspy-1.2.0/src/hunterHearsPy.egg-info/requires.txt +17 -0
  22. hunterhearspy-1.2.0/src/hunterHearsPy.egg-info/top_level.txt +1 -0
  23. hunterhearspy-1.2.0/tests/test_arrays.py +76 -0
  24. hunterhearspy-1.2.0/tests/test_io.py +30 -0
  25. hunterhearspy-1.2.0/tests/test_old_amplitude.py +100 -0
  26. hunterhearspy-1.2.0/tests/test_old_autoRevert.py +46 -0
  27. hunterhearspy-1.2.0/tests/test_old_clippingArrays.py +98 -0
  28. hunterhearspy-1.2.0/tests/test_old_spectrogram.py +246 -0
  29. hunterhearspy-1.2.0/tests/test_old_waveform.py +167 -0
  30. hunterhearspy-1.2.0/tests/test_resample.py +51 -0
  31. hunterhearspy-1.2.0/tests/test_windowingFunctions.py +51 -0
  32. hunterhearspy-1.2.0/tests/test_windowingFunctionsTensor.py +80 -0
  33. hunterhearspy-1.0.3/README.md +0 -78
  34. hunterhearspy-1.0.3/src/hunterHearsPy/__init__.py +0 -36
  35. hunterhearspy-1.0.3/src/hunterHearsPy/amplitude.py +0 -170
  36. hunterhearspy-1.0.3/src/hunterHearsPy/ioAudio.py +0 -663
  37. hunterhearspy-1.0.3/src/hunterHearsPy/theTypes.py +0 -118
  38. hunterhearspy-1.0.3/src/hunterHearsPy/windowingFunctions.py +0 -211
  39. hunterhearspy-1.0.3/src/hunterHearsPy/windowingFunctionsTensor.py +0 -168
  40. {hunterhearspy-1.0.3 → hunterhearspy-1.2.0}/LICENSE +0 -0
  41. {hunterhearspy-1.0.3 → hunterhearspy-1.2.0}/src/hunterHearsPy/py.typed +0 -0
@@ -1,12 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hunterHearsPy
3
- Version: 1.0.3
3
+ Version: 1.2.0
4
4
  Summary: Audio processing.
5
- Keywords:
6
- Author: Hunter Hogan
7
5
  Author-email: Hunter Hogan <HunterHogan@pm.me>
6
+ Maintainer-email: Hunter Hogan <HunterHogan@pm.me>
8
7
  License-Expression: CC-BY-NC-4.0
9
- License-File: LICENSE
8
+ Project-URL: Context7, https://context7.com/hunterhogan/hunterhearspy
9
+ Project-URL: Donate, https://www.patreon.com/integrated
10
+ Project-URL: Download, https://pypi.org/project/hunterHearsPy
11
+ Project-URL: Homepage, https://github.com/hunterhogan/hunterHearsPy
12
+ Project-URL: Issues, https://github.com/hunterhogan/hunterHearsPy/issues
13
+ Project-URL: Repository, https://github.com/hunterhogan/hunterHearsPy.git
10
14
  Classifier: Development Status :: 4 - Beta
11
15
  Classifier: Environment :: Console
12
16
  Classifier: Intended Audience :: Developers
@@ -14,26 +18,30 @@ Classifier: Intended Audience :: Science/Research
14
18
  Classifier: Natural Language :: English
15
19
  Classifier: Operating System :: OS Independent
16
20
  Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
17
24
  Classifier: Programming Language :: Python :: 3.14
18
25
  Classifier: Typing :: Typed
19
- Requires-Dist: huntermakespy>=0.7.0
26
+ Requires-Python: >=3.10
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: Z0Z_tools>=2.1.0
30
+ Requires-Dist: hunterMakesPy>=0.7.0
20
31
  Requires-Dist: numpy
21
32
  Requires-Dist: resampy
22
33
  Requires-Dist: scipy
23
34
  Requires-Dist: soundfile>=0.14.0
24
35
  Requires-Dist: tqdm
25
- Requires-Dist: torch ; extra == 'torch'
26
- Maintainer: Hunter Hogan
27
- Maintainer-email: Hunter Hogan <HunterHogan@pm.me>
28
- Requires-Python: >=3.10
29
- Project-URL: Context7, https://context7.com/hunterhogan/hunterhearspy
30
- Project-URL: Donate, https://www.patreon.com/integrated
31
- Project-URL: Download, https://pypi.org/project/hunterHearsPy
32
- Project-URL: Homepage, https://github.com/hunterhogan/hunterHearsPy
33
- Project-URL: Issues, https://github.com/hunterhogan/hunterHearsPy/issues
34
- Project-URL: Repository, https://github.com/hunterhogan/hunterHearsPy.git
36
+ Requires-Dist: typing-extensions>=4.0.0
37
+ Provides-Extra: testing
38
+ Requires-Dist: pytest; extra == "testing"
39
+ Requires-Dist: pytest-mock; extra == "testing"
40
+ Requires-Dist: pytest-xdist; extra == "testing"
41
+ Requires-Dist: torch; extra == "testing"
35
42
  Provides-Extra: torch
36
- Description-Content-Type: text/markdown
43
+ Requires-Dist: torch; extra == "torch"
44
+ Dynamic: license-file
37
45
 
38
46
  # hunterHearsPy
39
47
 
@@ -101,6 +109,78 @@ processor = waveformSpectrogramWaveform(boost_low_frequencies)
101
109
  processed_waveform = processor(original_waveform)
102
110
  ```
103
111
 
112
+ ## Development
113
+
114
+ I want:
115
+
116
+ - consistent waveforms,
117
+ - consistent spectrograms, and
118
+ - efficient, reliable transformations.
119
+
120
+ Therefore, I want one package, this package, to manage those objectives with a single source of truth for configurable universal settings.
121
+
122
+ - I need good default universal settings.
123
+ - I need an easy way to change a universal setting.
124
+ - I want _ad hoc_ overrides for some or all universal settings.
125
+
126
+ I don't know a good way to implement user-configurable universal settings. Therefore, as I normally do, I will use my HARDCODED system as a placeholder.
127
+
128
+ ### Semiotics
129
+
130
+ - channel, rarely channels.
131
+ - time is more generic than samples and often preferred.
132
+ - array is generic.
133
+ - when talking about a NumPy `ndarray`, write `ndarray` not array.
134
+ - `sampleRate` is giving me problems in the "ingest" functions.
135
+ - Regularly review these semiotics until the system is clear.
136
+ - Most of the time, `sampleRate` is descriptive: this is the sample rate. See `writeWAV`.
137
+ - In `readAudioFile`, the parameter is prescriptive: make this the sample rate. Even worse, the
138
+ attribute of the current sample rate is `readSoundFile.samplerate`, which is descriptive, of course.
139
+ - However, `def resampleWaveform(waveform, sampleRateDesired: float, sampleRateSource: float):` has
140
+ excellent semiotics because it does not use `sampleRate`.
141
+
142
+ ### Preferred packages
143
+
144
+ - NumPy
145
+ - scipy
146
+ - hunterMakesPy
147
+ - tqdm (for status messages)
148
+ - cytoolz via the [Z0Z_tools](https://github.com/hunterhogan/Z0Z_tools) package (until it finds a forever home)
149
+ - more_itertools
150
+ - `astToolKit`
151
+ - `pytest`
152
+
153
+ ### Probably won't need
154
+
155
+ - `analyzeAudio`
156
+ - `gmpy2`
157
+ - `numba`
158
+ - `platformdirs`
159
+ - `sympy`
160
+ - `torch-einops-kit`
161
+
162
+ ### Not using `PyTorch` for "business" logic
163
+
164
+ - But, I must have `torch` compatibility.
165
+ - At a minimum, a transformation to and from `torch` that the user must call.
166
+ - `astToolkit` easily creates real `torch` APIs and identifiers, however, for the `windowingFunctions` module in `windowingFunctionsTensor`.
167
+
168
+ ### Not using librosa
169
+
170
+ - Dependency bloated.
171
+ - Slow.
172
+ - Less precise than I want.
173
+ - I generally dislike the API and identifiers.
174
+
175
+ ### Disfavored packages
176
+
177
+ - `pandas`
178
+
179
+ ### More vectorization
180
+
181
+ - Few or no `for` loops.
182
+ - Few or no `for` object comprehensions.
183
+
104
184
  ## Installation
105
185
 
106
186
  ```bash
@@ -0,0 +1,150 @@
1
+ # hunterHearsPy
2
+
3
+ A comprehensive collection of Python utilities for audio processing.
4
+
5
+ ## Audio Processing Made Simple
6
+
7
+ ### Load and Save Audio Files
8
+
9
+ Read audio files with automatic stereo conversion and sample rate control:
10
+
11
+ ```python
12
+ from hunterHearsPy import readAudioFile, writeWAV
13
+
14
+ # Load audio with sample rate conversion
15
+ waveform = readAudioFile('input.wav', sampleRate=44100)
16
+
17
+ # Save in WAV format (always 32-bit float)
18
+ writeWAV('output.wav', waveform)
19
+ ```
20
+
21
+ ### Process Multiple Audio Files at Once
22
+
23
+ Load and process batches of audio files:
24
+
25
+ ```python
26
+ from hunterHearsPy import loadWaveforms
27
+
28
+ # Load multiple files with consistent formatting
29
+ array_waveforms = loadWaveforms(['file1.wav', 'file2.wav', 'file3.wav'])
30
+
31
+ # The result is a unified array with shape (channels, samples, file_count)
32
+ ```
33
+
34
+ ### Work with Spectrograms
35
+
36
+ Convert between waveforms and spectrograms:
37
+
38
+ ```python
39
+ from hunterHearsPy import stft, halfsine
40
+
41
+ # Create a spectrogram with a half-sine window
42
+ spectrogram = stft(waveform, windowingFunction=halfsine(1024))
43
+
44
+ # Convert back to a waveform
45
+ reconstructed = stft(spectrogram, inverse=True, lengthWaveform=original_length)
46
+ ```
47
+
48
+ ### Process Audio in the Frequency Domain
49
+
50
+ Create functions that operate on spectrograms:
51
+
52
+ ```python
53
+ from hunterHearsPy import waveformSpectrogramWaveform
54
+
55
+ def boost_low_frequencies(spectrogram):
56
+ # Boost frequencies below 500 Hz
57
+ spectrogram[:, :10, :] *= 2.0
58
+ return spectrogram
59
+
60
+ # Create a processor that handles the STFT/ISTFT automatically
61
+ processor = waveformSpectrogramWaveform(boost_low_frequencies)
62
+
63
+ # Apply the processor to a waveform
64
+ processed_waveform = processor(original_waveform)
65
+ ```
66
+
67
+ ## Development
68
+
69
+ I want:
70
+
71
+ - consistent waveforms,
72
+ - consistent spectrograms, and
73
+ - efficient, reliable transformations.
74
+
75
+ Therefore, I want one package, this package, to manage those objectives with a single source of truth for configurable universal settings.
76
+
77
+ - I need good default universal settings.
78
+ - I need an easy way to change a universal setting.
79
+ - I want _ad hoc_ overrides for some or all universal settings.
80
+
81
+ I don't know a good way to implement user-configurable universal settings. Therefore, as I normally do, I will use my HARDCODED system as a placeholder.
82
+
83
+ ### Semiotics
84
+
85
+ - channel, rarely channels.
86
+ - time is more generic than samples and often preferred.
87
+ - array is generic.
88
+ - when talking about a NumPy `ndarray`, write `ndarray` not array.
89
+ - `sampleRate` is giving me problems in the "ingest" functions.
90
+ - Regularly review these semiotics until the system is clear.
91
+ - Most of the time, `sampleRate` is descriptive: this is the sample rate. See `writeWAV`.
92
+ - In `readAudioFile`, the parameter is prescriptive: make this the sample rate. Even worse, the
93
+ attribute of the current sample rate is `readSoundFile.samplerate`, which is descriptive, of course.
94
+ - However, `def resampleWaveform(waveform, sampleRateDesired: float, sampleRateSource: float):` has
95
+ excellent semiotics because it does not use `sampleRate`.
96
+
97
+ ### Preferred packages
98
+
99
+ - NumPy
100
+ - scipy
101
+ - hunterMakesPy
102
+ - tqdm (for status messages)
103
+ - cytoolz via the [Z0Z_tools](https://github.com/hunterhogan/Z0Z_tools) package (until it finds a forever home)
104
+ - more_itertools
105
+ - `astToolKit`
106
+ - `pytest`
107
+
108
+ ### Probably won't need
109
+
110
+ - `analyzeAudio`
111
+ - `gmpy2`
112
+ - `numba`
113
+ - `platformdirs`
114
+ - `sympy`
115
+ - `torch-einops-kit`
116
+
117
+ ### Not using `PyTorch` for "business" logic
118
+
119
+ - But, I must have `torch` compatibility.
120
+ - At a minimum, a transformation to and from `torch` that the user must call.
121
+ - `astToolkit` easily creates real `torch` APIs and identifiers, however, for the `windowingFunctions` module in `windowingFunctionsTensor`.
122
+
123
+ ### Not using librosa
124
+
125
+ - Dependency bloated.
126
+ - Slow.
127
+ - Less precise than I want.
128
+ - I generally dislike the API and identifiers.
129
+
130
+ ### Disfavored packages
131
+
132
+ - `pandas`
133
+
134
+ ### More vectorization
135
+
136
+ - Few or no `for` loops.
137
+ - Few or no `for` object comprehensions.
138
+
139
+ ## Installation
140
+
141
+ ```bash
142
+ pip install hunterHearsPy
143
+ ```
144
+
145
+ ## My recovery
146
+
147
+ [![Static Badge](https://img.shields.io/badge/2011_August-Homeless_since-blue?style=flat)](https://HunterThinks.com/support)
148
+ [![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UC3Gx7kz61009NbhpRtPP7tw)](https://www.youtube.com/@HunterHogan)
149
+
150
+ [![CC-BY-NC-4.0](https://raw.githubusercontent.com/hunterhogan/hunterHearsPy/refs/heads/main/.github/CC-BY-NC-4.0.png)](https://creativecommons.org/licenses/by-nc/4.0/)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hunterHearsPy"
3
- version = "1.0.3"
3
+ version = "1.2.0"
4
4
  description = "Audio processing."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -17,18 +17,28 @@ classifiers = [
17
17
  "Natural Language :: English",
18
18
  "Operating System :: OS Independent",
19
19
  "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
20
23
  "Programming Language :: Python :: 3.14",
21
24
  "Typing :: Typed",
22
25
  ]
23
26
  dependencies = [
27
+ "Z0Z_tools>=2.1.0",
24
28
  "hunterMakesPy>=0.7.0",
25
29
  "numpy",
26
30
  "resampy",
27
31
  "scipy",
28
32
  "soundfile>=0.14.0",
29
33
  "tqdm",
34
+ "typing-extensions>=4.0.0",
30
35
  ]
31
- optional-dependencies = { torch = ["torch"] }
36
+ optional-dependencies = { testing = [
37
+ "pytest",
38
+ "pytest-mock",
39
+ "pytest-xdist",
40
+ "torch"
41
+ ], torch = ["torch"] }
32
42
 
33
43
  [project.urls]
34
44
  Context7 = "https://context7.com/hunterhogan/hunterhearspy"
@@ -40,12 +50,11 @@ Repository = "https://github.com/hunterhogan/hunterHearsPy.git"
40
50
 
41
51
  [dependency-groups]
42
52
  dev = ["astToolkit", "pytest-cov", "scipy-stubs", "types-resampy"]
43
- pytorch = ["torch==2.10.0", "torchaudio==2.10.0"]
44
- testing = ["pytest", "pytest-mock", "pytest-xdist", "torch"]
53
+ pytorch = ["torch==2.10.0"]
45
54
 
46
55
  [build-system]
47
- requires = ["uv_build>=0.11.8,<0.12.0"]
48
- build-backend = "uv_build"
56
+ requires = ["setuptools"]
57
+ build-backend = "setuptools.build_meta"
49
58
 
50
59
  [tool.coverage]
51
60
  report = { exclude_lines = ["if TYPE_CHECKING:", "if __name__ == \"__main__\":"], ignore_errors = false }
@@ -54,7 +63,7 @@ run = { branch = true, concurrency = ["multiprocessing"], data_file = "tests/cov
54
63
  ], parallel = true, source = ["."] }
55
64
 
56
65
  [tool.pytest]
57
- addopts = ["--color=auto", "-n 4"]
66
+ addopts = ["--color=auto", "--tb=line", "-n 4"]
58
67
  log_auto_indent = "On"
59
68
  pythonpath = ["src"]
60
69
  testpaths = ["tests"]
@@ -65,13 +74,22 @@ pathFilenameCitationSSOT = "citations/CITATION.cff"
65
74
 
66
75
  [tool.uv]
67
76
  build-backend = { module-name = "hunterHearsPy", module-root = "src" }
68
- default-groups = ["dev", "testing"]
77
+ default-groups = ["dev"]
69
78
 
70
79
  [[tool.uv.index]]
80
+ # NOTE I use this to install torch+cuda on my computer that doesn't have cuda. 2026-06-13
81
+ # `uv sync --group pytorch` OR `uv sync --all-groups` will trigger the following.
82
+ # dependency-groups.pytorch amends the dependencies: `torch`, limitation `==2.10.0`, group = "pytorch".
83
+ # At this point, the default source, pypi, is the highest priority on the `torch` sources list. (LIFO)
84
+ # tool.uv.sources sees '`torch`, group = "pytorch"' so pushes "indexPytorch" on top the `torch` sources list.
85
+ # tool.uv.index translates "indexPytorch" to the url of a package index.
86
+ # uv queries the package index for all `torch` that match all limitations.
87
+ # If the package index returns any matches, uv will install the "best" match.
88
+ # Because the package index only has cuda 12.8 versions, uv installs torch+cu128.
89
+ # I picked torch==2.10.0 + cuda 12.8 because that is the pre-installed package on Google Colab T4.
71
90
  explicit = true
72
91
  name = "indexPytorch"
73
92
  url = "https://download.pytorch.org/whl/cu128"
74
93
 
75
94
  [tool.uv.sources]
76
- torch = { index = "indexPytorch", group = "pytorch" }
77
- torchaudio = { index = "indexPytorch", group = "pytorch" }
95
+ torch = { group = "pytorch", index = "indexPytorch" }
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,52 @@
1
+ # ruff: noqa: D104
2
+ from __future__ import annotations
3
+
4
+ from hunterHearsPy.theTypes import (
5
+ ArraySpectrograms as ArraySpectrograms, ArraySpectrogramsShape as ArraySpectrogramsShape, ArrayWaveforms as ArrayWaveforms,
6
+ ArrayWaveformsFloating as ArrayWaveformsFloating, ArrayWaveformsShape as ArrayWaveformsShape, AxisMetadata as AxisMetadata,
7
+ callableReturnsNDArray as callableReturnsNDArray, E733TH4X0R as E733TH4X0R, FileDescriptorOrPath as FileDescriptorOrPath,
8
+ NormalizationReverter as NormalizationReverter, OptionsAlign as OptionsAlign, Parameters_loadSpectrograms as Parameters_loadSpectrograms,
9
+ Parameters_loadWaveforms as Parameters_loadWaveforms, Parameters_stft as Parameters_stft, ParametersShortTimeFFT as ParametersShortTimeFFT,
10
+ Spectrogram as Spectrogram, SpectrogramDtype as SpectrogramDtype, Waveform as Waveform, WaveformDtype as WaveformDtype,
11
+ WaveformFloating as WaveformFloating, WaveformFloatingDtype as WaveformFloatingDtype, WaveformMetadata as WaveformMetadata,
12
+ WaveformShape as WaveformShape, WindowingFunction as WindowingFunction, WindowingFunctionDtype as WindowingFunctionDtype, 个 as 个,
13
+ 形floating as 形floating, 形ndarray as 形ndarray, 形Shape as 形Shape)
14
+
15
+ # isort: split
16
+ from hunterHearsPy.windowingFunctions import cosineWings as cosineWings, equalPower as equalPower, halfsine as halfsine, tukey as tukey
17
+
18
+ # isort: split
19
+ from contextlib import suppress
20
+
21
+ with suppress(ModuleNotFoundError): # noqa: RUF067
22
+ from hunterHearsPy.windowingFunctionsTensor import (
23
+ cosineWingsTensor as cosineWingsTensor, equalPowerTensor as equalPowerTensor, halfsineTensor as halfsineTensor,
24
+ tukeyTensor as tukeyTensor)
25
+
26
+ # isort: split
27
+ from hunterHearsPy.theSSOT import getAxis as getAxis, setting as setting
28
+
29
+ # isort: split
30
+ from hunterHearsPy.dataBaskets import Translator as Translator
31
+
32
+ # isort: split
33
+ from hunterHearsPy._resample import resampleWaveform as resampleWaveform
34
+ from hunterHearsPy.amplitude import (
35
+ amplitudeIntegerToFloating as amplitudeIntegerToFloating, amplitudeToSoundfile as amplitudeToSoundfile,
36
+ normalizeArrayWaveforms as normalizeArrayWaveforms, normalizeWaveform as normalizeWaveform)
37
+
38
+ # isort: split
39
+ from hunterHearsPy._fft import stft as stft, waveformSpectrogramWaveform as waveformSpectrogramWaveform
40
+
41
+ # isort: split
42
+ from hunterHearsPy._io import readAudioFile as readAudioFile, spectrogramToWAV as spectrogramToWAV, writeWAV as writeWAV
43
+
44
+ # isort: split
45
+ from hunterHearsPy.clippingArrays import applyHardLimit as applyHardLimit, applyHardLimitComplexValued as applyHardLimitComplexValued
46
+
47
+ # isort: split
48
+ from hunterHearsPy.autoRevert import moveToAxisOfOperation as moveToAxisOfOperation
49
+
50
+ # isort: split
51
+ from hunterHearsPy._arrays import (
52
+ getWaveformMetadata as getWaveformMetadata, loadSpectrograms as loadSpectrograms, loadWaveforms as loadWaveforms)
@@ -0,0 +1,206 @@
1
+ # ruff: noqa: DOC201
2
+ from __future__ import annotations
3
+
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ from hunterHearsPy import (
6
+ ArraySpectrogramsShape, ArrayWaveformsShape, AxisMetadata, getAxis, readAudioFile, setting, stft, WaveformMetadata, WaveformShape)
7
+ from hunterMakesPy.parseParameters import defineConcurrencyLimit
8
+ from tqdm.auto import tqdm
9
+ from typing import TYPE_CHECKING
10
+ from typing_extensions import Unpack
11
+ import numpy
12
+ import sys
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Sequence
16
+ from hunterHearsPy import (
17
+ ArraySpectrograms, ArrayWaveforms, FileDescriptorOrPath, OptionsAlign, Parameters_loadSpectrograms, Parameters_loadWaveforms, Waveform)
18
+ from numpy.lib._arraypad_impl import _ModeKind
19
+ from numpy.typing import DTypeLike
20
+ from soundfile import dtype_str as Options_dtype_str
21
+ from typing import Any
22
+
23
+ def getWaveformMetadata(
24
+ listPathFilenames: Sequence[FileDescriptorOrPath], sampleRate: float, align: OptionsAlign
25
+ ) -> tuple[dict[int, WaveformMetadata], dict[str, AxisMetadata]]:
26
+ """Retrieve metadata for a collection of audio waveform files."""
27
+ #============== Initialize ==========================================================
28
+
29
+ axis: dict[str, AxisMetadata] = getAxis()
30
+ channelMaximum: int = 0
31
+ dictionaryWaveformMetadata: dict[int, WaveformMetadata] = {}
32
+ lengthMaximum: int = 0
33
+
34
+ #============== Populate ===========================================================
35
+
36
+ if len(listPathFilenames) == 0:
37
+ message: str = f'I received `{len(listPathFilenames) = }`, so `arrayWaveforms` will have zero-sized axes.'
38
+ sys.stderr.write(message + '\n')
39
+
40
+ for index, pathFilename in enumerate(tqdm(listPathFilenames, desc='Preparing combined array', leave=False)):
41
+ channels, lengthWaveform = readAudioFile(pathFilename, sampleRate).shape
42
+ dictionaryWaveformMetadata[index] = WaveformMetadata(
43
+ channels=channels, lengthWaveform=lengthWaveform, pathFilename=pathFilename, samplesStart=0, samplesStop=0
44
+ )
45
+ channelMaximum = max(channelMaximum, channels)
46
+ lengthMaximum = max(lengthMaximum, lengthWaveform)
47
+
48
+ axis['channel'] = AxisMetadata(number=axis['channel'].number, size=channelMaximum)
49
+ axis['indexing'] = AxisMetadata(number=axis['indexing'].number, size=len(listPathFilenames))
50
+ axis['time'] = AxisMetadata(number=axis['time'].number, size=lengthMaximum)
51
+
52
+ #============== Calculate ===========================================================
53
+
54
+ multiplicandSamplesStart: float = max((align == 'center') / 2, align == 'stop')
55
+
56
+ for metadata in dictionaryWaveformMetadata.values():
57
+ samplesPadding: int = axis['time'].size - metadata['lengthWaveform']
58
+ # TODO document that if `samplesPadding` is odd, the extra pad-sample is added to samplesStop.
59
+ metadata['samplesStart'] = int(samplesPadding * multiplicandSamplesStart)
60
+ metadata['samplesStop'] = metadata['samplesStart'] + metadata['lengthWaveform']
61
+
62
+ return dictionaryWaveformMetadata, axis
63
+
64
+ def loadWaveforms(listPathFilenames: Sequence[FileDescriptorOrPath], *, CPUlimit: bool | float | int | None = None, **keywordArguments: Unpack[Parameters_loadWaveforms]) -> ArrayWaveforms:
65
+ """Load a list of audio files into a single stacked NumPy array."""
66
+ #============== Initialize ==========================================================
67
+ align: OptionsAlign = keywordArguments.get('align', setting.align)
68
+ dtype: DTypeLike = keywordArguments.get('dtype', setting.dtypeWaveform)
69
+ dtype_str: Options_dtype_str = keywordArguments.get('dtype_str', setting.dtype_str)
70
+ sampleRateDesired: float = keywordArguments.get('sampleRateDesired', setting.sampleRate)
71
+
72
+ max_workers: int = defineConcurrencyLimit(limit=CPUlimit)
73
+
74
+ #============== Allocate memory ==========================================================
75
+ dictionaryWaveformMetadata, axis = getWaveformMetadata(listPathFilenames, sampleRateDesired, align)
76
+
77
+ arrayWaveforms: ArrayWaveforms = numpy.zeros(ArrayWaveformsShape(*(entry.size for entry in sorted(axis.values()))), dtype)
78
+
79
+ #============== Concurrent loading ==========================================================
80
+ # TODO the axis order is hardcoded.
81
+ def workhorse(index: int, metadata: WaveformMetadata) -> None:
82
+ arrayWaveforms[:, metadata['samplesStart'] : metadata['samplesStop'], index] = readAudioFile(
83
+ metadata['pathFilename'], sampleRateDesired, dtype_str
84
+ ).astype(dtype, copy=False)
85
+
86
+ with ThreadPoolExecutor(max_workers=max_workers) as threadManager:
87
+ tuple(tqdm(threadManager.map(workhorse, dictionaryWaveformMetadata, dictionaryWaveformMetadata.values()), total=len(dictionaryWaveformMetadata)))
88
+
89
+ """# DEVELOPMENT
90
+ test_loadWaveforms_align: this is an exception to the rule of 1 test function for non-error outcomes per function. The not-commented-out code in align.py is my proof of concept for this test. But I DO NOT want you to mimic the code I wrote. I want you to understand what we want to test, then I want you think about a generalized test function. I designed this ArrayWaveforms such that if all of the waveforms are align=start (which means "pad the end of the shorter waveforms"), then for the duration of the shortest waveform, that data is identical in all of the waveforms. In this case, the shortest is 20 seconds at 44100 Hz, so the first 44100*20 samples of the left channel in all 5 waveforms match each other and the first 44100*20 samples of the right channel in all 5 waveforms match each other. Said, differently, if you truncate to the shortest waveform, then all of the waveforms should be identical.
91
+
92
+ If align=stop, then if you truncate to the shortest waveform by removing the beginning of the longer waveforms, then all waveforms are identical.
93
+
94
+ If align=center, truncate by removing equal amounts from the beginning and end, and the waveforms are identical.
95
+
96
+ That is the test I want you to make. The first three parameters are the same as test_loadWaveforms because they are necessary to get the array. Then use the align parameter in the test function to truncate/trim the array to the shortest waveform. Then do a round-robin assert_array_equal for each waveform in the ArrayWaveforms.
97
+
98
+ How long is the shortest waveform? Well, fuck.
99
+
100
+ # TODO Do I need to return `dictionaryWaveformMetadata`? In addition to the padding data, it is a convenient way to get the original pathFilename.
101
+ I wrote a bunch of notes about this. I guess they are in old commits.
102
+ Instead of returning a simple tuple, I could return a named tuple, which is tuple+, of course.
103
+ """
104
+
105
+ return arrayWaveforms
106
+
107
+ def loadSpectrograms(listPathFilenames: Sequence[FileDescriptorOrPath], *, CPUlimit: bool | float | int | None = None, **keywordArguments: Any) -> tuple[ArraySpectrograms, dict[int, WaveformMetadata]]:
108
+ """Load spectrograms from a list of audio files."""
109
+ #============== Initialize ==========================================================
110
+
111
+ align: OptionsAlign = keywordArguments.get('align', setting.align)
112
+ align_pad_mode: _ModeKind = keywordArguments.get('align_pad_mode', setting.align_pad_mode)
113
+ dtype: DTypeLike = keywordArguments.get('dtype', setting.dtypeSpectrogram)
114
+ dtype_str: Options_dtype_str = keywordArguments.get('dtype_str', setting.dtype_str)
115
+ dtypeWaveform: DTypeLike = keywordArguments.get('dtypeWaveform', setting.dtypeWaveform)
116
+ sampleRateDesired: float = keywordArguments.get('sampleRateDesired', setting.sampleRate)
117
+
118
+ max_workers: int = defineConcurrencyLimit(limit=CPUlimit)
119
+
120
+ #============== Allocate memory ==========================================================
121
+
122
+ dictionaryWaveformMetadata, axis = getWaveformMetadata(listPathFilenames, sampleRateDesired, align)
123
+
124
+ waveformShape = WaveformShape(*(entry.size for entry in sorted((axis['channel'], axis['time']))))
125
+ axisSpectrogram = AxisMetadata(setting.axisSpectrogramIndexing, len(dictionaryWaveformMetadata))
126
+ arraySpectrogramsShape: list[int] = list(stft(numpy.zeros(waveformShape, dtypeWaveform), **keywordArguments).shape)
127
+ arraySpectrogramsShape.insert(axisSpectrogram.number, axisSpectrogram.size)
128
+
129
+ arraySpectrograms: ArraySpectrograms = numpy.zeros(ArraySpectrogramsShape(*arraySpectrogramsShape), dtype)
130
+
131
+ #============== Concurrent loading ==========================================================
132
+
133
+ def workhorse(index: int, metadata: WaveformMetadata) -> None:
134
+ # TODO make tests that can check if this works.
135
+
136
+ pad_width: dict[int, tuple[int, int]] = {
137
+ axis['channel'].number: (0, 0)
138
+ , axis['time'].number: (metadata['samplesStart'], axis['time'].size - metadata['samplesStop'])
139
+ }
140
+
141
+ # python3.10: TypeError: `pad_width` must be of integral type.
142
+ # f.m.l.
143
+ # TODO in 2026 October: drop Py3.10 and this crap. I just want semantic, dynamic access to the data! It's the year 2026, ffs!
144
+
145
+ pad_widthIntegralType: tuple[tuple[int, int], ...] = tuple(dict(sorted(pad_width.items())).values())
146
+
147
+ # TODO axisSpectrogram.number is hardcoded here. grr!
148
+ arraySpectrograms[..., index] = stft(
149
+ numpy.pad(
150
+ readAudioFile(metadata['pathFilename'], sampleRateDesired, dtype_str).astype(dtypeWaveform, copy=False)
151
+ , pad_widthIntegralType, mode=align_pad_mode)
152
+ , **keywordArguments
153
+ )
154
+
155
+ if False: # This fails the current tests.
156
+ arraySpectrogramsShape[axisSpectrogram.number] = index
157
+ indices = numpy.indices(tuple(arraySpectrogramsShape))
158
+ numpy.put_along_axis(arraySpectrograms, indices=indices, values=stft(
159
+ numpy.pad(
160
+ readAudioFile(metadata['pathFilename'], sampleRateDesired, dtype_str).astype(dtypeWaveform, copy=False)
161
+ , pad_widthIntegralType, mode=align_pad_mode)
162
+ , **keywordArguments
163
+ ), axis=axisSpectrogram.number
164
+ )
165
+
166
+ with ThreadPoolExecutor(max_workers=max_workers) as threadManager:
167
+ tuple(tqdm(threadManager.map(workhorse, dictionaryWaveformMetadata.keys(), dictionaryWaveformMetadata.values()), desc='Loading spectrograms', total=axisSpectrogram.size))
168
+
169
+ return arraySpectrograms, dictionaryWaveformMetadata
170
+
171
+ def BACKUPloadSpectrograms(listPathFilenames: Sequence[FileDescriptorOrPath], *, CPUlimit: bool | float | int | None = None, **keywordArguments: Any) -> tuple[ArraySpectrograms, dict[int, WaveformMetadata]]:
172
+ """Load spectrograms from a list of audio files."""
173
+ #============== Initialize ==========================================================
174
+
175
+ align: OptionsAlign = keywordArguments.get('align', setting.align)
176
+ dtype: DTypeLike = keywordArguments.get('dtype', setting.dtypeSpectrogram)
177
+ dtype_str: Options_dtype_str = keywordArguments.get('dtype_str', setting.dtype_str)
178
+ dtypeWaveform: DTypeLike = keywordArguments.get('dtypeWaveform', setting.dtypeWaveform)
179
+ sampleRateDesired: float = keywordArguments.get('sampleRateDesired', setting.sampleRate)
180
+
181
+ max_workers: int = defineConcurrencyLimit(limit=CPUlimit)
182
+
183
+ #============== Allocate memory ==========================================================
184
+
185
+ dictionaryWaveformMetadata, axis = getWaveformMetadata(listPathFilenames, sampleRateDesired, align)
186
+
187
+ waveformShape = WaveformShape(*(entry.size for entry in sorted((axis['channel'], axis['time']))))
188
+ waveformZeros: Waveform = numpy.zeros(waveformShape, dtypeWaveform)
189
+
190
+ axisSpectrogram = AxisMetadata(setting.axisSpectrogramIndexing, len(dictionaryWaveformMetadata))
191
+
192
+ arraySpectrogramsShape: list[int] = list(stft(waveformZeros, **keywordArguments).shape)
193
+ arraySpectrogramsShape.insert(axisSpectrogram.number, axisSpectrogram.size)
194
+ arraySpectrograms: ArraySpectrograms = numpy.zeros(ArraySpectrogramsShape(*arraySpectrogramsShape), dtype)
195
+
196
+ #============== Concurrent loading ==========================================================
197
+
198
+ def workhorse(index: int, metadata: WaveformMetadata) -> None:
199
+ waveform: Waveform = waveformZeros.copy()
200
+ waveform[:, metadata['samplesStart'] : metadata['samplesStop']] = readAudioFile(metadata['pathFilename'], sampleRateDesired, dtype_str).astype(dtypeWaveform, copy=False)
201
+ arraySpectrograms[..., index] = stft(waveform, **keywordArguments)
202
+
203
+ with ThreadPoolExecutor(max_workers=max_workers) as threadManager:
204
+ tuple(tqdm(threadManager.map(workhorse, dictionaryWaveformMetadata.keys(), dictionaryWaveformMetadata.values()), desc='Loading spectrograms', total=axisSpectrogram.size))
205
+
206
+ return arraySpectrograms, dictionaryWaveformMetadata