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.
- {hunterhearspy-1.1.0 → hunterhearspy-1.3.0}/PKG-INFO +20 -17
- {hunterhearspy-1.1.0 → hunterhearspy-1.3.0}/pyproject.toml +13 -14
- hunterhearspy-1.3.0/setup.cfg +4 -0
- {hunterhearspy-1.1.0 → hunterhearspy-1.3.0}/src/hunterHearsPy/__init__.py +8 -14
- hunterhearspy-1.3.0/src/hunterHearsPy/_arrays.py +239 -0
- hunterhearspy-1.3.0/src/hunterHearsPy/_fft.py +145 -0
- {hunterhearspy-1.1.0 → hunterhearspy-1.3.0}/src/hunterHearsPy/_io.py +12 -23
- {hunterhearspy-1.1.0 → hunterhearspy-1.3.0}/src/hunterHearsPy/_resample.py +1 -5
- hunterhearspy-1.3.0/src/hunterHearsPy/amplitude.py +265 -0
- {hunterhearspy-1.1.0 → hunterhearspy-1.3.0}/src/hunterHearsPy/autoRevert.py +3 -4
- {hunterhearspy-1.1.0 → hunterhearspy-1.3.0}/src/hunterHearsPy/clippingArrays.py +28 -17
- hunterhearspy-1.3.0/src/hunterHearsPy/dataBaskets.py +69 -0
- {hunterhearspy-1.1.0 → hunterhearspy-1.3.0}/src/hunterHearsPy/theSSOT.py +21 -7
- hunterhearspy-1.3.0/src/hunterHearsPy/theTypes.py +99 -0
- hunterhearspy-1.3.0/src/hunterHearsPy/windowingFunctions.py +166 -0
- hunterhearspy-1.3.0/src/hunterHearsPy/windowingFunctionsTensor.py +133 -0
- hunterhearspy-1.3.0/src/hunterHearsPy.egg-info/PKG-INFO +195 -0
- hunterhearspy-1.3.0/src/hunterHearsPy.egg-info/SOURCES.txt +33 -0
- hunterhearspy-1.3.0/src/hunterHearsPy.egg-info/dependency_links.txt +1 -0
- hunterhearspy-1.3.0/src/hunterHearsPy.egg-info/requires.txt +17 -0
- hunterhearspy-1.3.0/src/hunterHearsPy.egg-info/top_level.txt +1 -0
- hunterhearspy-1.3.0/tests/test_arrays.py +84 -0
- hunterhearspy-1.3.0/tests/test_fft.py +81 -0
- hunterhearspy-1.3.0/tests/test_io.py +30 -0
- hunterhearspy-1.3.0/tests/test_old_amplitude.py +102 -0
- hunterhearspy-1.3.0/tests/test_old_autoRevert.py +46 -0
- hunterhearspy-1.3.0/tests/test_old_clippingArrays.py +98 -0
- hunterhearspy-1.3.0/tests/test_old_spectrogram.py +100 -0
- hunterhearspy-1.3.0/tests/test_old_waveform.py +167 -0
- hunterhearspy-1.3.0/tests/test_resample.py +51 -0
- hunterhearspy-1.3.0/tests/test_windowingFunctions.py +51 -0
- hunterhearspy-1.3.0/tests/test_windowingFunctionsTensor.py +80 -0
- hunterhearspy-1.1.0/src/hunterHearsPy/_arrays.py +0 -109
- hunterhearspy-1.1.0/src/hunterHearsPy/_fft.py +0 -127
- hunterhearspy-1.1.0/src/hunterHearsPy/amplitude.py +0 -253
- hunterhearspy-1.1.0/src/hunterHearsPy/dataBaskets.py +0 -34
- hunterhearspy-1.1.0/src/hunterHearsPy/theTypes.py +0 -101
- hunterhearspy-1.1.0/src/hunterHearsPy/windowingFunctions.py +0 -211
- hunterhearspy-1.1.0/src/hunterHearsPy/windowingFunctionsTensor.py +0 -168
- {hunterhearspy-1.1.0 → hunterhearspy-1.3.0}/LICENSE +0 -0
- {hunterhearspy-1.1.0 → hunterhearspy-1.3.0}/README.md +0 -0
- {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.
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
30
|
-
Requires-Dist:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Requires-
|
|
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
|
-
|
|
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.
|
|
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 = {
|
|
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 = ["
|
|
58
|
-
build-backend = "
|
|
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"
|
|
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"
|
|
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
|
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
# ruff: noqa: D104
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
from hunterHearsPy
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
58
|
-
axis: dict[str,
|
|
59
|
-
waveform =
|
|
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
|
-
|
|
92
|
+
audioData: AudioData = amplitudeToSoundfile(waveform)
|
|
104
93
|
|
|
105
|
-
|
|
94
|
+
audioData = audioData.transpose()
|
|
106
95
|
|
|
107
|
-
soundfile.write(file=pathFilename, data=
|
|
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[
|
|
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
|