hunterHearsPy 1.2.0__tar.gz → 1.4.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.2.0/src/hunterHearsPy.egg-info → hunterhearspy-1.4.0}/PKG-INFO +2 -8
  2. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/README.md +1 -7
  3. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/pyproject.toml +1 -1
  4. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/__init__.py +11 -16
  5. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/_arrays.py +89 -55
  6. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/_fft.py +76 -60
  7. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/_io.py +45 -48
  8. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/_resample.py +1 -5
  9. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/amplitude.py +104 -112
  10. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/autoRevert.py +3 -4
  11. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/clippingArrays.py +1 -1
  12. hunterhearspy-1.4.0/src/hunterHearsPy/dataBaskets.py +69 -0
  13. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/theSSOT.py +3 -1
  14. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/theTypes.py +56 -78
  15. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/windowingFunctions.py +1 -1
  16. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/windowingFunctionsTensor.py +2 -1
  17. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0/src/hunterHearsPy.egg-info}/PKG-INFO +2 -8
  18. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy.egg-info/SOURCES.txt +1 -0
  19. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/tests/test_arrays.py +20 -12
  20. hunterhearspy-1.4.0/tests/test_fft.py +81 -0
  21. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/tests/test_io.py +7 -7
  22. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/tests/test_old_amplitude.py +7 -5
  23. hunterhearspy-1.4.0/tests/test_old_spectrogram.py +100 -0
  24. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/tests/test_resample.py +1 -1
  25. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/tests/test_windowingFunctions.py +1 -1
  26. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/tests/test_windowingFunctionsTensor.py +1 -1
  27. hunterhearspy-1.2.0/src/hunterHearsPy/dataBaskets.py +0 -34
  28. hunterhearspy-1.2.0/tests/test_old_spectrogram.py +0 -246
  29. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/LICENSE +0 -0
  30. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/setup.cfg +0 -0
  31. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/py.typed +0 -0
  32. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy.egg-info/dependency_links.txt +0 -0
  33. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy.egg-info/requires.txt +0 -0
  34. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy.egg-info/top_level.txt +0 -0
  35. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/tests/test_old_autoRevert.py +0 -0
  36. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/tests/test_old_clippingArrays.py +0 -0
  37. {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/tests/test_old_waveform.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hunterHearsPy
3
- Version: 1.2.0
3
+ Version: 1.4.0
4
4
  Summary: Audio processing.
5
5
  Author-email: Hunter Hogan <HunterHogan@pm.me>
6
6
  Maintainer-email: Hunter Hogan <HunterHogan@pm.me>
@@ -131,13 +131,7 @@ I don't know a good way to implement user-configurable universal settings. There
131
131
  - time is more generic than samples and often preferred.
132
132
  - array is generic.
133
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`.
134
+ - Use `sampleRate` unless I have a compelling reason not to.
141
135
 
142
136
  ### Preferred packages
143
137
 
@@ -86,13 +86,7 @@ I don't know a good way to implement user-configurable universal settings. There
86
86
  - time is more generic than samples and often preferred.
87
87
  - array is generic.
88
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`.
89
+ - Use `sampleRate` unless I have a compelling reason not to.
96
90
 
97
91
  ### Preferred packages
98
92
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hunterHearsPy"
3
- version = "1.2.0"
3
+ version = "1.4.0"
4
4
  description = "Audio processing."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -1,16 +1,8 @@
1
1
  # ruff: noqa: D104
2
+ # TODO design and implement a namespace.
2
3
  from __future__ import annotations
3
4
 
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)
5
+ from hunterHearsPy import theTypes as theTypes
14
6
 
15
7
  # isort: split
16
8
  from hunterHearsPy.windowingFunctions import cosineWings as cosineWings, equalPower as equalPower, halfsine as halfsine, tukey as tukey
@@ -24,10 +16,12 @@ with suppress(ModuleNotFoundError): # noqa: RUF067
24
16
  tukeyTensor as tukeyTensor)
25
17
 
26
18
  # isort: split
27
- from hunterHearsPy.theSSOT import getAxis as getAxis, setting as setting
19
+ from hunterHearsPy.dataBaskets import (
20
+ AxisMetadata as AxisMetadata, SpectrogramsAndMetadata as SpectrogramsAndMetadata, WaveformMetadata as WaveformMetadata,
21
+ WaveformsAndMetadata as WaveformsAndMetadata)
28
22
 
29
23
  # isort: split
30
- from hunterHearsPy.dataBaskets import Translator as Translator
24
+ from hunterHearsPy.theSSOT import getAxis as getAxis, setting as setting
31
25
 
32
26
  # isort: split
33
27
  from hunterHearsPy._resample import resampleWaveform as resampleWaveform
@@ -36,16 +30,17 @@ from hunterHearsPy.amplitude import (
36
30
  normalizeArrayWaveforms as normalizeArrayWaveforms, normalizeWaveform as normalizeWaveform)
37
31
 
38
32
  # isort: split
39
- from hunterHearsPy._fft import stft as stft, waveformSpectrogramWaveform as waveformSpectrogramWaveform
33
+ from hunterHearsPy.autoRevert import moveToAxisOfOperation as moveToAxisOfOperation
40
34
 
41
35
  # isort: split
42
- from hunterHearsPy._io import readAudioFile as readAudioFile, spectrogramToWAV as spectrogramToWAV, writeWAV as writeWAV
36
+ from hunterHearsPy._fft import stft as stft, waveformSpectrogramWaveform as waveformSpectrogramWaveform
43
37
 
44
38
  # isort: split
45
- from hunterHearsPy.clippingArrays import applyHardLimit as applyHardLimit, applyHardLimitComplexValued as applyHardLimitComplexValued
39
+ from hunterHearsPy._io import (
40
+ getSampleRate as getSampleRate, readAudioFile as readAudioFile, spectrogramToWAV as spectrogramToWAV, writeWAV as writeWAV)
46
41
 
47
42
  # isort: split
48
- from hunterHearsPy.autoRevert import moveToAxisOfOperation as moveToAxisOfOperation
43
+ from hunterHearsPy.clippingArrays import applyHardLimit as applyHardLimit, applyHardLimitComplexValued as applyHardLimitComplexValued
49
44
 
50
45
  # isort: split
51
46
  from hunterHearsPy._arrays import (
@@ -3,22 +3,38 @@ from __future__ import annotations
3
3
 
4
4
  from concurrent.futures import ThreadPoolExecutor
5
5
  from hunterHearsPy import (
6
- ArraySpectrogramsShape, ArrayWaveformsShape, AxisMetadata, getAxis, readAudioFile, setting, stft, WaveformMetadata, WaveformShape)
6
+ AxisMetadata, getAxis, readAudioFile, setting, SpectrogramsAndMetadata, stft, WaveformMetadata, WaveformsAndMetadata)
7
7
  from hunterMakesPy.parseParameters import defineConcurrencyLimit
8
8
  from tqdm.auto import tqdm
9
- from typing import TYPE_CHECKING
9
+ from typing import NamedTuple, TYPE_CHECKING
10
10
  from typing_extensions import Unpack
11
11
  import numpy
12
12
  import sys
13
13
 
14
14
  if TYPE_CHECKING:
15
15
  from collections.abc import Sequence
16
- from hunterHearsPy import (
16
+ from hunterHearsPy.theTypes import (
17
17
  ArraySpectrograms, ArrayWaveforms, FileDescriptorOrPath, OptionsAlign, Parameters_loadSpectrograms, Parameters_loadWaveforms, Waveform)
18
18
  from numpy.lib._arraypad_impl import _ModeKind
19
19
  from numpy.typing import DTypeLike
20
20
  from soundfile import dtype_str as Options_dtype_str
21
- from typing import Any
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
22
38
 
23
39
  def getWaveformMetadata(
24
40
  listPathFilenames: Sequence[FileDescriptorOrPath], sampleRate: float, align: OptionsAlign
@@ -38,6 +54,7 @@ def getWaveformMetadata(
38
54
  sys.stderr.write(message + '\n')
39
55
 
40
56
  for index, pathFilename in enumerate(tqdm(listPathFilenames, desc='Preparing combined array', leave=False)):
57
+ # NOTE Use readAudioFile with the prescribed sampleRate to get the exact length.
41
58
  channels, lengthWaveform = readAudioFile(pathFilename, sampleRate).shape
42
59
  dictionaryWaveformMetadata[index] = WaveformMetadata(
43
60
  channels=channels, lengthWaveform=lengthWaveform, pathFilename=pathFilename, samplesStart=0, samplesStop=0
@@ -55,78 +72,74 @@ def getWaveformMetadata(
55
72
 
56
73
  for metadata in dictionaryWaveformMetadata.values():
57
74
  samplesPadding: int = axis['time'].size - metadata['lengthWaveform']
58
- # TODO document that if `samplesPadding` is odd, the extra pad-sample is added to samplesStop.
75
+ # DOCUMENT that if `samplesPadding` is odd, the extra pad-sample is added to samplesStop.
59
76
  metadata['samplesStart'] = int(samplesPadding * multiplicandSamplesStart)
60
77
  metadata['samplesStop'] = metadata['samplesStart'] + metadata['lengthWaveform']
61
78
 
62
79
  return dictionaryWaveformMetadata, axis
63
80
 
64
- def loadWaveforms(listPathFilenames: Sequence[FileDescriptorOrPath], *, CPUlimit: bool | float | int | None = None, **keywordArguments: Unpack[Parameters_loadWaveforms]) -> ArrayWaveforms:
81
+ def loadWaveforms(
82
+ listPathFilenames: Sequence[FileDescriptorOrPath]
83
+ , *
84
+ , CPUlimit: bool | float | int | None = None
85
+ , **keywordArguments: Unpack[Parameters_loadWaveforms]
86
+ ) -> WaveformsAndMetadata:
65
87
  """Load a list of audio files into a single stacked NumPy array."""
66
88
  #============== Initialize ==========================================================
89
+
67
90
  align: OptionsAlign = keywordArguments.get('align', setting.align)
68
- dtype: DTypeLike = keywordArguments.get('dtype', setting.dtypeWaveform)
91
+ dtypeWaveform: DTypeLike = keywordArguments.get('dtypeWaveform', setting.dtypeWaveform)
69
92
  dtype_str: Options_dtype_str = keywordArguments.get('dtype_str', setting.dtype_str)
70
- sampleRateDesired: float = keywordArguments.get('sampleRateDesired', setting.sampleRate)
93
+ sampleRate: float = keywordArguments.get('sampleRate', setting.sampleRate)
71
94
 
72
95
  max_workers: int = defineConcurrencyLimit(limit=CPUlimit)
73
96
 
74
97
  #============== Allocate memory ==========================================================
75
- dictionaryWaveformMetadata, axis = getWaveformMetadata(listPathFilenames, sampleRateDesired, align)
76
98
 
77
- arrayWaveforms: ArrayWaveforms = numpy.zeros(ArrayWaveformsShape(*(entry.size for entry in sorted(axis.values()))), dtype)
99
+ dictionaryWaveformMetadata, axis = getWaveformMetadata(listPathFilenames, sampleRate, align)
100
+ arrayWaveforms: ArrayWaveforms = numpy.zeros(Axes3(*(entry.size for entry in sorted(axis.values()))), dtypeWaveform)
78
101
 
79
102
  #============== Concurrent loading ==========================================================
103
+
80
104
  # TODO the axis order is hardcoded.
81
105
  def workhorse(index: int, metadata: WaveformMetadata) -> None:
82
106
  arrayWaveforms[:, metadata['samplesStart'] : metadata['samplesStop'], index] = readAudioFile(
83
- metadata['pathFilename'], sampleRateDesired, dtype_str
84
- ).astype(dtype, copy=False)
107
+ metadata['pathFilename'], sampleRate, dtype_str
108
+ ).astype(dtypeWaveform, copy=False)
85
109
 
86
110
  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.
111
+ tuple(tqdm(threadManager.map(workhorse, dictionaryWaveformMetadata, dictionaryWaveformMetadata.values())
112
+ , desc='Loading waveforms.'
113
+ , total=len(dictionaryWaveformMetadata)
114
+ ))
91
115
 
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.
116
+ return WaveformsAndMetadata(array=arrayWaveforms, metadata=dictionaryWaveformMetadata)
93
117
 
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]]:
118
+ def loadSpectrograms(
119
+ listPathFilenames: Sequence[FileDescriptorOrPath], *, CPUlimit: bool | float | int | None = None, **keywordArguments: Unpack[Parameters_loadSpectrograms]
120
+ ) -> SpectrogramsAndMetadata:
108
121
  """Load spectrograms from a list of audio files."""
109
122
  #============== Initialize ==========================================================
110
123
 
111
124
  align: OptionsAlign = keywordArguments.get('align', setting.align)
112
125
  align_pad_mode: _ModeKind = keywordArguments.get('align_pad_mode', setting.align_pad_mode)
113
- dtype: DTypeLike = keywordArguments.get('dtype', setting.dtypeSpectrogram)
126
+ dtypeSpectrogram: DTypeLike = keywordArguments.get('dtypeSpectrogram', setting.dtypeSpectrogram)
114
127
  dtype_str: Options_dtype_str = keywordArguments.get('dtype_str', setting.dtype_str)
115
128
  dtypeWaveform: DTypeLike = keywordArguments.get('dtypeWaveform', setting.dtypeWaveform)
116
- sampleRateDesired: float = keywordArguments.get('sampleRateDesired', setting.sampleRate)
129
+ sampleRate: float = keywordArguments.get('sampleRate', setting.sampleRate)
117
130
 
118
131
  max_workers: int = defineConcurrencyLimit(limit=CPUlimit)
119
132
 
120
133
  #============== Allocate memory ==========================================================
121
134
 
122
- dictionaryWaveformMetadata, axis = getWaveformMetadata(listPathFilenames, sampleRateDesired, align)
135
+ dictionaryWaveformMetadata, axis = getWaveformMetadata(listPathFilenames, sampleRate, align)
123
136
 
124
- waveformShape = WaveformShape(*(entry.size for entry in sorted((axis['channel'], axis['time']))))
137
+ waveformShape = Axes2(*(entry.size for entry in sorted((axis['channel'], axis['time']))))
125
138
  axisSpectrogram = AxisMetadata(setting.axisSpectrogramIndexing, len(dictionaryWaveformMetadata))
126
139
  arraySpectrogramsShape: list[int] = list(stft(numpy.zeros(waveformShape, dtypeWaveform), **keywordArguments).shape)
127
140
  arraySpectrogramsShape.insert(axisSpectrogram.number, axisSpectrogram.size)
128
141
 
129
- arraySpectrograms: ArraySpectrograms = numpy.zeros(ArraySpectrogramsShape(*arraySpectrogramsShape), dtype)
142
+ arraySpectrograms: ArraySpectrograms = numpy.zeros(Axes4(*arraySpectrogramsShape), dtypeSpectrogram)
130
143
 
131
144
  #============== Concurrent loading ==========================================================
132
145
 
@@ -147,60 +160,81 @@ def loadSpectrograms(listPathFilenames: Sequence[FileDescriptorOrPath], *, CPUli
147
160
  # TODO axisSpectrogram.number is hardcoded here. grr!
148
161
  arraySpectrograms[..., index] = stft(
149
162
  numpy.pad(
150
- readAudioFile(metadata['pathFilename'], sampleRateDesired, dtype_str).astype(dtypeWaveform, copy=False)
151
- , pad_widthIntegralType, mode=align_pad_mode)
163
+ readAudioFile(metadata['pathFilename'], sampleRate, dtype_str).astype(dtypeWaveform, copy=False)
164
+ , pad_widthIntegralType
165
+ , align_pad_mode
166
+ )
152
167
  , **keywordArguments
153
168
  )
154
169
 
155
170
  if False: # This fails the current tests.
156
171
  arraySpectrogramsShape[axisSpectrogram.number] = index
157
172
  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
- )
173
+ numpy.put_along_axis(
174
+ arraySpectrograms,
175
+ indices=indices,
176
+ values=stft(
177
+ numpy.pad(
178
+ readAudioFile(metadata['pathFilename'], sampleRate, dtype_str).astype(dtypeWaveform, copy=False),
179
+ pad_widthIntegralType,
180
+ mode=align_pad_mode,
181
+ ),
182
+ **keywordArguments,
183
+ ),
184
+ axis=axisSpectrogram.number,
185
+ )
165
186
 
166
187
  with ThreadPoolExecutor(max_workers=max_workers) as threadManager:
167
- tuple(tqdm(threadManager.map(workhorse, dictionaryWaveformMetadata.keys(), dictionaryWaveformMetadata.values()), desc='Loading spectrograms', total=axisSpectrogram.size))
188
+ tuple(tqdm(threadManager.map(workhorse, dictionaryWaveformMetadata.keys(), dictionaryWaveformMetadata.values())
189
+ , desc='Loading spectrograms.'
190
+ , total=axisSpectrogram.size
191
+ ))
168
192
 
169
- return arraySpectrograms, dictionaryWaveformMetadata
193
+ return SpectrogramsAndMetadata(arraySpectrograms, dictionaryWaveformMetadata)
170
194
 
171
- def BACKUPloadSpectrograms(listPathFilenames: Sequence[FileDescriptorOrPath], *, CPUlimit: bool | float | int | None = None, **keywordArguments: Any) -> tuple[ArraySpectrograms, dict[int, WaveformMetadata]]:
195
+ def BACKUPloadSpectrograms(
196
+ listPathFilenames: Sequence[FileDescriptorOrPath], *, CPUlimit: bool | float | int | None = None, **keywordArguments: Unpack[Parameters_loadSpectrograms]
197
+ ) -> tuple[ArraySpectrograms, dict[int, WaveformMetadata]]:
172
198
  """Load spectrograms from a list of audio files."""
173
199
  #============== Initialize ==========================================================
174
200
 
175
201
  align: OptionsAlign = keywordArguments.get('align', setting.align)
176
- dtype: DTypeLike = keywordArguments.get('dtype', setting.dtypeSpectrogram)
202
+ dtypeSpectrogram: DTypeLike = keywordArguments.get('dtypeSpectrogram', setting.dtypeSpectrogram)
177
203
  dtype_str: Options_dtype_str = keywordArguments.get('dtype_str', setting.dtype_str)
178
204
  dtypeWaveform: DTypeLike = keywordArguments.get('dtypeWaveform', setting.dtypeWaveform)
179
- sampleRateDesired: float = keywordArguments.get('sampleRateDesired', setting.sampleRate)
205
+ sampleRate: float = keywordArguments.get('sampleRate', setting.sampleRate)
180
206
 
181
207
  max_workers: int = defineConcurrencyLimit(limit=CPUlimit)
182
208
 
183
209
  #============== Allocate memory ==========================================================
184
210
 
185
- dictionaryWaveformMetadata, axis = getWaveformMetadata(listPathFilenames, sampleRateDesired, align)
211
+ dictionaryWaveformMetadata, axis = getWaveformMetadata(listPathFilenames, sampleRate, align)
186
212
 
187
- waveformShape = WaveformShape(*(entry.size for entry in sorted((axis['channel'], axis['time']))))
213
+ waveformShape = Axes2(*(entry.size for entry in sorted((axis['channel'], axis['time']))))
188
214
  waveformZeros: Waveform = numpy.zeros(waveformShape, dtypeWaveform)
189
215
 
190
216
  axisSpectrogram = AxisMetadata(setting.axisSpectrogramIndexing, len(dictionaryWaveformMetadata))
191
217
 
192
218
  arraySpectrogramsShape: list[int] = list(stft(waveformZeros, **keywordArguments).shape)
193
219
  arraySpectrogramsShape.insert(axisSpectrogram.number, axisSpectrogram.size)
194
- arraySpectrograms: ArraySpectrograms = numpy.zeros(ArraySpectrogramsShape(*arraySpectrogramsShape), dtype)
220
+ arraySpectrograms: ArraySpectrograms = numpy.zeros(Axes4(*arraySpectrogramsShape), dtypeSpectrogram)
195
221
 
196
222
  #============== Concurrent loading ==========================================================
197
223
 
198
224
  def workhorse(index: int, metadata: WaveformMetadata) -> None:
199
225
  waveform: Waveform = waveformZeros.copy()
200
- waveform[:, metadata['samplesStart'] : metadata['samplesStop']] = readAudioFile(metadata['pathFilename'], sampleRateDesired, dtype_str).astype(dtypeWaveform, copy=False)
226
+ waveform[:, metadata['samplesStart'] : metadata['samplesStop']] = readAudioFile(
227
+ metadata['pathFilename'], sampleRate, dtype_str
228
+ ).astype(dtypeWaveform, copy=False)
201
229
  arraySpectrograms[..., index] = stft(waveform, **keywordArguments)
202
230
 
203
231
  with ThreadPoolExecutor(max_workers=max_workers) as threadManager:
204
- tuple(tqdm(threadManager.map(workhorse, dictionaryWaveformMetadata.keys(), dictionaryWaveformMetadata.values()), desc='Loading spectrograms', total=axisSpectrogram.size))
232
+ tuple(
233
+ tqdm(
234
+ threadManager.map(workhorse, dictionaryWaveformMetadata.keys(), dictionaryWaveformMetadata.values()),
235
+ desc='Loading spectrograms',
236
+ total=axisSpectrogram.size,
237
+ )
238
+ )
205
239
 
206
240
  return arraySpectrograms, dictionaryWaveformMetadata
@@ -1,75 +1,101 @@
1
+ # pyright: reportArgumentType=false
2
+ # pyright: reportAssignmentType=false
3
+ # ty:ignore[invalid-argument-type]
4
+ # ty:ignore[invalid-assignment]
1
5
  from __future__ import annotations
2
6
 
3
7
  from humpy_cytoolz.dicttoolz import keyfilter, merge
4
- from hunterHearsPy import amplitudeIntegerToFloating, ParametersShortTimeFFT, setting, Translator
8
+ from hunterHearsPy import amplitudeIntegerToFloating, getAxis, setting
9
+ from hunterHearsPy.dataBaskets import ParametersShortTimeFFT
10
+ from numpy import complexfloating, floating, integer
5
11
  from scipy.signal import ShortTimeFFT
6
12
  from typing import overload, TYPE_CHECKING
7
13
  from typing_extensions import Unpack
14
+ import dataclasses
8
15
  import numpy
9
16
 
10
17
  if TYPE_CHECKING:
11
18
  from collections.abc import Callable
12
- from hunterHearsPy import (
19
+ from hunterHearsPy.theTypes import (
13
20
  ArraySpectrograms, ArrayWaveforms, ArrayWaveformsFloating, Parameters_stft, Spectrogram, Waveform, WaveformFloating)
14
21
  from pathlib import PurePath
15
22
  from scipy.signal._short_time_fft import _PadType
16
23
 
17
24
  @overload # stft 1 ndarray
18
- def stft(arrayTarget: Waveform, *, lengthWaveform: int = 0, indexingAxis: int = -1, **keywordArguments: Unpack[Parameters_stft]) -> Spectrogram: ...
25
+ def stft(
26
+ arrayTarget: Waveform, *, lengthWaveform: int = 0
27
+ , indexingAxis: int = -1, **keywordArguments: Unpack[Parameters_stft]
28
+ ) -> Spectrogram: ...
19
29
  @overload # stft many ndarray
20
- def stft(arrayTarget: ArrayWaveforms, *, lengthWaveform: int = 0, indexingAxis: int = -1, **keywordArguments: Unpack[Parameters_stft]) -> ArraySpectrograms: ...
30
+ def stft(
31
+ arrayTarget: ArrayWaveforms, *, lengthWaveform: int = 0
32
+ , indexingAxis: int = -1, **keywordArguments: Unpack[Parameters_stft]
33
+ ) -> ArraySpectrograms: ...
21
34
  @overload # istft 1 ndarray
22
- def stft(arrayTarget: Spectrogram, *, lengthWaveform: int, indexingAxis: int = -1, **keywordArguments: Unpack[Parameters_stft]) -> Waveform: ...
35
+ def stft(
36
+ arrayTarget: Spectrogram, *, lengthWaveform: int
37
+ , indexingAxis: int = -1, **keywordArguments: Unpack[Parameters_stft]
38
+ ) -> Waveform: ...
23
39
  @overload # istft many ndarray
24
- def stft(arrayTarget: ArraySpectrograms, *, lengthWaveform: int, indexingAxis: int = -1, **keywordArguments: Unpack[Parameters_stft]) -> ArrayWaveforms: ...
25
- def stft(arrayTarget: Waveform | ArrayWaveforms | Spectrogram | ArraySpectrograms
26
- , *
27
- , lengthWaveform: int = 0
28
- , indexingAxis: int = -1
29
- , **keywordArguments: Unpack[Parameters_stft]
30
- ) -> Waveform | ArrayWaveforms | Spectrogram | ArraySpectrograms:
31
- if numpy.issubdtype(arrayTarget.dtype, numpy.integer):
32
- if arrayTarget.ndim == 3:
33
- arrayFloating: ArrayWaveformsFloating = amplitudeIntegerToFloating(arrayTarget)
34
- if arrayTarget.ndim == 2:
35
- arrayFloating: WaveformFloating = amplitudeIntegerToFloating(arrayTarget)
36
- else:
37
- arrayFloating = arrayTarget
38
- if numpy.issubdtype(arrayTarget.dtype, numpy.complexfloating) and (lengthWaveform < 1):
39
- from hunterHearsPy._io import saveOnError # noqa: PLC0415
40
- pathFilename: PurePath = saveOnError(arrayTarget)
41
- message: str = (
42
- "I did not receive `lengthWaveform`, so I could not perform the inverse STFT. "
43
- "I saved `arrayTarget` to a file in this computer's temporary directory so you might recover the data. "
44
- f"{arrayTarget.shape = }, {arrayTarget.dtype = }\n"
45
- f"{pathFilename = }"
46
- )
47
- raise ValueError(message)
48
-
49
- parametersShortTimeFFT = Translator(**ParametersShortTimeFFT(
50
- keyfilter(setting.ShortTimeFFT.keys().__contains__, merge(setting.ShortTimeFFT, keywordArguments)))) # pyright: ignore[reportArgumentType] # ty:ignore[invalid-argument-type]
51
-
52
- padding: _PadType = keywordArguments.get('padding', setting.padding)
53
-
54
- workhorseSTFT: ShortTimeFFT = ShortTimeFFT(**parametersShortTimeFFT.e733T)
55
-
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:
56
51
  def mushroom(waveform: WaveformFloating) -> Spectrogram:
57
52
  return workhorseSTFT.stft(x=waveform, padding=padding)
58
53
 
59
54
  def turtleShell(spectrogram: Spectrogram, lengthWaveform: int) -> WaveformFloating:
60
55
  return workhorseSTFT.istft(S=spectrogram, k1=lengthWaveform)
61
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.
62
87
  if arrayFloating.ndim == 2:
63
- arrayFloating: ArrayWaveformsFloating = numpy.expand_dims(arrayFloating, indexingAxis)
64
- elif (arrayTarget.ndim == 3) and (numpy.issubdtype(arrayTarget.dtype, numpy.complexfloating)):
88
+ arrayWaveforms: ArrayWaveformsFloating = numpy.expand_dims(arrayFloating, indexingAxis)
89
+ elif (arrayTarget.ndim == 3) and (numpy.issubdtype(arrayTarget.dtype, complexfloating)):
65
90
  spectrogram: Spectrogram = arrayTarget
66
91
  return turtleShell(spectrogram, lengthWaveform)
67
92
 
68
- if (arrayFloating.ndim == 3) and (numpy.issubdtype(arrayFloating.dtype, numpy.floating)):
69
- arrayWaveforms: ArrayWaveformsFloating = arrayFloating
93
+ if (arrayWaveforms.ndim == 3) and (numpy.issubdtype(arrayWaveforms.dtype, floating)):
70
94
  arrayWaveforms = numpy.moveaxis(arrayWaveforms, indexingAxis, -1)
71
95
  index = 0
72
- arraySpectrograms: ArraySpectrograms = numpy.tile(mushroom(arrayWaveforms[..., index])[..., numpy.newaxis], arrayWaveforms.shape[-1])
96
+ arraySpectrograms: ArraySpectrograms = numpy.tile(
97
+ mushroom(arrayWaveforms[..., index])[..., numpy.newaxis], arrayWaveforms.shape[-1]
98
+ )
73
99
  for index in range(1, arrayWaveforms.shape[-1]):
74
100
  arraySpectrograms[..., index] = mushroom(arrayWaveforms[..., index])
75
101
  arraySpectrograms = numpy.moveaxis(arraySpectrograms, -1, indexingAxis)
@@ -77,11 +103,13 @@ def stft(arrayTarget: Waveform | ArrayWaveforms | Spectrogram | ArraySpectrogram
77
103
  arraySpectrograms = numpy.squeeze(arraySpectrograms, indexingAxis)
78
104
  return arraySpectrograms
79
105
 
80
- elif (arrayTarget.ndim == 4) and (numpy.issubdtype(arrayTarget.dtype, numpy.complexfloating)):
106
+ elif (arrayTarget.ndim == 4) and (numpy.issubdtype(arrayTarget.dtype, complexfloating)):
81
107
  arrayTARGET: ArraySpectrograms = arrayTarget
82
108
  arrayTARGET = numpy.moveaxis(arrayTARGET, indexingAxis, -1)
83
109
  index = 0
84
- arrayTransformed: ArrayWaveforms = numpy.tile(turtleShell(arrayTARGET[..., index], lengthWaveform)[..., numpy.newaxis], arrayTARGET.shape[-1])
110
+ arrayTransformed: ArrayWaveforms = numpy.tile(
111
+ turtleShell(arrayTARGET[..., index], lengthWaveform)[..., numpy.newaxis], arrayTARGET.shape[-1]
112
+ )
85
113
  for index in range(1, arrayTARGET.shape[-1]):
86
114
  arrayTransformed[..., index] = turtleShell(arrayTARGET[..., index], lengthWaveform)
87
115
  return numpy.moveaxis(arrayTransformed, -1, indexingAxis)
@@ -108,22 +136,10 @@ def waveformSpectrogramWaveform(callableNeedsSpectrogram: Callable[[Spectrogram]
108
136
  A function that accepts a `Waveform` [2], converts it to a `Spectrogram` [1], applies
109
137
  `callableNeedsSpectrogram`, and returns the reconstructed `Waveform` [2] at the original
110
138
  length.
111
-
112
- Time Axis Assumption
113
- --------------------
114
- The inner function `stft_istft` assumes the time axis of the input `Waveform` [2] is the last axis
115
- (`-1`). This matches the `(channels, samples)` shape convention.
116
-
117
- References
118
- ----------
119
- [1] `Spectrogram`
120
-
121
- [2] `Waveform`
122
-
123
139
  """
124
140
  def stft_istft(waveform: Waveform) -> Waveform:
125
- axisTime = -1
126
- arrayTarget = stft(waveform)
127
- spectrogram = callableNeedsSpectrogram(arrayTarget)
128
- return stft(spectrogram, lengthWaveform=waveform.shape[axisTime])
141
+ axisWaveformTime: int = getAxis()['time'].number
142
+
143
+ return stft(callableNeedsSpectrogram(stft(waveform)), lengthWaveform=waveform.shape[axisWaveformTime])
144
+
129
145
  return stft_istft