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.
- {hunterhearspy-1.2.0/src/hunterHearsPy.egg-info → hunterhearspy-1.4.0}/PKG-INFO +2 -8
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/README.md +1 -7
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/pyproject.toml +1 -1
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/__init__.py +11 -16
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/_arrays.py +89 -55
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/_fft.py +76 -60
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/_io.py +45 -48
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/_resample.py +1 -5
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/amplitude.py +104 -112
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/autoRevert.py +3 -4
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/clippingArrays.py +1 -1
- hunterhearspy-1.4.0/src/hunterHearsPy/dataBaskets.py +69 -0
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/theSSOT.py +3 -1
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/theTypes.py +56 -78
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/windowingFunctions.py +1 -1
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/windowingFunctionsTensor.py +2 -1
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0/src/hunterHearsPy.egg-info}/PKG-INFO +2 -8
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy.egg-info/SOURCES.txt +1 -0
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/tests/test_arrays.py +20 -12
- hunterhearspy-1.4.0/tests/test_fft.py +81 -0
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/tests/test_io.py +7 -7
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/tests/test_old_amplitude.py +7 -5
- hunterhearspy-1.4.0/tests/test_old_spectrogram.py +100 -0
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/tests/test_resample.py +1 -1
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/tests/test_windowingFunctions.py +1 -1
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/tests/test_windowingFunctionsTensor.py +1 -1
- hunterhearspy-1.2.0/src/hunterHearsPy/dataBaskets.py +0 -34
- hunterhearspy-1.2.0/tests/test_old_spectrogram.py +0 -246
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/LICENSE +0 -0
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/setup.cfg +0 -0
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy/py.typed +0 -0
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy.egg-info/dependency_links.txt +0 -0
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy.egg-info/requires.txt +0 -0
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/src/hunterHearsPy.egg-info/top_level.txt +0 -0
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/tests/test_old_autoRevert.py +0 -0
- {hunterhearspy-1.2.0 → hunterhearspy-1.4.0}/tests/test_old_clippingArrays.py +0 -0
- {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.
|
|
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`
|
|
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`
|
|
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,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
|
|
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.
|
|
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.
|
|
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.
|
|
33
|
+
from hunterHearsPy.autoRevert import moveToAxisOfOperation as moveToAxisOfOperation
|
|
40
34
|
|
|
41
35
|
# isort: split
|
|
42
|
-
from hunterHearsPy.
|
|
36
|
+
from hunterHearsPy._fft import stft as stft, waveformSpectrogramWaveform as waveformSpectrogramWaveform
|
|
43
37
|
|
|
44
38
|
# isort: split
|
|
45
|
-
from hunterHearsPy.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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(
|
|
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
|
-
|
|
91
|
+
dtypeWaveform: DTypeLike = keywordArguments.get('dtypeWaveform', setting.dtypeWaveform)
|
|
69
92
|
dtype_str: Options_dtype_str = keywordArguments.get('dtype_str', setting.dtype_str)
|
|
70
|
-
|
|
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
|
-
|
|
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'],
|
|
84
|
-
).astype(
|
|
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())
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
111
|
+
tuple(tqdm(threadManager.map(workhorse, dictionaryWaveformMetadata, dictionaryWaveformMetadata.values())
|
|
112
|
+
, desc='Loading waveforms.'
|
|
113
|
+
, total=len(dictionaryWaveformMetadata)
|
|
114
|
+
))
|
|
91
115
|
|
|
92
|
-
|
|
116
|
+
return WaveformsAndMetadata(array=arrayWaveforms, metadata=dictionaryWaveformMetadata)
|
|
93
117
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
135
|
+
dictionaryWaveformMetadata, axis = getWaveformMetadata(listPathFilenames, sampleRate, align)
|
|
123
136
|
|
|
124
|
-
waveformShape =
|
|
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(
|
|
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'],
|
|
151
|
-
, pad_widthIntegralType
|
|
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(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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())
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
211
|
+
dictionaryWaveformMetadata, axis = getWaveformMetadata(listPathFilenames, sampleRate, align)
|
|
186
212
|
|
|
187
|
-
waveformShape =
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
64
|
-
elif (arrayTarget.ndim == 3) and (numpy.issubdtype(arrayTarget.dtype,
|
|
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 (
|
|
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(
|
|
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,
|
|
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(
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
141
|
+
axisWaveformTime: int = getAxis()['time'].number
|
|
142
|
+
|
|
143
|
+
return stft(callableNeedsSpectrogram(stft(waveform)), lengthWaveform=waveform.shape[axisWaveformTime])
|
|
144
|
+
|
|
129
145
|
return stft_istft
|