hunterHearsPy 1.1.0__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 (37) hide show
  1. {hunterhearspy-1.1.0 → hunterhearspy-1.2.0}/PKG-INFO +20 -17
  2. {hunterhearspy-1.1.0 → hunterhearspy-1.2.0}/pyproject.toml +13 -14
  3. hunterhearspy-1.2.0/setup.cfg +4 -0
  4. {hunterhearspy-1.1.0 → hunterhearspy-1.2.0}/src/hunterHearsPy/__init__.py +8 -7
  5. hunterhearspy-1.2.0/src/hunterHearsPy/_arrays.py +206 -0
  6. {hunterhearspy-1.1.0 → hunterhearspy-1.2.0}/src/hunterHearsPy/_fft.py +5 -3
  7. {hunterhearspy-1.1.0 → hunterhearspy-1.2.0}/src/hunterHearsPy/_io.py +11 -23
  8. {hunterhearspy-1.1.0 → hunterhearspy-1.2.0}/src/hunterHearsPy/amplitude.py +90 -70
  9. {hunterhearspy-1.1.0 → hunterhearspy-1.2.0}/src/hunterHearsPy/clippingArrays.py +28 -17
  10. {hunterhearspy-1.1.0 → hunterhearspy-1.2.0}/src/hunterHearsPy/theSSOT.py +19 -7
  11. {hunterhearspy-1.1.0 → hunterhearspy-1.2.0}/src/hunterHearsPy/theTypes.py +47 -27
  12. hunterhearspy-1.2.0/src/hunterHearsPy/windowingFunctions.py +166 -0
  13. hunterhearspy-1.2.0/src/hunterHearsPy/windowingFunctionsTensor.py +132 -0
  14. hunterhearspy-1.2.0/src/hunterHearsPy.egg-info/PKG-INFO +195 -0
  15. hunterhearspy-1.2.0/src/hunterHearsPy.egg-info/SOURCES.txt +32 -0
  16. hunterhearspy-1.2.0/src/hunterHearsPy.egg-info/dependency_links.txt +1 -0
  17. hunterhearspy-1.2.0/src/hunterHearsPy.egg-info/requires.txt +17 -0
  18. hunterhearspy-1.2.0/src/hunterHearsPy.egg-info/top_level.txt +1 -0
  19. hunterhearspy-1.2.0/tests/test_arrays.py +76 -0
  20. hunterhearspy-1.2.0/tests/test_io.py +30 -0
  21. hunterhearspy-1.2.0/tests/test_old_amplitude.py +100 -0
  22. hunterhearspy-1.2.0/tests/test_old_autoRevert.py +46 -0
  23. hunterhearspy-1.2.0/tests/test_old_clippingArrays.py +98 -0
  24. hunterhearspy-1.2.0/tests/test_old_spectrogram.py +246 -0
  25. hunterhearspy-1.2.0/tests/test_old_waveform.py +167 -0
  26. hunterhearspy-1.2.0/tests/test_resample.py +51 -0
  27. hunterhearspy-1.2.0/tests/test_windowingFunctions.py +51 -0
  28. hunterhearspy-1.2.0/tests/test_windowingFunctionsTensor.py +80 -0
  29. hunterhearspy-1.1.0/src/hunterHearsPy/_arrays.py +0 -109
  30. hunterhearspy-1.1.0/src/hunterHearsPy/windowingFunctions.py +0 -211
  31. hunterhearspy-1.1.0/src/hunterHearsPy/windowingFunctionsTensor.py +0 -168
  32. {hunterhearspy-1.1.0 → hunterhearspy-1.2.0}/LICENSE +0 -0
  33. {hunterhearspy-1.1.0 → hunterhearspy-1.2.0}/README.md +0 -0
  34. {hunterhearspy-1.1.0 → hunterhearspy-1.2.0}/src/hunterHearsPy/_resample.py +0 -0
  35. {hunterhearspy-1.1.0 → hunterhearspy-1.2.0}/src/hunterHearsPy/autoRevert.py +0 -0
  36. {hunterhearspy-1.1.0 → hunterhearspy-1.2.0}/src/hunterHearsPy/dataBaskets.py +0 -0
  37. {hunterhearspy-1.1.0 → 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.1.0
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
@@ -19,26 +23,25 @@ Classifier: Programming Language :: Python :: 3.12
19
23
  Classifier: Programming Language :: Python :: 3.13
20
24
  Classifier: Programming Language :: Python :: 3.14
21
25
  Classifier: Typing :: Typed
22
- 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
23
31
  Requires-Dist: numpy
24
32
  Requires-Dist: resampy
25
33
  Requires-Dist: scipy
26
34
  Requires-Dist: soundfile>=0.14.0
27
35
  Requires-Dist: tqdm
28
36
  Requires-Dist: typing-extensions>=4.0.0
29
- Requires-Dist: z0z-tools>=2.1.0
30
- Requires-Dist: torch ; extra == 'torch'
31
- Maintainer: Hunter Hogan
32
- Maintainer-email: Hunter Hogan <HunterHogan@pm.me>
33
- Requires-Python: >=3.10
34
- Project-URL: Context7, https://context7.com/hunterhogan/hunterhearspy
35
- Project-URL: Donate, https://www.patreon.com/integrated
36
- Project-URL: Download, https://pypi.org/project/hunterHearsPy
37
- Project-URL: Homepage, https://github.com/hunterhogan/hunterHearsPy
38
- Project-URL: Issues, https://github.com/hunterhogan/hunterHearsPy/issues
39
- Project-URL: Repository, https://github.com/hunterhogan/hunterHearsPy.git
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"
40
42
  Provides-Extra: torch
41
- Description-Content-Type: text/markdown
43
+ Requires-Dist: torch; extra == "torch"
44
+ Dynamic: license-file
42
45
 
43
46
  # hunterHearsPy
44
47
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hunterHearsPy"
3
- version = "1.1.0"
3
+ version = "1.2.0"
4
4
  description = "Audio processing."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -24,6 +24,7 @@ classifiers = [
24
24
  "Typing :: Typed",
25
25
  ]
26
26
  dependencies = [
27
+ "Z0Z_tools>=2.1.0",
27
28
  "hunterMakesPy>=0.7.0",
28
29
  "numpy",
29
30
  "resampy",
@@ -31,9 +32,13 @@ dependencies = [
31
32
  "soundfile>=0.14.0",
32
33
  "tqdm",
33
34
  "typing-extensions>=4.0.0",
34
- "Z0Z_tools>=2.1.0",
35
35
  ]
36
- optional-dependencies = { torch = ["torch"] }
36
+ optional-dependencies = { testing = [
37
+ "pytest",
38
+ "pytest-mock",
39
+ "pytest-xdist",
40
+ "torch"
41
+ ], torch = ["torch"] }
37
42
 
38
43
  [project.urls]
39
44
  Context7 = "https://context7.com/hunterhogan/hunterhearspy"
@@ -44,18 +49,12 @@ Issues = "https://github.com/hunterhogan/hunterHearsPy/issues"
44
49
  Repository = "https://github.com/hunterhogan/hunterHearsPy.git"
45
50
 
46
51
  [dependency-groups]
47
- dev = [
48
- "astToolkit",
49
- "pytest-cov",
50
- "scipy-stubs",
51
- "types-resampy",
52
- ]
52
+ dev = ["astToolkit", "pytest-cov", "scipy-stubs", "types-resampy"]
53
53
  pytorch = ["torch==2.10.0"]
54
- testing = ["pytest", "pytest-mock", "pytest-xdist", "torch"]
55
54
 
56
55
  [build-system]
57
- requires = ["uv_build>=0.11.8,<0.12.0"]
58
- build-backend = "uv_build"
56
+ requires = ["setuptools"]
57
+ build-backend = "setuptools.build_meta"
59
58
 
60
59
  [tool.coverage]
61
60
  report = { exclude_lines = ["if TYPE_CHECKING:", "if __name__ == \"__main__\":"], ignore_errors = false }
@@ -64,7 +63,7 @@ run = { branch = true, concurrency = ["multiprocessing"], data_file = "tests/cov
64
63
  ], parallel = true, source = ["."] }
65
64
 
66
65
  [tool.pytest]
67
- addopts = ["--color=auto", "-n 4", "--tb=line"]
66
+ addopts = ["--color=auto", "--tb=line", "-n 4"]
68
67
  log_auto_indent = "On"
69
68
  pythonpath = ["src"]
70
69
  testpaths = ["tests"]
@@ -75,7 +74,7 @@ pathFilenameCitationSSOT = "citations/CITATION.cff"
75
74
 
76
75
  [tool.uv]
77
76
  build-backend = { module-name = "hunterHearsPy", module-root = "src" }
78
- default-groups = ["dev", "testing"]
77
+ default-groups = ["dev"]
79
78
 
80
79
  [[tool.uv.index]]
81
80
  # NOTE I use this to install torch+cuda on my computer that doesn't have cuda. 2026-06-13
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -2,14 +2,15 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  from hunterHearsPy.theTypes import (
5
- ArraySpectrograms as ArraySpectrograms, ArrayWaveforms as ArrayWaveforms, ArrayWaveformsFloating as ArrayWaveformsFloating,
6
- ArrayWaveformsShape as ArrayWaveformsShape, callableReturnsNDArray as callableReturnsNDArray, E733TH4X0R as E733TH4X0R,
7
- FileDescriptorOrPath as FileDescriptorOrPath, NormalizationReverter as NormalizationReverter, OptionsAlign as OptionsAlign,
8
- Parameters_stft as Parameters_stft, ParametersShortTimeFFT as ParametersShortTimeFFT, Spectrogram as Spectrogram,
9
- SpectrogramDtype as SpectrogramDtype, Waveform as Waveform, WaveformAxes as WaveformAxes, WaveformDtype as WaveformDtype,
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,
10
11
  WaveformFloating as WaveformFloating, WaveformFloatingDtype as WaveformFloatingDtype, WaveformMetadata as WaveformMetadata,
11
- WindowingFunction as WindowingFunction, WindowingFunctionDtype as WindowingFunctionDtype, 个 as 个, 形floating as 形floating,
12
- 形ndarray as 形ndarray, 形Shape as 形Shape)
12
+ WaveformShape as WaveformShape, WindowingFunction as WindowingFunction, WindowingFunctionDtype as WindowingFunctionDtype, 个 as 个,
13
+ 形floating as 形floating, 形ndarray as 形ndarray, 形Shape as 形Shape)
13
14
 
14
15
  # isort: split
15
16
  from hunterHearsPy.windowingFunctions import cosineWings as cosineWings, equalPower as equalPower, halfsine as halfsine, tukey as tukey
@@ -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
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from humpy_cytoolz.dicttoolz import keyfilter, merge
4
- from hunterHearsPy import amplitudeIntegerToFloating, Parameters_stft, ParametersShortTimeFFT, setting, Translator
4
+ from hunterHearsPy import amplitudeIntegerToFloating, ParametersShortTimeFFT, setting, Translator
5
5
  from scipy.signal import ShortTimeFFT
6
6
  from typing import overload, TYPE_CHECKING
7
7
  from typing_extensions import Unpack
@@ -9,7 +9,8 @@ import numpy
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from collections.abc import Callable
12
- from hunterHearsPy import ArraySpectrograms, ArrayWaveforms, ArrayWaveformsFloating, Spectrogram, Waveform, WaveformFloating
12
+ from hunterHearsPy import (
13
+ ArraySpectrograms, ArrayWaveforms, ArrayWaveformsFloating, Parameters_stft, Spectrogram, Waveform, WaveformFloating)
13
14
  from pathlib import PurePath
14
15
  from scipy.signal._short_time_fft import _PadType
15
16
 
@@ -45,7 +46,8 @@ def stft(arrayTarget: Waveform | ArrayWaveforms | Spectrogram | ArraySpectrogram
45
46
  )
46
47
  raise ValueError(message)
47
48
 
48
- parametersShortTimeFFT = Translator(**ParametersShortTimeFFT(keyfilter(setting.ShortTimeFFT.keys().__contains__, merge(setting.ShortTimeFFT, keywordArguments)))) # pyright: ignore[reportArgumentType] # ty:ignore[invalid-argument-type]
49
+ parametersShortTimeFFT = Translator(**ParametersShortTimeFFT(
50
+ keyfilter(setting.ShortTimeFFT.keys().__contains__, merge(setting.ShortTimeFFT, keywordArguments)))) # pyright: ignore[reportArgumentType] # ty:ignore[invalid-argument-type]
49
51
 
50
52
  padding: _PadType = keywordArguments.get('padding', setting.padding)
51
53
 
@@ -1,10 +1,9 @@
1
- # pyright: reportArgumentType=false
2
- # ty:ignore[invalid-assignment]
3
1
  from __future__ import annotations
4
2
 
5
3
  from hunterHearsPy import amplitudeToSoundfile, getAxis, resampleWaveform, setting, stft
6
4
  from hunterMakesPy.filesystemToolkit import makeDirectorySafely
7
5
  from pathlib import Path, PurePath
6
+ from soundfile import AudioData
8
7
  from typing import TYPE_CHECKING
9
8
  from typing_extensions import Unpack
10
9
  import numpy
@@ -13,22 +12,11 @@ import tempfile
13
12
  import uuid
14
13
 
15
14
  if TYPE_CHECKING:
16
- from hunterHearsPy import FileDescriptorOrPath, Parameters_stft, Spectrogram, Waveform, WaveformAxes
15
+ from hunterHearsPy import AxisMetadata, FileDescriptorOrPath, Parameters_stft, Spectrogram, Waveform
17
16
  from numpy import dtype, ndarray
18
- from soundfile import dtype_str as Options_dtype_str
17
+ from soundfile import AudioData_2d, dtype_str as Options_dtype_str
19
18
  from typing import Any
20
19
 
21
- # TODO. The typing has too many moving parts.
22
- # 1. This function: that should be the easiest--if I get the other parts right.
23
- # 2. `SoundFile.read(dtype=setting.dtype_str` is dynamic, and the static checker is using the type of
24
- # the field, `dtype_str: soundfile_dtype_str`, `Literal["float64", "float32", "int32", "int16"]`
25
- # 3. `Soundfile.read` has decent typing, but I have total control because I have a custom stub file in
26
- # `stubFileNotFound`.
27
- # 4. `resampy` accepts integer or floating input and returns float32 or the same floating type.
28
-
29
- # AND NOW, `writeWAV` is pulled into the problem.
30
-
31
- # This is a general purpose function: it is not a subroutine of _arrays.
32
20
  def readAudioFile(pathFilename: FileDescriptorOrPath, sampleRateDesired: float | None = None, dtype_str: Options_dtype_str | None = None) -> Waveform:
33
21
  """Read an audio file and return waveform data as a NumPy array.
34
22
 
@@ -54,11 +42,11 @@ def readAudioFile(pathFilename: FileDescriptorOrPath, sampleRateDesired: float |
54
42
  sampleRateDesired = sampleRateDesired or setting.sampleRate
55
43
  with soundfile.SoundFile(pathFilename) as readSoundFile:
56
44
  sampleRateSource: int = readSoundFile.samplerate
57
- waveform: Waveform = readSoundFile.read(dtype=dtype_str or setting.dtype_str, always_2d=True)
58
- axis: dict[str, WaveformAxes] = getAxis()
59
- waveform = waveform.transpose((axis['time'].number, axis['channel'].number))
45
+ audioData: AudioData_2d = readSoundFile.read(dtype=dtype_str or setting.dtype_str, always_2d=True) # ty:ignore[invalid-assignment] https://github.com/astral-sh/ty/issues/2799
46
+ axis: dict[str, AxisMetadata] = getAxis()
47
+ waveform: Waveform = audioData.transpose((axis['time'].number, axis['channel'].number))
60
48
 
61
- return resampleWaveform(waveform, sampleRateDesired, sampleRateSource, axis['time'].number)
49
+ return resampleWaveform(waveform, sampleRateDesired, sampleRateSource, axis['time'].number) # ty:ignore[no-matching-overload] https://github.com/astral-sh/ty/issues/2799
62
50
 
63
51
  def writeWAV(pathFilename: FileDescriptorOrPath, waveform: Waveform, sampleRate: float | None = None, subtype: str | None = None) -> FileDescriptorOrPath:
64
52
  """Write a waveform array to a WAV file.
@@ -100,11 +88,11 @@ def writeWAV(pathFilename: FileDescriptorOrPath, waveform: Waveform, sampleRate:
100
88
  subtype = subtype or setting.subtype
101
89
  makeDirectorySafely(pathFilename)
102
90
 
103
- waveform = amplitudeToSoundfile(waveform)
91
+ audioData: AudioData = amplitudeToSoundfile(waveform)
104
92
 
105
- waveform = waveform.transpose()
93
+ audioData = audioData.transpose()
106
94
 
107
- soundfile.write(file=pathFilename, data=waveform, samplerate=sampleRate, subtype=subtype, format='WAV')
95
+ soundfile.write(file=pathFilename, data=audioData, samplerate=sampleRate, subtype=subtype, format='WAV')
108
96
  return pathFilename
109
97
 
110
98
  def spectrogramToWAV(spectrogram: Spectrogram, pathFilename: FileDescriptorOrPath, lengthWaveform: int, **parametersSTFT: Unpack[Parameters_stft]) -> None:
@@ -144,7 +132,7 @@ def spectrogramToWAV(spectrogram: Spectrogram, pathFilename: FileDescriptorOrPat
144
132
  sampleRate: float = parametersSTFT.get('sampleRate', setting.sampleRate)
145
133
  writeWAV(pathFilename, waveform, sampleRate)
146
134
 
147
- def saveOnError(arrayTarget: ndarray[tuple[int, ...], dtype[Any]], *, identifierTarget: str = 'arrayTarget') -> PurePath:
135
+ def saveOnError(arrayTarget: ndarray[tuple[Any, ...], dtype[Any]], *, identifierTarget: str = 'arrayTarget') -> PurePath:
148
136
  pathFilename: Path = Path(tempfile.mkdtemp(prefix='hunterHearsPy'), f"{identifierTarget}_{uuid.uuid4().hex}.npy").resolve()
149
137
  numpy.save(pathFilename, arrayTarget)
150
138