hunterHearsPy 1.1.0__tar.gz → 1.3.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 (42) hide show
  1. {hunterhearspy-1.1.0 → hunterhearspy-1.3.0}/PKG-INFO +20 -17
  2. {hunterhearspy-1.1.0 → hunterhearspy-1.3.0}/pyproject.toml +13 -14
  3. hunterhearspy-1.3.0/setup.cfg +4 -0
  4. {hunterhearspy-1.1.0 → hunterhearspy-1.3.0}/src/hunterHearsPy/__init__.py +8 -14
  5. hunterhearspy-1.3.0/src/hunterHearsPy/_arrays.py +239 -0
  6. hunterhearspy-1.3.0/src/hunterHearsPy/_fft.py +145 -0
  7. {hunterhearspy-1.1.0 → hunterhearspy-1.3.0}/src/hunterHearsPy/_io.py +12 -23
  8. {hunterhearspy-1.1.0 → hunterhearspy-1.3.0}/src/hunterHearsPy/_resample.py +1 -5
  9. hunterhearspy-1.3.0/src/hunterHearsPy/amplitude.py +265 -0
  10. {hunterhearspy-1.1.0 → hunterhearspy-1.3.0}/src/hunterHearsPy/autoRevert.py +3 -4
  11. {hunterhearspy-1.1.0 → hunterhearspy-1.3.0}/src/hunterHearsPy/clippingArrays.py +28 -17
  12. hunterhearspy-1.3.0/src/hunterHearsPy/dataBaskets.py +69 -0
  13. {hunterhearspy-1.1.0 → hunterhearspy-1.3.0}/src/hunterHearsPy/theSSOT.py +21 -7
  14. hunterhearspy-1.3.0/src/hunterHearsPy/theTypes.py +99 -0
  15. hunterhearspy-1.3.0/src/hunterHearsPy/windowingFunctions.py +166 -0
  16. hunterhearspy-1.3.0/src/hunterHearsPy/windowingFunctionsTensor.py +133 -0
  17. hunterhearspy-1.3.0/src/hunterHearsPy.egg-info/PKG-INFO +195 -0
  18. hunterhearspy-1.3.0/src/hunterHearsPy.egg-info/SOURCES.txt +33 -0
  19. hunterhearspy-1.3.0/src/hunterHearsPy.egg-info/dependency_links.txt +1 -0
  20. hunterhearspy-1.3.0/src/hunterHearsPy.egg-info/requires.txt +17 -0
  21. hunterhearspy-1.3.0/src/hunterHearsPy.egg-info/top_level.txt +1 -0
  22. hunterhearspy-1.3.0/tests/test_arrays.py +84 -0
  23. hunterhearspy-1.3.0/tests/test_fft.py +81 -0
  24. hunterhearspy-1.3.0/tests/test_io.py +30 -0
  25. hunterhearspy-1.3.0/tests/test_old_amplitude.py +102 -0
  26. hunterhearspy-1.3.0/tests/test_old_autoRevert.py +46 -0
  27. hunterhearspy-1.3.0/tests/test_old_clippingArrays.py +98 -0
  28. hunterhearspy-1.3.0/tests/test_old_spectrogram.py +100 -0
  29. hunterhearspy-1.3.0/tests/test_old_waveform.py +167 -0
  30. hunterhearspy-1.3.0/tests/test_resample.py +51 -0
  31. hunterhearspy-1.3.0/tests/test_windowingFunctions.py +51 -0
  32. hunterhearspy-1.3.0/tests/test_windowingFunctionsTensor.py +80 -0
  33. hunterhearspy-1.1.0/src/hunterHearsPy/_arrays.py +0 -109
  34. hunterhearspy-1.1.0/src/hunterHearsPy/_fft.py +0 -127
  35. hunterhearspy-1.1.0/src/hunterHearsPy/amplitude.py +0 -253
  36. hunterhearspy-1.1.0/src/hunterHearsPy/dataBaskets.py +0 -34
  37. hunterhearspy-1.1.0/src/hunterHearsPy/theTypes.py +0 -101
  38. hunterhearspy-1.1.0/src/hunterHearsPy/windowingFunctions.py +0 -211
  39. hunterhearspy-1.1.0/src/hunterHearsPy/windowingFunctionsTensor.py +0 -168
  40. {hunterhearspy-1.1.0 → hunterhearspy-1.3.0}/LICENSE +0 -0
  41. {hunterhearspy-1.1.0 → hunterhearspy-1.3.0}/README.md +0 -0
  42. {hunterhearspy-1.1.0 → hunterhearspy-1.3.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.3.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.3.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
+
@@ -1,15 +1,7 @@
1
1
  # ruff: noqa: D104
2
2
  from __future__ import annotations
3
3
 
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,
10
- 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)
4
+ from hunterHearsPy import theTypes as theTypes
13
5
 
14
6
  # isort: split
15
7
  from hunterHearsPy.windowingFunctions import cosineWings as cosineWings, equalPower as equalPower, halfsine as halfsine, tukey as tukey
@@ -23,10 +15,12 @@ with suppress(ModuleNotFoundError): # noqa: RUF067
23
15
  tukeyTensor as tukeyTensor)
24
16
 
25
17
  # isort: split
26
- from hunterHearsPy.theSSOT import getAxis as getAxis, setting as setting
18
+ from hunterHearsPy.dataBaskets import (
19
+ AxisMetadata as AxisMetadata, SpectrogramsAndMetadata as SpectrogramsAndMetadata, WaveformMetadata as WaveformMetadata,
20
+ WaveformsAndMetadata as WaveformsAndMetadata)
27
21
 
28
22
  # isort: split
29
- from hunterHearsPy.dataBaskets import Translator as Translator
23
+ from hunterHearsPy.theSSOT import getAxis as getAxis, setting as setting
30
24
 
31
25
  # isort: split
32
26
  from hunterHearsPy._resample import resampleWaveform as resampleWaveform
@@ -34,6 +28,9 @@ from hunterHearsPy.amplitude import (
34
28
  amplitudeIntegerToFloating as amplitudeIntegerToFloating, amplitudeToSoundfile as amplitudeToSoundfile,
35
29
  normalizeArrayWaveforms as normalizeArrayWaveforms, normalizeWaveform as normalizeWaveform)
36
30
 
31
+ # isort: split
32
+ from hunterHearsPy.autoRevert import moveToAxisOfOperation as moveToAxisOfOperation
33
+
37
34
  # isort: split
38
35
  from hunterHearsPy._fft import stft as stft, waveformSpectrogramWaveform as waveformSpectrogramWaveform
39
36
 
@@ -43,9 +40,6 @@ from hunterHearsPy._io import readAudioFile as readAudioFile, spectrogramToWAV a
43
40
  # isort: split
44
41
  from hunterHearsPy.clippingArrays import applyHardLimit as applyHardLimit, applyHardLimitComplexValued as applyHardLimitComplexValued
45
42
 
46
- # isort: split
47
- from hunterHearsPy.autoRevert import moveToAxisOfOperation as moveToAxisOfOperation
48
-
49
43
  # isort: split
50
44
  from hunterHearsPy._arrays import (
51
45
  getWaveformMetadata as getWaveformMetadata, loadSpectrograms as loadSpectrograms, loadWaveforms as loadWaveforms)
@@ -0,0 +1,239 @@
1
+ # ruff: noqa: DOC201
2
+ from __future__ import annotations
3
+
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ from hunterHearsPy import (
6
+ AxisMetadata, getAxis, readAudioFile, setting, SpectrogramsAndMetadata, stft, WaveformMetadata, WaveformsAndMetadata)
7
+ from hunterMakesPy.parseParameters import defineConcurrencyLimit
8
+ from tqdm.auto import tqdm
9
+ from typing import NamedTuple, 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.theTypes 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
+
22
+ #======== Idiosyncratic classes tell type checkers about the shape of arrays ======================
23
+
24
+ class Axes2(NamedTuple):
25
+ a0: int
26
+ a1: int
27
+
28
+ class Axes3(NamedTuple):
29
+ a0: int
30
+ a1: int
31
+ a2: int
32
+
33
+ class Axes4(NamedTuple):
34
+ a0: int
35
+ a1: int
36
+ a2: int
37
+ a3: int
38
+
39
+ def getWaveformMetadata(
40
+ listPathFilenames: Sequence[FileDescriptorOrPath], sampleRate: float, align: OptionsAlign
41
+ ) -> tuple[dict[int, WaveformMetadata], dict[str, AxisMetadata]]:
42
+ """Retrieve metadata for a collection of audio waveform files."""
43
+ #============== Initialize ==========================================================
44
+
45
+ axis: dict[str, AxisMetadata] = getAxis()
46
+ channelMaximum: int = 0
47
+ dictionaryWaveformMetadata: dict[int, WaveformMetadata] = {}
48
+ lengthMaximum: int = 0
49
+
50
+ #============== Populate ===========================================================
51
+
52
+ if len(listPathFilenames) == 0:
53
+ message: str = f'I received `{len(listPathFilenames) = }`, so `arrayWaveforms` will have zero-sized axes.'
54
+ sys.stderr.write(message + '\n')
55
+
56
+ for index, pathFilename in enumerate(tqdm(listPathFilenames, desc='Preparing combined array', leave=False)):
57
+ channels, lengthWaveform = readAudioFile(pathFilename, sampleRate).shape
58
+ dictionaryWaveformMetadata[index] = WaveformMetadata(
59
+ channels=channels, lengthWaveform=lengthWaveform, pathFilename=pathFilename, samplesStart=0, samplesStop=0
60
+ )
61
+ channelMaximum = max(channelMaximum, channels)
62
+ lengthMaximum = max(lengthMaximum, lengthWaveform)
63
+
64
+ axis['channel'] = AxisMetadata(number=axis['channel'].number, size=channelMaximum)
65
+ axis['indexing'] = AxisMetadata(number=axis['indexing'].number, size=len(listPathFilenames))
66
+ axis['time'] = AxisMetadata(number=axis['time'].number, size=lengthMaximum)
67
+
68
+ #============== Calculate ===========================================================
69
+
70
+ multiplicandSamplesStart: float = max((align == 'center') / 2, align == 'stop')
71
+
72
+ for metadata in dictionaryWaveformMetadata.values():
73
+ samplesPadding: int = axis['time'].size - metadata['lengthWaveform']
74
+ # TODO document that if `samplesPadding` is odd, the extra pad-sample is added to samplesStop.
75
+ metadata['samplesStart'] = int(samplesPadding * multiplicandSamplesStart)
76
+ metadata['samplesStop'] = metadata['samplesStart'] + metadata['lengthWaveform']
77
+
78
+ return dictionaryWaveformMetadata, axis
79
+
80
+ def loadWaveforms(
81
+ listPathFilenames: Sequence[FileDescriptorOrPath]
82
+ , *
83
+ , CPUlimit: bool | float | int | None = None
84
+ , **keywordArguments: Unpack[Parameters_loadWaveforms]
85
+ ) -> WaveformsAndMetadata:
86
+ """Load a list of audio files into a single stacked NumPy array."""
87
+ #============== Initialize ==========================================================
88
+
89
+ align: OptionsAlign = keywordArguments.get('align', setting.align)
90
+ dtypeWaveform: DTypeLike = keywordArguments.get('dtypeWaveform', setting.dtypeWaveform)
91
+ dtype_str: Options_dtype_str = keywordArguments.get('dtype_str', setting.dtype_str)
92
+ sampleRate: float = keywordArguments.get('sampleRate', setting.sampleRate)
93
+
94
+ max_workers: int = defineConcurrencyLimit(limit=CPUlimit)
95
+
96
+ #============== Allocate memory ==========================================================
97
+
98
+ dictionaryWaveformMetadata, axis = getWaveformMetadata(listPathFilenames, sampleRate, align)
99
+ arrayWaveforms: ArrayWaveforms = numpy.zeros(Axes3(*(entry.size for entry in sorted(axis.values()))), dtypeWaveform)
100
+
101
+ #============== Concurrent loading ==========================================================
102
+
103
+ # TODO the axis order is hardcoded.
104
+ def workhorse(index: int, metadata: WaveformMetadata) -> None:
105
+ arrayWaveforms[:, metadata['samplesStart'] : metadata['samplesStop'], index] = readAudioFile(
106
+ metadata['pathFilename'], sampleRate, dtype_str
107
+ ).astype(dtypeWaveform, copy=False)
108
+
109
+ with ThreadPoolExecutor(max_workers=max_workers) as threadManager:
110
+ tuple(tqdm(threadManager.map(workhorse, dictionaryWaveformMetadata, dictionaryWaveformMetadata.values())
111
+ , desc='Loading waveforms.'
112
+ , total=len(dictionaryWaveformMetadata)
113
+ ))
114
+
115
+ return WaveformsAndMetadata(array=arrayWaveforms, metadata=dictionaryWaveformMetadata)
116
+
117
+ def loadSpectrograms(
118
+ listPathFilenames: Sequence[FileDescriptorOrPath], *, CPUlimit: bool | float | int | None = None, **keywordArguments: Unpack[Parameters_loadSpectrograms]
119
+ ) -> SpectrogramsAndMetadata:
120
+ """Load spectrograms from a list of audio files."""
121
+ #============== Initialize ==========================================================
122
+
123
+ align: OptionsAlign = keywordArguments.get('align', setting.align)
124
+ align_pad_mode: _ModeKind = keywordArguments.get('align_pad_mode', setting.align_pad_mode)
125
+ dtypeSpectrogram: DTypeLike = keywordArguments.get('dtypeSpectrogram', setting.dtypeSpectrogram)
126
+ dtype_str: Options_dtype_str = keywordArguments.get('dtype_str', setting.dtype_str)
127
+ dtypeWaveform: DTypeLike = keywordArguments.get('dtypeWaveform', setting.dtypeWaveform)
128
+ sampleRate: float = keywordArguments.get('sampleRate', setting.sampleRate)
129
+
130
+ max_workers: int = defineConcurrencyLimit(limit=CPUlimit)
131
+
132
+ #============== Allocate memory ==========================================================
133
+
134
+ dictionaryWaveformMetadata, axis = getWaveformMetadata(listPathFilenames, sampleRate, align)
135
+
136
+ waveformShape = Axes2(*(entry.size for entry in sorted((axis['channel'], axis['time']))))
137
+ axisSpectrogram = AxisMetadata(setting.axisSpectrogramIndexing, len(dictionaryWaveformMetadata))
138
+ arraySpectrogramsShape: list[int] = list(stft(numpy.zeros(waveformShape, dtypeWaveform), **keywordArguments).shape)
139
+ arraySpectrogramsShape.insert(axisSpectrogram.number, axisSpectrogram.size)
140
+
141
+ arraySpectrograms: ArraySpectrograms = numpy.zeros(Axes4(*arraySpectrogramsShape), dtypeSpectrogram)
142
+
143
+ #============== Concurrent loading ==========================================================
144
+
145
+ def workhorse(index: int, metadata: WaveformMetadata) -> None:
146
+ # TODO make tests that can check if this works.
147
+
148
+ pad_width: dict[int, tuple[int, int]] = {
149
+ axis['channel'].number: (0, 0)
150
+ , axis['time'].number: (metadata['samplesStart'], axis['time'].size - metadata['samplesStop'])
151
+ }
152
+
153
+ # python3.10: TypeError: `pad_width` must be of integral type.
154
+ # f.m.l.
155
+ # 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!
156
+
157
+ pad_widthIntegralType: tuple[tuple[int, int], ...] = tuple(dict(sorted(pad_width.items())).values())
158
+
159
+ # TODO axisSpectrogram.number is hardcoded here. grr!
160
+ arraySpectrograms[..., index] = stft(
161
+ numpy.pad(
162
+ readAudioFile(metadata['pathFilename'], sampleRate, dtype_str).astype(dtypeWaveform, copy=False)
163
+ , pad_widthIntegralType
164
+ , align_pad_mode
165
+ )
166
+ , **keywordArguments
167
+ )
168
+
169
+ if False: # This fails the current tests.
170
+ arraySpectrogramsShape[axisSpectrogram.number] = index
171
+ indices = numpy.indices(tuple(arraySpectrogramsShape))
172
+ numpy.put_along_axis(
173
+ arraySpectrograms,
174
+ indices=indices,
175
+ values=stft(
176
+ numpy.pad(
177
+ readAudioFile(metadata['pathFilename'], sampleRate, dtype_str).astype(dtypeWaveform, copy=False),
178
+ pad_widthIntegralType,
179
+ mode=align_pad_mode,
180
+ ),
181
+ **keywordArguments,
182
+ ),
183
+ axis=axisSpectrogram.number,
184
+ )
185
+
186
+ with ThreadPoolExecutor(max_workers=max_workers) as threadManager:
187
+ tuple(tqdm(threadManager.map(workhorse, dictionaryWaveformMetadata.keys(), dictionaryWaveformMetadata.values())
188
+ , desc='Loading spectrograms.'
189
+ , total=axisSpectrogram.size
190
+ ))
191
+
192
+ return SpectrogramsAndMetadata(arraySpectrograms, dictionaryWaveformMetadata)
193
+
194
+ def BACKUPloadSpectrograms(
195
+ listPathFilenames: Sequence[FileDescriptorOrPath], *, CPUlimit: bool | float | int | None = None, **keywordArguments: Unpack[Parameters_loadSpectrograms]
196
+ ) -> tuple[ArraySpectrograms, dict[int, WaveformMetadata]]:
197
+ """Load spectrograms from a list of audio files."""
198
+ #============== Initialize ==========================================================
199
+
200
+ align: OptionsAlign = keywordArguments.get('align', setting.align)
201
+ dtypeSpectrogram: DTypeLike = keywordArguments.get('dtypeSpectrogram', setting.dtypeSpectrogram)
202
+ dtype_str: Options_dtype_str = keywordArguments.get('dtype_str', setting.dtype_str)
203
+ dtypeWaveform: DTypeLike = keywordArguments.get('dtypeWaveform', setting.dtypeWaveform)
204
+ sampleRate: float = keywordArguments.get('sampleRate', setting.sampleRate)
205
+
206
+ max_workers: int = defineConcurrencyLimit(limit=CPUlimit)
207
+
208
+ #============== Allocate memory ==========================================================
209
+
210
+ dictionaryWaveformMetadata, axis = getWaveformMetadata(listPathFilenames, sampleRate, align)
211
+
212
+ waveformShape = Axes2(*(entry.size for entry in sorted((axis['channel'], axis['time']))))
213
+ waveformZeros: Waveform = numpy.zeros(waveformShape, dtypeWaveform)
214
+
215
+ axisSpectrogram = AxisMetadata(setting.axisSpectrogramIndexing, len(dictionaryWaveformMetadata))
216
+
217
+ arraySpectrogramsShape: list[int] = list(stft(waveformZeros, **keywordArguments).shape)
218
+ arraySpectrogramsShape.insert(axisSpectrogram.number, axisSpectrogram.size)
219
+ arraySpectrograms: ArraySpectrograms = numpy.zeros(Axes4(*arraySpectrogramsShape), dtypeSpectrogram)
220
+
221
+ #============== Concurrent loading ==========================================================
222
+
223
+ def workhorse(index: int, metadata: WaveformMetadata) -> None:
224
+ waveform: Waveform = waveformZeros.copy()
225
+ waveform[:, metadata['samplesStart'] : metadata['samplesStop']] = readAudioFile(
226
+ metadata['pathFilename'], sampleRate, dtype_str
227
+ ).astype(dtypeWaveform, copy=False)
228
+ arraySpectrograms[..., index] = stft(waveform, **keywordArguments)
229
+
230
+ with ThreadPoolExecutor(max_workers=max_workers) as threadManager:
231
+ tuple(
232
+ tqdm(
233
+ threadManager.map(workhorse, dictionaryWaveformMetadata.keys(), dictionaryWaveformMetadata.values()),
234
+ desc='Loading spectrograms',
235
+ total=axisSpectrogram.size,
236
+ )
237
+ )
238
+
239
+ return arraySpectrograms, dictionaryWaveformMetadata
@@ -0,0 +1,145 @@
1
+ # pyright: reportArgumentType=false
2
+ # pyright: reportAssignmentType=false
3
+ # ty:ignore[invalid-argument-type]
4
+ # ty:ignore[invalid-assignment]
5
+ from __future__ import annotations
6
+
7
+ from humpy_cytoolz.dicttoolz import keyfilter, merge
8
+ from hunterHearsPy import amplitudeIntegerToFloating, getAxis, setting
9
+ from hunterHearsPy.dataBaskets import ParametersShortTimeFFT
10
+ from numpy import complexfloating, floating, integer
11
+ from scipy.signal import ShortTimeFFT
12
+ from typing import overload, TYPE_CHECKING
13
+ from typing_extensions import Unpack
14
+ import dataclasses
15
+ import numpy
16
+
17
+ if TYPE_CHECKING:
18
+ from collections.abc import Callable
19
+ from hunterHearsPy.theTypes import (
20
+ ArraySpectrograms, ArrayWaveforms, ArrayWaveformsFloating, Parameters_stft, Spectrogram, Waveform, WaveformFloating)
21
+ from pathlib import PurePath
22
+ from scipy.signal._short_time_fft import _PadType
23
+
24
+ @overload # stft 1 ndarray
25
+ def stft(
26
+ arrayTarget: Waveform, *, lengthWaveform: int = 0
27
+ , indexingAxis: int = -1, **keywordArguments: Unpack[Parameters_stft]
28
+ ) -> Spectrogram: ...
29
+ @overload # stft many ndarray
30
+ def stft(
31
+ arrayTarget: ArrayWaveforms, *, lengthWaveform: int = 0
32
+ , indexingAxis: int = -1, **keywordArguments: Unpack[Parameters_stft]
33
+ ) -> ArraySpectrograms: ...
34
+ @overload # istft 1 ndarray
35
+ def stft(
36
+ arrayTarget: Spectrogram, *, lengthWaveform: int
37
+ , indexingAxis: int = -1, **keywordArguments: Unpack[Parameters_stft]
38
+ ) -> Waveform: ...
39
+ @overload # istft many ndarray
40
+ def stft(
41
+ arrayTarget: ArraySpectrograms, *, lengthWaveform: int
42
+ , indexingAxis: int = -1, **keywordArguments: Unpack[Parameters_stft]
43
+ ) -> ArrayWaveforms: ...
44
+ def stft(
45
+ arrayTarget: Waveform | ArrayWaveforms | Spectrogram | ArraySpectrograms
46
+ , *
47
+ , lengthWaveform: int = 0
48
+ , indexingAxis: int = -1
49
+ , **keywordArguments: Unpack[Parameters_stft]
50
+ ) -> Waveform | ArrayWaveforms | Spectrogram | ArraySpectrograms:
51
+ def mushroom(waveform: WaveformFloating) -> Spectrogram:
52
+ return workhorseSTFT.stft(x=waveform, padding=padding)
53
+
54
+ def turtleShell(spectrogram: Spectrogram, lengthWaveform: int) -> WaveformFloating:
55
+ return workhorseSTFT.istft(S=spectrogram, k1=lengthWaveform)
56
+
57
+ #============== Initialize ==========================================================
58
+
59
+ parametersShortTimeFFT = ParametersShortTimeFFT(**keyfilter(dataclasses.asdict(setting.ShortTimeFFT).keys().__contains__, merge(dataclasses.asdict(setting.ShortTimeFFT), keywordArguments)))
60
+ padding: _PadType = keywordArguments.get('padding', setting.padding)
61
+
62
+ workhorseSTFT: ShortTimeFFT = ShortTimeFFT(**parametersShortTimeFFT.e733T)
63
+
64
+ # DEVELOPMENT This assumes I will only run stft on floating-point valued waveform data. But is
65
+ # that true? Is that necessary? Is that desirable?
66
+ arrayFloating = arrayTarget
67
+ arrayWaveforms = arrayTarget
68
+ if numpy.issubdtype(arrayTarget.dtype, integer):
69
+ if arrayTarget.ndim == 3:
70
+ arrayWaveforms: ArrayWaveformsFloating = amplitudeIntegerToFloating(arrayTarget)
71
+ if arrayTarget.ndim == 2:
72
+ arrayFloating: WaveformFloating = amplitudeIntegerToFloating(arrayTarget)
73
+ elif numpy.issubdtype(arrayTarget.dtype, complexfloating) and (lengthWaveform < 1):
74
+ from hunterHearsPy._io import saveOnError # noqa: PLC0415
75
+
76
+ pathFilename: PurePath = saveOnError(arrayTarget)
77
+ message: str = (
78
+ 'I did not receive `lengthWaveform`, so I could not perform the inverse STFT. '
79
+ 'I saved `arrayTarget` to a file in the temporary directory of this computer so you might recover the data. '
80
+ f'{arrayTarget.shape = }, {arrayTarget.dtype = }\n'
81
+ f'{pathFilename = }'
82
+ )
83
+ raise ValueError(message)
84
+
85
+ # DEVELOPMENT There are eleventy different systems below because I have been trying to find 1)
86
+ # efficient logic that 2) the type checkers understand.
87
+ if arrayFloating.ndim == 2:
88
+ arrayWaveforms: ArrayWaveformsFloating = numpy.expand_dims(arrayFloating, indexingAxis)
89
+ elif (arrayTarget.ndim == 3) and (numpy.issubdtype(arrayTarget.dtype, complexfloating)):
90
+ spectrogram: Spectrogram = arrayTarget
91
+ return turtleShell(spectrogram, lengthWaveform)
92
+
93
+ if (arrayWaveforms.ndim == 3) and (numpy.issubdtype(arrayWaveforms.dtype, floating)):
94
+ arrayWaveforms = numpy.moveaxis(arrayWaveforms, indexingAxis, -1)
95
+ index = 0
96
+ arraySpectrograms: ArraySpectrograms = numpy.tile(
97
+ mushroom(arrayWaveforms[..., index])[..., numpy.newaxis], arrayWaveforms.shape[-1]
98
+ )
99
+ for index in range(1, arrayWaveforms.shape[-1]):
100
+ arraySpectrograms[..., index] = mushroom(arrayWaveforms[..., index])
101
+ arraySpectrograms = numpy.moveaxis(arraySpectrograms, -1, indexingAxis)
102
+ if arraySpectrograms.shape[indexingAxis] == 1:
103
+ arraySpectrograms = numpy.squeeze(arraySpectrograms, indexingAxis)
104
+ return arraySpectrograms
105
+
106
+ elif (arrayTarget.ndim == 4) and (numpy.issubdtype(arrayTarget.dtype, complexfloating)):
107
+ arrayTARGET: ArraySpectrograms = arrayTarget
108
+ arrayTARGET = numpy.moveaxis(arrayTARGET, indexingAxis, -1)
109
+ index = 0
110
+ arrayTransformed: ArrayWaveforms = numpy.tile(
111
+ turtleShell(arrayTARGET[..., index], lengthWaveform)[..., numpy.newaxis], arrayTARGET.shape[-1]
112
+ )
113
+ for index in range(1, arrayTARGET.shape[-1]):
114
+ arrayTransformed[..., index] = turtleShell(arrayTARGET[..., index], lengthWaveform)
115
+ return numpy.moveaxis(arrayTransformed, -1, indexingAxis)
116
+ else:
117
+ return arrayTarget
118
+
119
+ def waveformSpectrogramWaveform(callableNeedsSpectrogram: Callable[[Spectrogram], Spectrogram]) -> Callable[[Waveform], Waveform]:
120
+ """Decorate a spectrogram-processing callable to accept and return waveforms.
121
+
122
+ You can use this function as a decorator when you have a function that transforms `Spectrogram`
123
+ [1] data and you want a version that operates directly on `Waveform` [2] data. The returned
124
+ function applies `stft` to convert the input `Waveform` [2] to a `Spectrogram` [1], calls
125
+ `callableNeedsSpectrogram`, then applies inverse `stft` to convert the result back to a `Waveform`
126
+ [2] of the original length.
127
+
128
+ Parameters
129
+ ----------
130
+ callableNeedsSpectrogram : Callable[[Spectrogram], Spectrogram]
131
+ A function that accepts and returns a `Spectrogram` [1].
132
+
133
+ Returns
134
+ -------
135
+ stft_istft : Callable[[Waveform], Waveform]
136
+ A function that accepts a `Waveform` [2], converts it to a `Spectrogram` [1], applies
137
+ `callableNeedsSpectrogram`, and returns the reconstructed `Waveform` [2] at the original
138
+ length.
139
+ """
140
+ def stft_istft(waveform: Waveform) -> Waveform:
141
+ axisWaveformTime: int = getAxis()['time'].number
142
+
143
+ return stft(callableNeedsSpectrogram(stft(waveform)), lengthWaveform=waveform.shape[axisWaveformTime])
144
+
145
+ return stft_istft
@@ -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,12 @@ 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
16
+ from hunterHearsPy.theTypes import FileDescriptorOrPath, Parameters_stft, Spectrogram, Waveform
17
17
  from numpy import dtype, ndarray
18
- from soundfile import dtype_str as Options_dtype_str
18
+ from soundfile import AudioData_2d, dtype_str as Options_dtype_str
19
19
  from typing import Any
20
20
 
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
21
  def readAudioFile(pathFilename: FileDescriptorOrPath, sampleRateDesired: float | None = None, dtype_str: Options_dtype_str | None = None) -> Waveform:
33
22
  """Read an audio file and return waveform data as a NumPy array.
34
23
 
@@ -54,11 +43,11 @@ def readAudioFile(pathFilename: FileDescriptorOrPath, sampleRateDesired: float |
54
43
  sampleRateDesired = sampleRateDesired or setting.sampleRate
55
44
  with soundfile.SoundFile(pathFilename) as readSoundFile:
56
45
  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))
46
+ 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
47
+ axis: dict[str, AxisMetadata] = getAxis()
48
+ waveform: Waveform = audioData.transpose((axis['time'].number, axis['channel'].number))
60
49
 
61
- return resampleWaveform(waveform, sampleRateDesired, sampleRateSource, axis['time'].number)
50
+ return resampleWaveform(waveform, sampleRateDesired, sampleRateSource, axis['time'].number) # ty:ignore[no-matching-overload] https://github.com/astral-sh/ty/issues/2799
62
51
 
63
52
  def writeWAV(pathFilename: FileDescriptorOrPath, waveform: Waveform, sampleRate: float | None = None, subtype: str | None = None) -> FileDescriptorOrPath:
64
53
  """Write a waveform array to a WAV file.
@@ -100,11 +89,11 @@ def writeWAV(pathFilename: FileDescriptorOrPath, waveform: Waveform, sampleRate:
100
89
  subtype = subtype or setting.subtype
101
90
  makeDirectorySafely(pathFilename)
102
91
 
103
- waveform = amplitudeToSoundfile(waveform)
92
+ audioData: AudioData = amplitudeToSoundfile(waveform)
104
93
 
105
- waveform = waveform.transpose()
94
+ audioData = audioData.transpose()
106
95
 
107
- soundfile.write(file=pathFilename, data=waveform, samplerate=sampleRate, subtype=subtype, format='WAV')
96
+ soundfile.write(file=pathFilename, data=audioData, samplerate=sampleRate, subtype=subtype, format='WAV')
108
97
  return pathFilename
109
98
 
110
99
  def spectrogramToWAV(spectrogram: Spectrogram, pathFilename: FileDescriptorOrPath, lengthWaveform: int, **parametersSTFT: Unpack[Parameters_stft]) -> None:
@@ -144,7 +133,7 @@ def spectrogramToWAV(spectrogram: Spectrogram, pathFilename: FileDescriptorOrPat
144
133
  sampleRate: float = parametersSTFT.get('sampleRate', setting.sampleRate)
145
134
  writeWAV(pathFilename, waveform, sampleRate)
146
135
 
147
- def saveOnError(arrayTarget: ndarray[tuple[int, ...], dtype[Any]], *, identifierTarget: str = 'arrayTarget') -> PurePath:
136
+ def saveOnError(arrayTarget: ndarray[tuple[Any, ...], dtype[Any]], *, identifierTarget: str = 'arrayTarget') -> PurePath:
148
137
  pathFilename: Path = Path(tempfile.mkdtemp(prefix='hunterHearsPy'), f"{identifierTarget}_{uuid.uuid4().hex}.npy").resolve()
149
138
  numpy.save(pathFilename, arrayTarget)
150
139
 
@@ -7,7 +7,7 @@ from typing import overload, TYPE_CHECKING
7
7
  import resampy
8
8
 
9
9
  if TYPE_CHECKING:
10
- from hunterHearsPy import 形floating, 形Shape
10
+ from hunterHearsPy.theTypes import 形floating, 形Shape
11
11
  from numpy import dtype, float32, integer, ndarray
12
12
  from typing import Any
13
13
 
@@ -18,10 +18,6 @@ def resampleWaveform(waveform: ndarray[形Shape, dtype[形floating]], sampleRate
18
18
  def resampleWaveform(waveform: ndarray[形Shape, dtype[形floating]] | ndarray[形Shape, dtype[integer[Any]]], sampleRateDesired: float, sampleRateSource: float, axisTime: int = -1) -> ndarray[形Shape, dtype[形floating]] | ndarray[形Shape, dtype[float32]]:
19
19
  """Resample `waveform` array to `sampleRateDesired` along the `axisTime` axis.
20
20
 
21
- Warning
22
- -------
23
- The returned `ndarray` always has a floating-point `dtype`, even if the sample rate is unchanged.
24
-
25
21
  This function is _not_ regulated by the universal settings.
26
22
 
27
23
  Parameters