hunterHearsPy 1.3.0__tar.gz → 1.4.1__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 (45) hide show
  1. {hunterhearspy-1.3.0/src/hunterHearsPy.egg-info → hunterhearspy-1.4.1}/PKG-INFO +17 -14
  2. {hunterhearspy-1.3.0 → hunterhearspy-1.4.1}/README.md +7 -8
  3. hunterhearspy-1.4.1/astTransformations/__init__.py +1 -0
  4. hunterhearspy-1.4.1/astTransformations/windowingFunctions.py +109 -0
  5. {hunterhearspy-1.3.0/src → hunterhearspy-1.4.1}/hunterHearsPy/__init__.py +5 -3
  6. {hunterhearspy-1.3.0/src → hunterhearspy-1.4.1}/hunterHearsPy/_arrays.py +2 -2
  7. {hunterhearspy-1.3.0/src → hunterhearspy-1.4.1}/hunterHearsPy/_fft.py +1 -1
  8. {hunterhearspy-1.3.0/src → hunterhearspy-1.4.1}/hunterHearsPy/_io.py +47 -52
  9. {hunterhearspy-1.3.0/src → hunterhearspy-1.4.1}/hunterHearsPy/amplitude.py +4 -4
  10. {hunterhearspy-1.3.0/src → hunterhearspy-1.4.1}/hunterHearsPy/dataBaskets.py +2 -2
  11. {hunterhearspy-1.3.0/src → hunterhearspy-1.4.1}/hunterHearsPy/theSSOT.py +1 -1
  12. {hunterhearspy-1.3.0/src → hunterhearspy-1.4.1}/hunterHearsPy/theTypes.py +1 -1
  13. {hunterhearspy-1.3.0/src → hunterhearspy-1.4.1}/hunterHearsPy/windowingFunctionsTensor.py +5 -5
  14. {hunterhearspy-1.3.0 → hunterhearspy-1.4.1/hunterHearsPy.egg-info}/PKG-INFO +17 -14
  15. hunterhearspy-1.4.1/hunterHearsPy.egg-info/SOURCES.txt +40 -0
  16. {hunterhearspy-1.3.0/src → hunterhearspy-1.4.1}/hunterHearsPy.egg-info/requires.txt +8 -2
  17. hunterhearspy-1.4.1/hunterHearsPy.egg-info/top_level.txt +5 -0
  18. hunterhearspy-1.4.1/pyproject.toml +64 -0
  19. hunterhearspy-1.4.1/tests/__init__.py +7 -0
  20. hunterhearspy-1.4.1/tests/_theSSOT.py +8 -0
  21. hunterhearspy-1.4.1/tests/conftest.py +105 -0
  22. hunterhearspy-1.4.1/tests/conftestAnnex.py +38 -0
  23. hunterhearspy-1.4.1/tests/oldSampleData.py +42 -0
  24. {hunterhearspy-1.3.0 → hunterhearspy-1.4.1}/tests/test_io.py +5 -5
  25. {hunterhearspy-1.3.0 → hunterhearspy-1.4.1}/tests/test_old_clippingArrays.py +2 -2
  26. {hunterhearspy-1.3.0 → hunterhearspy-1.4.1}/tests/test_old_spectrogram.py +1 -1
  27. {hunterhearspy-1.3.0 → hunterhearspy-1.4.1}/tests/test_old_waveform.py +1 -1
  28. hunterhearspy-1.3.0/pyproject.toml +0 -95
  29. hunterhearspy-1.3.0/src/hunterHearsPy.egg-info/SOURCES.txt +0 -33
  30. hunterhearspy-1.3.0/src/hunterHearsPy.egg-info/top_level.txt +0 -1
  31. {hunterhearspy-1.3.0 → hunterhearspy-1.4.1}/LICENSE +0 -0
  32. {hunterhearspy-1.3.0/src → hunterhearspy-1.4.1}/hunterHearsPy/_resample.py +0 -0
  33. {hunterhearspy-1.3.0/src → hunterhearspy-1.4.1}/hunterHearsPy/autoRevert.py +0 -0
  34. {hunterhearspy-1.3.0/src → hunterhearspy-1.4.1}/hunterHearsPy/clippingArrays.py +0 -0
  35. {hunterhearspy-1.3.0/src → hunterhearspy-1.4.1}/hunterHearsPy/py.typed +0 -0
  36. {hunterhearspy-1.3.0/src → hunterhearspy-1.4.1}/hunterHearsPy/windowingFunctions.py +0 -0
  37. {hunterhearspy-1.3.0/src → hunterhearspy-1.4.1}/hunterHearsPy.egg-info/dependency_links.txt +0 -0
  38. {hunterhearspy-1.3.0 → hunterhearspy-1.4.1}/setup.cfg +0 -0
  39. {hunterhearspy-1.3.0 → hunterhearspy-1.4.1}/tests/test_arrays.py +0 -0
  40. {hunterhearspy-1.3.0 → hunterhearspy-1.4.1}/tests/test_fft.py +0 -0
  41. {hunterhearspy-1.3.0 → hunterhearspy-1.4.1}/tests/test_old_amplitude.py +0 -0
  42. {hunterhearspy-1.3.0 → hunterhearspy-1.4.1}/tests/test_old_autoRevert.py +0 -0
  43. {hunterhearspy-1.3.0 → hunterhearspy-1.4.1}/tests/test_resample.py +0 -0
  44. {hunterhearspy-1.3.0 → hunterhearspy-1.4.1}/tests/test_windowingFunctions.py +0 -0
  45. {hunterhearspy-1.3.0 → hunterhearspy-1.4.1}/tests/test_windowingFunctionsTensor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hunterHearsPy
3
- Version: 1.3.0
3
+ Version: 1.4.1
4
4
  Summary: Audio processing.
5
5
  Author-email: Hunter Hogan <HunterHogan@pm.me>
6
6
  Maintainer-email: Hunter Hogan <HunterHogan@pm.me>
@@ -17,23 +17,27 @@ Classifier: Intended Audience :: Developers
17
17
  Classifier: Intended Audience :: Science/Research
18
18
  Classifier: Natural Language :: English
19
19
  Classifier: Operating System :: OS Independent
20
- Classifier: Programming Language :: Python :: 3.10
21
- Classifier: Programming Language :: Python :: 3.11
22
20
  Classifier: Programming Language :: Python :: 3.12
23
21
  Classifier: Programming Language :: Python :: 3.13
24
22
  Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Programming Language :: Python :: 3.15
25
24
  Classifier: Typing :: Typed
26
- Requires-Python: >=3.10
25
+ Requires-Python: >=3.12
27
26
  Description-Content-Type: text/markdown
28
27
  License-File: LICENSE
29
- Requires-Dist: Z0Z_tools>=2.1.0
30
- Requires-Dist: hunterMakesPy>=0.7.0
28
+ Requires-Dist: Z0Z_tools>=4.0.0
29
+ Requires-Dist: hunterMakesPy>=0.10.0
31
30
  Requires-Dist: numpy
32
31
  Requires-Dist: resampy
33
32
  Requires-Dist: scipy
34
33
  Requires-Dist: soundfile>=0.14.0
35
34
  Requires-Dist: tqdm
36
35
  Requires-Dist: typing-extensions>=4.0.0
36
+ Provides-Extra: development
37
+ Requires-Dist: astToolkit; extra == "development"
38
+ Requires-Dist: pytest-cov; extra == "development"
39
+ Requires-Dist: scipy-stubs; extra == "development"
40
+ Requires-Dist: types-resampy; extra == "development"
37
41
  Provides-Extra: testing
38
42
  Requires-Dist: pytest; extra == "testing"
39
43
  Requires-Dist: pytest-mock; extra == "testing"
@@ -131,13 +135,7 @@ I don't know a good way to implement user-configurable universal settings. There
131
135
  - time is more generic than samples and often preferred.
132
136
  - array is generic.
133
137
  - 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`.
138
+ - Use `sampleRate` unless I have a compelling reason not to.
141
139
 
142
140
  ### Preferred packages
143
141
 
@@ -190,6 +188,11 @@ pip install hunterHearsPy
190
188
  ## My recovery
191
189
 
192
190
  [![Static Badge](https://img.shields.io/badge/2011_August-Homeless_since-blue?style=flat)](https://HunterThinks.com/support)
193
- [![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UC3Gx7kz61009NbhpRtPP7tw)](https://www.youtube.com/@HunterHogan)
194
191
 
195
192
  [![CC-BY-NC-4.0](https://raw.githubusercontent.com/hunterhogan/hunterHearsPy/refs/heads/main/.github/CC-BY-NC-4.0.png)](https://creativecommons.org/licenses/by-nc/4.0/)
193
+
194
+ By law, authors retain their copyrights unless they grant exceptions. The [Attribution-NonCommercial 4.0 International Deed](https://github.com/hunterhogan/hunterHearsPy#License-1-ov-file) outlines how you may use this work.
195
+
196
+ The restriction on commercial use (without a license) limits advantage-taking, unjust enrichment, and exploitation. You would be morally wrong if you were to enrich yourself by using this work while I am denied healthcare, housing, security of property, and the safety of my person.
197
+
198
+ To obtain and retain the right to use this work, you incur the cost of attributing the work to me. Your nominal cost might accrue as a benefit that mitigates my suffering or saves my life.
@@ -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
 
@@ -145,6 +139,11 @@ pip install hunterHearsPy
145
139
  ## My recovery
146
140
 
147
141
  [![Static Badge](https://img.shields.io/badge/2011_August-Homeless_since-blue?style=flat)](https://HunterThinks.com/support)
148
- [![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UC3Gx7kz61009NbhpRtPP7tw)](https://www.youtube.com/@HunterHogan)
149
142
 
150
143
  [![CC-BY-NC-4.0](https://raw.githubusercontent.com/hunterhogan/hunterHearsPy/refs/heads/main/.github/CC-BY-NC-4.0.png)](https://creativecommons.org/licenses/by-nc/4.0/)
144
+
145
+ By law, authors retain their copyrights unless they grant exceptions. The [Attribution-NonCommercial 4.0 International Deed](https://github.com/hunterhogan/hunterHearsPy#License-1-ov-file) outlines how you may use this work.
146
+
147
+ The restriction on commercial use (without a license) limits advantage-taking, unjust enrichment, and exploitation. You would be morally wrong if you were to enrich yourself by using this work while I am denied healthcare, housing, security of property, and the safety of my person.
148
+
149
+ To obtain and retain the right to use this work, you incur the cost of attributing the work to me. Your nominal cost might accrue as a benefit that mitigates my suffering or saves my life.
@@ -0,0 +1 @@
1
+ # ruff:ignore[undocumented-public-package]
@@ -0,0 +1,109 @@
1
+ """Generate PyTorch tensor windowing functions from NumPy windowing functions.
2
+
3
+ This module programmatically creates PyTorch tensor versions of windowing functions by transforming
4
+ existing functions from the `windowingFunctions` module. It generates functions that return PyTorch
5
+ tensors instead of NumPy arrays.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from astToolkit import Be, Make, NodeTourist, Then
10
+ from astToolkit.changeDef import makeDictionaryFunctionDef
11
+ from astToolkit.filesystem import parseLogicalPath2astModule, write_astModule
12
+ from hunterHearsPy.theSSOT import settingsPackage
13
+ from hunterMakesPy import raiseIfNone
14
+ from typing import TYPE_CHECKING
15
+ import ast
16
+
17
+ if TYPE_CHECKING:
18
+ from pathlib import Path
19
+
20
+ moduleDestination = 'windowingFunctionsTensor'
21
+ moduleSource = 'windowingFunctions'
22
+
23
+ pathFilenameDestination: Path = settingsPackage.pathPackage / (moduleDestination + settingsPackage.fileExtension)
24
+ pathFilenameSource: Path = pathFilenameDestination.with_stem(moduleSource)
25
+
26
+ def make_windowingFunctionsTensor() -> None:
27
+ """Generate PyTorch tensor windowing functions from NumPy windowing functions."""
28
+ listImports: list[ast.stmt] = [Make.ImportFrom('__future__', [Make.alias('annotations')])
29
+ , Make.ImportFrom('typing', [Make.alias('TYPE_CHECKING')])]
30
+ listBody: list[ast.stmt] = []
31
+
32
+ listBody.append(Make.If(test=Make.Name('TYPE_CHECKING') # ruff: ignore[repeated-append]
33
+ , body=[Make.ImportFrom('torch.types', [Make.alias('Device')])
34
+ , Make.ImportFrom('typing', [Make.alias('Any')])
35
+ , Make.ImportFrom(settingsPackage.identifierPackage + '.theTypes', [Make.alias('callableReturnsNDArray')])
36
+ ]))
37
+ listBody.append(Make.FunctionDef(
38
+ '_convertToTensor'
39
+ , Make.arguments(vararg=Make.arg('arguments', annotation=Make.Name('Any'))
40
+ , kwonlyargs=[Make.arg('callableTarget', annotation=Make.Name('callableReturnsNDArray'))
41
+ , Make.arg('device', annotation=Make.BitOr.join([Make.Name('Device'), Make.Constant(None)]))
42
+ , Make.arg('dtype', annotation=Make.BitOr.join([Make.Attribute(Make.Name('torch'), 'dtype'), Make.Constant(None)]))]
43
+ , kw_defaults=[None, Make.Constant(None), Make.Constant(None)]
44
+ , kwarg=Make.arg('keywordArguments', annotation=Make.Name('Any'))
45
+ )
46
+ , body=[Make.Assign([Make.Name('arrayTarget', ast.Store())]
47
+ , value=Make.Call(Make.Name('callableTarget')
48
+ , listParameters=[Make.Starred(value=Make.Name('arguments'))]
49
+ , list_keyword=[Make.keyword(None, value=Make.Name('keywordArguments'))])
50
+ )
51
+ , Make.If(test=Make.Compare(left=Make.Name('device'), ops=[Make.Is()], comparators=[Make.Constant(None)])
52
+ , body=[Make.Assign([Make.Name('device', ast.Store())], value=Make.Call(Make.Attribute(Make.Name('torch'), 'device'), list_keyword=[Make.keyword('device', value=Make.Constant('cpu'))]))]
53
+ )
54
+ , Make.Return(Make.Call(Make.Attribute(Make.Name('torch'), 'tensor'), list_keyword=[
55
+ Make.keyword('data', value=Make.Name('arrayTarget'))
56
+ , Make.keyword('dtype', value=Make.Or.join([Make.Name('dtype'), Make.Attribute(Make.Name('torch'), 'float32')]))
57
+ , Make.keyword('device', value=Make.Name('device'))
58
+ ]))
59
+ ]
60
+ , returns=Make.Attribute(Make.Name('torch'), 'Tensor')
61
+ ))
62
+
63
+ dictionaryFunctionDef: dict[str, ast.FunctionDef] = makeDictionaryFunctionDef(parseLogicalPath2astModule('.'.join([settingsPackage.identifierPackage, moduleSource])))
64
+
65
+ ImaIndent: str = ' ' * 4
66
+ ImaReturnsSection: str = f"\n{ImaIndent}Returns"
67
+ docstringDevice: str = f"{ImaIndent}device : Device = torch.device(device='cpu')\n{ImaIndent}{ImaIndent}PyTorch device for `Tensor`.\n"
68
+ docstringDtype: str = f"{ImaIndent}dtype : torch.dtype = torch.float32\n{ImaIndent}{ImaIndent}PyTorch data type for `Tensor`.\n"
69
+
70
+ for callableIdentifier, astFunctionDef in dictionaryFunctionDef.items():
71
+ if callableIdentifier.startswith('_'):
72
+ continue
73
+
74
+ docstring: ast.Expr = Make.Expr(Make.Constant(
75
+ raiseIfNone(ast.get_docstring(astFunctionDef, clean=False), errorMessage="Where's the windowing function docstring?")
76
+ .replace('\t', ImaIndent).replace(ImaReturnsSection, docstringDevice + docstringDtype + ImaReturnsSection)))
77
+
78
+ listImports.append(Make.ImportFrom(settingsPackage.identifierPackage, [Make.alias(callableIdentifier)]))
79
+
80
+ argumentSpecification: ast.arguments = raiseIfNone(NodeTourist(Be.arguments, Then.extractIt).captureLastMatch(astFunctionDef))
81
+ args: list[ast.expr] = [Make.Name(ast_arg.arg) for ast_arg in [*argumentSpecification.args, *argumentSpecification.kwonlyargs]]
82
+
83
+ list_keyword: list[ast.keyword] = [Make.keyword('callableTarget', Make.Name(callableIdentifier))
84
+ , Make.keyword('device', Make.Name('device'))
85
+ , Make.keyword('dtype', Make.Name('dtype'))]
86
+ if argumentSpecification.kwarg:
87
+ list_keyword.append(Make.keyword(None, value=Make.Name(argumentSpecification.kwarg.arg)))
88
+
89
+ argumentSpecification.args.append(Make.arg('device', annotation=Make.BitOr.join([Make.Name('Device'), Make.Constant(None)])))
90
+ argumentSpecification.defaults.append(Make.Constant(None))
91
+ argumentSpecification.args.append(Make.arg('dtype', annotation=Make.BitOr.join([Make.Attribute(Make.Name('torch'), 'dtype'), Make.Constant(None)])))
92
+ argumentSpecification.defaults.append(Make.Constant(None))
93
+
94
+ listBody.append(Make.FunctionDef(callableIdentifier + 'Tensor'
95
+ , argumentSpecification
96
+ , body=[docstring
97
+ , Make.Return(Make.Call(Make.Name('_convertToTensor'), listParameters=args, list_keyword=list_keyword))]
98
+ , returns=Make.Attribute(Make.Name('torch'), 'Tensor')
99
+ ))
100
+
101
+ listImports.append(Make.Import('torch'))
102
+
103
+ docstringModule = Make.Expr(Make.Constant("""Create PyTorch tensor windowing functions."""))
104
+ astModule = Make.Module(body=[docstringModule, *listImports, *listBody])
105
+
106
+ write_astModule(astModule, pathFilenameDestination, identifierPackage=settingsPackage.identifierPackage)
107
+
108
+ if __name__ == '__main__':
109
+ make_windowingFunctionsTensor()
@@ -1,4 +1,5 @@
1
- # ruff: noqa: D104
1
+ # ruff:file-ignore[undocumented-public-package]
2
+ # TODO design and implement a namespace.
2
3
  from __future__ import annotations
3
4
 
4
5
  from hunterHearsPy import theTypes as theTypes
@@ -9,7 +10,7 @@ from hunterHearsPy.windowingFunctions import cosineWings as cosineWings, equalPo
9
10
  # isort: split
10
11
  from contextlib import suppress
11
12
 
12
- with suppress(ModuleNotFoundError): # noqa: RUF067
13
+ with suppress(ModuleNotFoundError): # ruff:ignore[non-empty-init-module]
13
14
  from hunterHearsPy.windowingFunctionsTensor import (
14
15
  cosineWingsTensor as cosineWingsTensor, equalPowerTensor as equalPowerTensor, halfsineTensor as halfsineTensor,
15
16
  tukeyTensor as tukeyTensor)
@@ -35,7 +36,8 @@ from hunterHearsPy.autoRevert import moveToAxisOfOperation as moveToAxisOfOperat
35
36
  from hunterHearsPy._fft import stft as stft, waveformSpectrogramWaveform as waveformSpectrogramWaveform
36
37
 
37
38
  # isort: split
38
- from hunterHearsPy._io import readAudioFile as readAudioFile, spectrogramToWAV as spectrogramToWAV, writeWAV as writeWAV
39
+ from hunterHearsPy._io import (
40
+ getSampleRate as getSampleRate, readAudioFile as readAudioFile, spectrogramToWAV as spectrogramToWAV, writeWAV as writeWAV)
39
41
 
40
42
  # isort: split
41
43
  from hunterHearsPy.clippingArrays import applyHardLimit as applyHardLimit, applyHardLimitComplexValued as applyHardLimitComplexValued
@@ -1,4 +1,3 @@
1
- # ruff: noqa: DOC201
2
1
  from __future__ import annotations
3
2
 
4
3
  from concurrent.futures import ThreadPoolExecutor
@@ -54,6 +53,7 @@ def getWaveformMetadata(
54
53
  sys.stderr.write(message + '\n')
55
54
 
56
55
  for index, pathFilename in enumerate(tqdm(listPathFilenames, desc='Preparing combined array', leave=False)):
56
+ # NOTE Use readAudioFile with the prescribed sampleRate to get the exact length.
57
57
  channels, lengthWaveform = readAudioFile(pathFilename, sampleRate).shape
58
58
  dictionaryWaveformMetadata[index] = WaveformMetadata(
59
59
  channels=channels, lengthWaveform=lengthWaveform, pathFilename=pathFilename, samplesStart=0, samplesStop=0
@@ -71,7 +71,7 @@ def getWaveformMetadata(
71
71
 
72
72
  for metadata in dictionaryWaveformMetadata.values():
73
73
  samplesPadding: int = axis['time'].size - metadata['lengthWaveform']
74
- # TODO document that if `samplesPadding` is odd, the extra pad-sample is added to samplesStop.
74
+ # DOCUMENT that if `samplesPadding` is odd, the extra pad-sample is added to samplesStop.
75
75
  metadata['samplesStart'] = int(samplesPadding * multiplicandSamplesStart)
76
76
  metadata['samplesStop'] = metadata['samplesStart'] + metadata['lengthWaveform']
77
77
 
@@ -71,7 +71,7 @@ def stft(
71
71
  if arrayTarget.ndim == 2:
72
72
  arrayFloating: WaveformFloating = amplitudeIntegerToFloating(arrayTarget)
73
73
  elif numpy.issubdtype(arrayTarget.dtype, complexfloating) and (lengthWaveform < 1):
74
- from hunterHearsPy._io import saveOnError # noqa: PLC0415
74
+ from hunterHearsPy._io import saveOnError # ruff:ignore[import-outside-top-level]
75
75
 
76
76
  pathFilename: PurePath = saveOnError(arrayTarget)
77
77
  message: str = (
@@ -1,11 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from hunterHearsPy import amplitudeToSoundfile, getAxis, resampleWaveform, setting, stft
4
- from hunterMakesPy.filesystemToolkit import makeDirectorySafely
4
+ from hunterMakesPy.kitFilesystem import makeDirectorySafely
5
5
  from pathlib import Path, PurePath
6
6
  from soundfile import AudioData
7
7
  from typing import TYPE_CHECKING
8
- from typing_extensions import Unpack
9
8
  import numpy
10
9
  import soundfile
11
10
  import tempfile
@@ -16,20 +15,23 @@ if TYPE_CHECKING:
16
15
  from hunterHearsPy.theTypes import FileDescriptorOrPath, Parameters_stft, Spectrogram, Waveform
17
16
  from numpy import dtype, ndarray
18
17
  from soundfile import AudioData_2d, dtype_str as Options_dtype_str
19
- from typing import Any
18
+ from typing import Any, Unpack
20
19
 
21
- def readAudioFile(pathFilename: FileDescriptorOrPath, sampleRateDesired: float | None = None, dtype_str: Options_dtype_str | None = None) -> Waveform:
20
+ def getSampleRate(pathFilename: FileDescriptorOrPath) -> int:
21
+ return soundfile.info(pathFilename).samplerate
22
+
23
+ def readAudioFile(pathFilename: FileDescriptorOrPath, sampleRate: float | None = None, dtype_str: Options_dtype_str | None = None) -> Waveform:
22
24
  """Read an audio file and return waveform data as a NumPy array.
23
25
 
24
26
  You can use this function to load any audio file that `soundfile` [1] supports. The returned
25
- `Waveform` [2] is always shaped `(channels, samples)`. When `sampleRateDesired` differs from the file's
27
+ `Waveform` [2] is always shaped `(channels, samples)`. When `sampleRate` differs from the file's
26
28
  native sample rate, `readAudioFile` resamples using `resampleWaveform`.
27
29
 
28
30
  Parameters
29
31
  ----------
30
32
  pathFilename : FileDescriptorOrPath
31
33
  Path to the audio file or a binary stream.
32
- sampleRateDesired : float | None = 44100
34
+ sampleRate : float | None = 44100
33
35
  Target sample rate of the returned `Waveform` [2] in Hz. Defaults to `setting.sampleRate`,
34
36
  which is probably 44100, when `None`.
35
37
  dtype_str : Literal["float64", "float32", "int32", "int16"] | None = 'float32'
@@ -40,14 +42,50 @@ def readAudioFile(pathFilename: FileDescriptorOrPath, sampleRateDesired: float |
40
42
  waveform : Waveform
41
43
  Audio data shaped `(channels, samples)` as `setting.dtypeWaveform`.
42
44
  """
43
- sampleRateDesired = sampleRateDesired or setting.sampleRate
45
+ sampleRateDesired: float = sampleRate or setting.sampleRate
44
46
  with soundfile.SoundFile(pathFilename) as readSoundFile:
45
47
  sampleRateSource: int = readSoundFile.samplerate
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
48
+ audioData: AudioData_2d = readSoundFile.read(dtype=dtype_str or setting.dtype_str, always_2d=True)
47
49
  axis: dict[str, AxisMetadata] = getAxis()
48
50
  waveform: Waveform = audioData.transpose((axis['time'].number, axis['channel'].number))
49
51
 
50
- return resampleWaveform(waveform, sampleRateDesired, sampleRateSource, axis['time'].number) # ty:ignore[no-matching-overload] https://github.com/astral-sh/ty/issues/2799
52
+ return resampleWaveform(waveform, sampleRateDesired, sampleRateSource, axis['time'].number)
53
+
54
+ def saveOnError(arrayTarget: ndarray[tuple[Any, ...], dtype[Any]], *, identifierTarget: str = 'arrayTarget') -> PurePath:
55
+ pathFilename: Path = Path(tempfile.mkdtemp(prefix='hunterHearsPy'), f"{identifierTarget}_{uuid.uuid4().hex}.npy").resolve()
56
+ numpy.save(pathFilename, arrayTarget)
57
+
58
+ return PurePath(pathFilename)
59
+
60
+ def spectrogramToWAV(spectrogram: Spectrogram, pathFilename: FileDescriptorOrPath, lengthWaveform: int, **parametersSTFT: Unpack[Parameters_stft]) -> None:
61
+ """Write a complex spectrogram to a WAV file by computing the inverse STFT.
62
+
63
+ You can use this function to reconstruct a waveform from a `Spectrogram` [1] and save
64
+ it directly to a WAV file. `spectrogramToWAV` calls `stft` with `inverse=True` to
65
+ obtain the reconstructed `Waveform` [2], then passes it to `writeWAV`.
66
+
67
+ Parameters
68
+ ----------
69
+ spectrogram : Spectrogram
70
+ Complex spectrogram to convert back to a waveform.
71
+ pathFilename : FileDescriptorOrPath
72
+ Destination path for the WAV file, or a binary stream.
73
+ lengthWaveform : int
74
+ Number of samples in the output waveform. The inverse STFT cannot recover the
75
+ original length from the spectrogram alone, so `lengthWaveform` is required.
76
+ sampleRate : float | None = None
77
+ Sample rate for the output WAV file in Hz. Defaults to `44100` when `None`.
78
+ **parametersSTFT : Any
79
+ Keyword parameters forwarded to `stft`, such as `lengthWindowingFunction` and
80
+ `lengthHop`.
81
+
82
+ File Overwrite and Format
83
+ -------------------------
84
+ See `writeWAV` for file overwrite behavior and output format details.
85
+ """
86
+ waveform: Waveform = stft(spectrogram, lengthWaveform=lengthWaveform, **parametersSTFT)
87
+ sampleRate: float = parametersSTFT.get('sampleRate', setting.sampleRate)
88
+ writeWAV(pathFilename, waveform, sampleRate)
51
89
 
52
90
  def writeWAV(pathFilename: FileDescriptorOrPath, waveform: Waveform, sampleRate: float | None = None, subtype: str | None = None) -> FileDescriptorOrPath:
53
91
  """Write a waveform array to a WAV file.
@@ -95,46 +133,3 @@ def writeWAV(pathFilename: FileDescriptorOrPath, waveform: Waveform, sampleRate:
95
133
 
96
134
  soundfile.write(file=pathFilename, data=audioData, samplerate=sampleRate, subtype=subtype, format='WAV')
97
135
  return pathFilename
98
-
99
- def spectrogramToWAV(spectrogram: Spectrogram, pathFilename: FileDescriptorOrPath, lengthWaveform: int, **parametersSTFT: Unpack[Parameters_stft]) -> None:
100
- """Write a complex spectrogram to a WAV file by computing the inverse STFT.
101
-
102
- You can use this function to reconstruct a waveform from a `Spectrogram` [1] and save
103
- it directly to a WAV file. `spectrogramToWAV` calls `stft` with `inverse=True` to
104
- obtain the reconstructed `Waveform` [2], then passes it to `writeWAV`.
105
-
106
- Parameters
107
- ----------
108
- spectrogram : Spectrogram
109
- Complex spectrogram to convert back to a waveform.
110
- pathFilename : FileDescriptorOrPath
111
- Destination path for the WAV file, or a binary stream.
112
- lengthWaveform : int
113
- Number of samples in the output waveform. The inverse STFT cannot recover the
114
- original length from the spectrogram alone, so `lengthWaveform` is required.
115
- sampleRate : float | None = None
116
- Sample rate for the output WAV file in Hz. Defaults to `44100` when `None`.
117
- **parametersSTFT : Any
118
- Keyword parameters forwarded to `stft`, such as `lengthWindowingFunction` and
119
- `lengthHop`.
120
-
121
- File Overwrite and Format
122
- -------------------------
123
- See `writeWAV` for file overwrite behavior and output format details.
124
-
125
- References
126
- ----------
127
- [1] `Spectrogram`
128
-
129
- [2] `Waveform`
130
-
131
- """
132
- waveform: Waveform = stft(spectrogram, lengthWaveform=lengthWaveform, **parametersSTFT)
133
- sampleRate: float = parametersSTFT.get('sampleRate', setting.sampleRate)
134
- writeWAV(pathFilename, waveform, sampleRate)
135
-
136
- def saveOnError(arrayTarget: ndarray[tuple[Any, ...], dtype[Any]], *, identifierTarget: str = 'arrayTarget') -> PurePath:
137
- pathFilename: Path = Path(tempfile.mkdtemp(prefix='hunterHearsPy'), f"{identifierTarget}_{uuid.uuid4().hex}.npy").resolve()
138
- numpy.save(pathFilename, arrayTarget)
139
-
140
- return PurePath(pathFilename)
@@ -38,7 +38,7 @@ if TYPE_CHECKING:
38
38
  bits: Final[int] = 8
39
39
 
40
40
  def amplitudeIntegerToFloating(arrayTarget: ndarray[形Shape, dtype[integer[Any]]]) -> ndarray[形Shape, dtype[floating[Any]]]:
41
- """The original purpose of this function was to convert integer PCM waveforms passed to `stft`.""" # noqa: DOC201
41
+ """The original purpose of this function was to convert integer PCM waveforms passed to `stft`."""
42
42
  amplitudeMaximum: int = 2 ** ((arrayTarget.dtype.itemsize * bits) - 1) # This works for signed (2s complement) and unsigned because the MSD is the sign, right?
43
43
  # TODO Why is this fixed at float32? Can/ought I to derive it or get it from `setting` or something?
44
44
  Z0Z_floatWidth = float32
@@ -96,12 +96,12 @@ def amplitudeToSoundfile(arrayTarget: ndarray[形Shape, dtype[Any]]) -> AudioDat
96
96
 
97
97
  elif numpy.issubdtype(arrayTarget.dtype, numpy.floating):
98
98
  dtypeSoundfileFloating: frozenset[dtype[floating[Any]]] = frozenset(filter(lambda _dtype: numpy.issubdtype(_dtype, floating), dtypeSoundfile))
99
- dtypeNewFloating: dtype[floating[Any]] = min(min(arrayTarget.dtype, *dtypeSoundfileFloating), max(dtypeSoundfileFloating)) # noqa: PLW3301
99
+ dtypeNewFloating: dtype[floating[Any]] = min(min(arrayTarget.dtype, *dtypeSoundfileFloating), max(dtypeSoundfileFloating)) # ruff:ignore[nested-min-max]
100
100
  arraySoundfile = numpy.astype(arrayTarget, dtypeNewFloating, copy=False)
101
101
 
102
102
  elif numpy.issubdtype(arrayTarget.dtype, numpy.integer):
103
103
  dtypesInteger: frozenset[dtype[integer[Any]]] = frozenset(filter(lambda _dtype: numpy.issubdtype(_dtype, integer), dtypeSoundfile))
104
- dtypeNewInteger: dtype[integer[Any]] = min(min(arrayTarget.dtype, *dtypesInteger), max(dtypesInteger)) # noqa: PLW3301
104
+ dtypeNewInteger: dtype[integer[Any]] = min(min(arrayTarget.dtype, *dtypesInteger), max(dtypesInteger)) # ruff:ignore[nested-min-max]
105
105
 
106
106
  amplitudeMaximumTarget: int = 2 ** ((arrayTarget.dtype.itemsize * bits) - 1)
107
107
  amplitudeMaximumNew: int = 2 ** ((dtypeNewInteger.itemsize * bits) - 1)
@@ -118,7 +118,7 @@ def amplitudeToSoundfile(arrayTarget: ndarray[形Shape, dtype[Any]]) -> AudioDat
118
118
 
119
119
  arraySoundfile = arraySoundfile.astype(dtypeNewInteger, copy=True) # pyright: ignore[reportAssignmentType]
120
120
  else:
121
- from hunterHearsPy._io import saveOnError # noqa: PLC0415
121
+ from hunterHearsPy._io import saveOnError # ruff:ignore[import-outside-top-level]
122
122
 
123
123
  pathFilename: PurePath = saveOnError(arrayTarget)
124
124
  message: str = (
@@ -1,4 +1,4 @@
1
- # ruff: noqa: D100, D101, D102
1
+ # ruff:file-ignore[undocumented-public-module, undocumented-public-class, undocumented-public-method]
2
2
  from __future__ import annotations
3
3
 
4
4
  from typing import NamedTuple, TYPE_CHECKING, TypedDict
@@ -58,7 +58,7 @@ class WaveformMetadata(TypedDict):
58
58
  channels: int
59
59
  lengthWaveform: int
60
60
  pathFilename: FileDescriptorOrPath
61
- # NOTE If the following values were assigned directly to a `slice` object, the slice object would
61
+ # DOCUMENT If the following values were assigned directly to a `slice` object, the slice object would
62
62
  # work as desired. https://docs.python.org/3/library/functions.html#slice Therefore, maintain this
63
63
  # functionality, and keep the semiotics aligned: `slice(start, stop)`.
64
64
  samplesStart: int
@@ -1,4 +1,4 @@
1
- # ruff: noqa: D100, D101, D103
1
+ # ruff:file-ignore[undocumented-public-module, undocumented-public-class, undocumented-public-function]
2
2
  from __future__ import annotations
3
3
 
4
4
  from hunterHearsPy import AxisMetadata, tukey
@@ -1,4 +1,4 @@
1
- # ruff: noqa: D101
1
+ # ruff:file-ignore[undocumented-public-class]
2
2
  """Type definitions for audio signal processing and waveform analysis.
3
3
 
4
4
  I'm starting to feel that for _all_ identifiers in the package, there ought to be a strong correlation
@@ -10,13 +10,13 @@ if TYPE_CHECKING:
10
10
  from torch.types import Device
11
11
  from typing import Any
12
12
 
13
- def _convertToTensor(*arguments: Any, callableTarget: callableReturnsNDArray, device: Device | None=None, dtype: torch.dtype | None=None, **keywordArguments: Any) -> torch.Tensor:
13
+ def _convertToTensor(*arguments: Any, callableTarget: callableReturnsNDArray, device: Device | None = None, dtype: torch.dtype | None = None, **keywordArguments: Any) -> torch.Tensor:
14
14
  arrayTarget = callableTarget(*arguments, **keywordArguments)
15
15
  if device is None:
16
16
  device = torch.device(device='cpu')
17
17
  return torch.tensor(data=arrayTarget, dtype=dtype or torch.float32, device=device)
18
18
 
19
- def cosineWingsTensor(lengthSupport: int, ratioTaper: float=0.1, device: Device | None=None, dtype: torch.dtype | None=None) -> torch.Tensor:
19
+ def cosineWingsTensor(lengthSupport: int, ratioTaper: float = 0.1, device: Device | None = None, dtype: torch.dtype | None = None) -> torch.Tensor:
20
20
  """Generate a cosine-tapered windowingFunction with a flat center and tapered ends.
21
21
 
22
22
  You can use this function to produce a 1-D windowingFunction of length `lengthSupport` whose ends
@@ -43,7 +43,7 @@ def cosineWingsTensor(lengthSupport: int, ratioTaper: float=0.1, device: Device
43
43
  """
44
44
  return _convertToTensor(lengthSupport, ratioTaper, callableTarget=cosineWings, device=device, dtype=dtype)
45
45
 
46
- def equalPowerTensor(lengthSupport: int, ratioTaper: float=0.1, device: Device | None=None, dtype: torch.dtype | None=None) -> torch.Tensor:
46
+ def equalPowerTensor(lengthSupport: int, ratioTaper: float = 0.1, device: Device | None = None, dtype: torch.dtype | None = None) -> torch.Tensor:
47
47
  """Generate an equal-power windowingFunction suitable for crossfades.
48
48
 
49
49
  You can use this function to build a 1-D windowingFunction whose ends follow a square-root ramp.
@@ -70,7 +70,7 @@ def equalPowerTensor(lengthSupport: int, ratioTaper: float=0.1, device: Device |
70
70
  """
71
71
  return _convertToTensor(lengthSupport, ratioTaper, callableTarget=equalPower, device=device, dtype=dtype)
72
72
 
73
- def halfsineTensor(lengthSupport: int, device: Device | None=None, dtype: torch.dtype | None=None) -> torch.Tensor:
73
+ def halfsineTensor(lengthSupport: int, device: Device | None = None, dtype: torch.dtype | None = None) -> torch.Tensor:
74
74
  """Generate a half-sine windowingFunction of the requested length.
75
75
 
76
76
  This function returns a 1-D half-sine windowingFunction of length `lengthSupport`. The value at
@@ -99,7 +99,7 @@ def halfsineTensor(lengthSupport: int, device: Device | None=None, dtype: torch.
99
99
  """
100
100
  return _convertToTensor(lengthSupport, callableTarget=halfsine, device=device, dtype=dtype)
101
101
 
102
- def tukeyTensor(lengthSupport: int, ratioTaper: float=0.1, device: Device | None=None, dtype: torch.dtype | None=None, **keywordArguments: float) -> torch.Tensor:
102
+ def tukeyTensor(lengthSupport: int, ratioTaper: float = 0.1, device: Device | None = None, dtype: torch.dtype | None = None, **keywordArguments: float) -> torch.Tensor:
103
103
  """Generate a Tukey windowingFunction.
104
104
 
105
105
  You can use this function to obtain a tapered windowingFunction where the central region is
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hunterHearsPy
3
- Version: 1.3.0
3
+ Version: 1.4.1
4
4
  Summary: Audio processing.
5
5
  Author-email: Hunter Hogan <HunterHogan@pm.me>
6
6
  Maintainer-email: Hunter Hogan <HunterHogan@pm.me>
@@ -17,23 +17,27 @@ Classifier: Intended Audience :: Developers
17
17
  Classifier: Intended Audience :: Science/Research
18
18
  Classifier: Natural Language :: English
19
19
  Classifier: Operating System :: OS Independent
20
- Classifier: Programming Language :: Python :: 3.10
21
- Classifier: Programming Language :: Python :: 3.11
22
20
  Classifier: Programming Language :: Python :: 3.12
23
21
  Classifier: Programming Language :: Python :: 3.13
24
22
  Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Programming Language :: Python :: 3.15
25
24
  Classifier: Typing :: Typed
26
- Requires-Python: >=3.10
25
+ Requires-Python: >=3.12
27
26
  Description-Content-Type: text/markdown
28
27
  License-File: LICENSE
29
- Requires-Dist: Z0Z_tools>=2.1.0
30
- Requires-Dist: hunterMakesPy>=0.7.0
28
+ Requires-Dist: Z0Z_tools>=4.0.0
29
+ Requires-Dist: hunterMakesPy>=0.10.0
31
30
  Requires-Dist: numpy
32
31
  Requires-Dist: resampy
33
32
  Requires-Dist: scipy
34
33
  Requires-Dist: soundfile>=0.14.0
35
34
  Requires-Dist: tqdm
36
35
  Requires-Dist: typing-extensions>=4.0.0
36
+ Provides-Extra: development
37
+ Requires-Dist: astToolkit; extra == "development"
38
+ Requires-Dist: pytest-cov; extra == "development"
39
+ Requires-Dist: scipy-stubs; extra == "development"
40
+ Requires-Dist: types-resampy; extra == "development"
37
41
  Provides-Extra: testing
38
42
  Requires-Dist: pytest; extra == "testing"
39
43
  Requires-Dist: pytest-mock; extra == "testing"
@@ -131,13 +135,7 @@ I don't know a good way to implement user-configurable universal settings. There
131
135
  - time is more generic than samples and often preferred.
132
136
  - array is generic.
133
137
  - 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`.
138
+ - Use `sampleRate` unless I have a compelling reason not to.
141
139
 
142
140
  ### Preferred packages
143
141
 
@@ -190,6 +188,11 @@ pip install hunterHearsPy
190
188
  ## My recovery
191
189
 
192
190
  [![Static Badge](https://img.shields.io/badge/2011_August-Homeless_since-blue?style=flat)](https://HunterThinks.com/support)
193
- [![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UC3Gx7kz61009NbhpRtPP7tw)](https://www.youtube.com/@HunterHogan)
194
191
 
195
192
  [![CC-BY-NC-4.0](https://raw.githubusercontent.com/hunterhogan/hunterHearsPy/refs/heads/main/.github/CC-BY-NC-4.0.png)](https://creativecommons.org/licenses/by-nc/4.0/)
193
+
194
+ By law, authors retain their copyrights unless they grant exceptions. The [Attribution-NonCommercial 4.0 International Deed](https://github.com/hunterhogan/hunterHearsPy#License-1-ov-file) outlines how you may use this work.
195
+
196
+ The restriction on commercial use (without a license) limits advantage-taking, unjust enrichment, and exploitation. You would be morally wrong if you were to enrich yourself by using this work while I am denied healthcare, housing, security of property, and the safety of my person.
197
+
198
+ To obtain and retain the right to use this work, you incur the cost of attributing the work to me. Your nominal cost might accrue as a benefit that mitigates my suffering or saves my life.
@@ -0,0 +1,40 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ astTransformations/__init__.py
5
+ astTransformations/windowingFunctions.py
6
+ hunterHearsPy/__init__.py
7
+ hunterHearsPy/_arrays.py
8
+ hunterHearsPy/_fft.py
9
+ hunterHearsPy/_io.py
10
+ hunterHearsPy/_resample.py
11
+ hunterHearsPy/amplitude.py
12
+ hunterHearsPy/autoRevert.py
13
+ hunterHearsPy/clippingArrays.py
14
+ hunterHearsPy/dataBaskets.py
15
+ hunterHearsPy/py.typed
16
+ hunterHearsPy/theSSOT.py
17
+ hunterHearsPy/theTypes.py
18
+ hunterHearsPy/windowingFunctions.py
19
+ hunterHearsPy/windowingFunctionsTensor.py
20
+ hunterHearsPy.egg-info/PKG-INFO
21
+ hunterHearsPy.egg-info/SOURCES.txt
22
+ hunterHearsPy.egg-info/dependency_links.txt
23
+ hunterHearsPy.egg-info/requires.txt
24
+ hunterHearsPy.egg-info/top_level.txt
25
+ tests/__init__.py
26
+ tests/_theSSOT.py
27
+ tests/conftest.py
28
+ tests/conftestAnnex.py
29
+ tests/oldSampleData.py
30
+ tests/test_arrays.py
31
+ tests/test_fft.py
32
+ tests/test_io.py
33
+ tests/test_old_amplitude.py
34
+ tests/test_old_autoRevert.py
35
+ tests/test_old_clippingArrays.py
36
+ tests/test_old_spectrogram.py
37
+ tests/test_old_waveform.py
38
+ tests/test_resample.py
39
+ tests/test_windowingFunctions.py
40
+ tests/test_windowingFunctionsTensor.py
@@ -1,5 +1,5 @@
1
- Z0Z_tools>=2.1.0
2
- hunterMakesPy>=0.7.0
1
+ Z0Z_tools>=4.0.0
2
+ hunterMakesPy>=0.10.0
3
3
  numpy
4
4
  resampy
5
5
  scipy
@@ -7,6 +7,12 @@ soundfile>=0.14.0
7
7
  tqdm
8
8
  typing-extensions>=4.0.0
9
9
 
10
+ [development]
11
+ astToolkit
12
+ pytest-cov
13
+ scipy-stubs
14
+ types-resampy
15
+
10
16
  [testing]
11
17
  pytest
12
18
  pytest-mock
@@ -0,0 +1,5 @@
1
+ astTransformations
2
+ citations
3
+ dist
4
+ hunterHearsPy
5
+ tests
@@ -0,0 +1,64 @@
1
+ [project]
2
+ name = "hunterHearsPy"
3
+ version = "1.4.1"
4
+ description = "Audio processing."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "CC-BY-NC-4.0"
8
+ license-files = ["LICEN[CS]E*"]
9
+ authors = [{ name = "Hunter Hogan", email = "HunterHogan@pm.me" }]
10
+ maintainers = [{ name = "Hunter Hogan", email = "HunterHogan@pm.me" }]
11
+ keywords = []
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Environment :: Console",
15
+ "Intended Audience :: Developers",
16
+ "Intended Audience :: Science/Research",
17
+ "Natural Language :: English",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Programming Language :: Python :: 3.14",
22
+ "Programming Language :: Python :: 3.15",
23
+ "Typing :: Typed",
24
+ ]
25
+ dependencies = [
26
+ "Z0Z_tools>=4.0.0",
27
+ "hunterMakesPy>=0.10.0",
28
+ "numpy",
29
+ "resampy",
30
+ "scipy",
31
+ "soundfile>=0.14.0",
32
+ "tqdm",
33
+ "typing-extensions>=4.0.0",
34
+ ]
35
+ optional-dependencies = { development = ["astToolkit", "pytest-cov", "scipy-stubs", "types-resampy"], testing = ["pytest", "pytest-mock", "pytest-xdist", "torch"], torch = ["torch"] }
36
+
37
+ [project.urls]
38
+ Context7 = "https://context7.com/hunterhogan/hunterhearspy"
39
+ Donate = "https://www.patreon.com/integrated"
40
+ Download = "https://pypi.org/project/hunterHearsPy"
41
+ Homepage = "https://github.com/hunterhogan/hunterHearsPy"
42
+ Issues = "https://github.com/hunterhogan/hunterHearsPy/issues"
43
+ Repository = "https://github.com/hunterhogan/hunterHearsPy.git"
44
+
45
+ [build-system]
46
+ requires = ["setuptools"]
47
+ build-backend = "setuptools.build_meta"
48
+
49
+ [tool.coverage]
50
+ report = { exclude_lines = ["if TYPE_CHECKING:", "if __name__ == \"__main__\":"], ignore_errors = false }
51
+ run = { branch = true, concurrency = ["multiprocessing"], data_file = "tests/coverage/.coverage", omit = ["tests/*"], parallel = true, source = ["."] }
52
+
53
+ [tool.pytest]
54
+ addopts = ["--color=auto", "--tb=line", "-n", "4"]
55
+ log_auto_indent = "On"
56
+ testpaths = ["tests"]
57
+
58
+ [tool.setuptools]
59
+ package-data = { "*" = ["py.typed"]}
60
+ packages = { find = {} }
61
+
62
+ [tool.updateCitation]
63
+ filenameCitationDOTcff = "CITATION.cff"
64
+ pathFilenameCitationSSOT = "citations/CITATION.cff"
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from tests._theSSOT import (
4
+ dtypeTokens as dtypeTokens, pathDataSamples as pathDataSamples, pathDataSamplesExpected as pathDataSamplesExpected)
5
+ from tests.conftestAnnex import (
6
+ assert_allclose as assert_allclose, assert_approx as assert_approx, assert_array_equal as assert_array_equal,
7
+ assertEqualTo as assertEqualTo, messageTestFailure as messageTestFailure, messageTestFailure_ndarray as messageTestFailure_ndarray)
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ pathDataSamples = Path('tests/dataSamples')
6
+ pathDataSamplesExpected: Path = pathDataSamples / 'expected'
7
+
8
+ dtypeTokens: frozenset[str] = frozenset(('int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64', 'float16', 'float32', 'float64', 'complex64', 'complex128'))
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ from hashlib import blake2b
4
+ from humpy_cytoolz.dicttoolz import keyfilter, merge
5
+ from hunterHearsPy import loadWaveforms, readAudioFile
6
+ from hunterMakesPy.dataStructures import stringItUp
7
+ from more_itertools import one
8
+ from soundfile import dtype_str as Options_dtype_str
9
+ from tests import pathDataSamples, pathDataSamplesExpected
10
+ from typing import cast, TYPE_CHECKING
11
+ import inspect
12
+ import numpy
13
+ import operator
14
+ import pytest
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Sequence
18
+ from hunterHearsPy.theTypes import ArrayWaveforms, Waveform, 形ndarray
19
+ from inspect import Parameter
20
+ from pathlib import Path
21
+ from pytest import FixtureRequest
22
+ from torch.types import Device
23
+ from types import MappingProxyType
24
+
25
+ # ================== Test-function parameters ======================================================
26
+
27
+ @pytest.fixture()
28
+ def approx_abs(request: FixtureRequest) -> float:
29
+ """The `abs` (***abs***olute tolerance) parameter value for `pytest.approx`."""
30
+ return 1e-12
31
+
32
+ @pytest.fixture()
33
+ def approx_rel(request: FixtureRequest) -> float:
34
+ """The `rel` (***rel***ative tolerance) parameter value for `pytest.approx`."""
35
+ return 1e-6
36
+
37
+ @pytest.fixture()
38
+ def atol(request: FixtureRequest) -> float:
39
+ """The `atol` (***a***bsolute ***tol***erance) parameter value for `numpy.allclose`."""
40
+ return 1e-08
41
+
42
+ @pytest.fixture()
43
+ def expected(request: FixtureRequest) -> 形ndarray:
44
+ """Test-function and its parameters encoded in a `__` delimited filename.
45
+
46
+ Each parameter created with `parametrize` (as opposed to, for example, `fixture`) is encoded as `{parameter}~{value}`.
47
+ """
48
+ request_nodeParameters: MappingProxyType[str, Parameter] = cast('MappingProxyType[str, Parameter]', request.node.callspec.params) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
49
+ filenameStem: str = '__'.join((
50
+ str(request.function.__name__).removeprefix('test_')
51
+ , *(f'{keyAndValue[0]}~{"".join(stringItUp(keyAndValue[1]) or ["None"])}'
52
+ for keyAndValue in keyfilter(request_nodeParameters.keys().__contains__, merge(inspect.signature(request.function).parameters, request_nodeParameters)).items()
53
+ )))
54
+ if 251 < len(filenameStem):
55
+ filenameStem = f'{str(request.function.__name__).removeprefix("test_")}__blake2b~{blake2b(filenameStem.encode(), digest_size=16).hexdigest()}'
56
+ pathFilename: Path = pathDataSamplesExpected / f'{filenameStem}.npz'
57
+ with numpy.load(pathFilename, allow_pickle=False) as archiveExpected:
58
+ return archiveExpected['expected']
59
+
60
+ @pytest.fixture()
61
+ def rtol(request: FixtureRequest) -> float:
62
+ """The `rtol` (***r***elative ***tol***erance) parameter value for `numpy.allclose`."""
63
+ return 1e-05
64
+
65
+ # ================== Parameter values to test against the package's `Callable` =====================
66
+
67
+ @pytest.fixture()
68
+ def arrayTarget(request: FixtureRequest) -> Waveform | ArrayWaveforms:
69
+ return cast('Waveform | ArrayWaveforms', request.getfixturevalue(getattr(request, 'param', 'waveform')))
70
+
71
+ @pytest.fixture()
72
+ def arrayWaveforms(listPathFilenames: tuple[Path, ...], CPUlimit: bool | float | int | None) -> ArrayWaveforms:
73
+ return loadWaveforms(listPathFilenames, CPUlimit=CPUlimit).array
74
+
75
+ @pytest.fixture()
76
+ def CPUlimit(request: FixtureRequest) -> int:
77
+ return 1
78
+
79
+ @pytest.fixture(params=tuple(map(pytest.param, (None, 'cpu'))))
80
+ def device(request: FixtureRequest) -> Device | None:
81
+ return request.param
82
+
83
+ @pytest.fixture()
84
+ def dtype_str(pathFilename: Path) -> Options_dtype_str | None:
85
+ return one(set(Options_dtype_str.__args__).intersection(pathFilename.stem.split('_')), too_short=None)
86
+
87
+ # TODO Resolving the actual location for every `pathFilename` and `listPathFilenames` parameter MUST
88
+ # be centralized in one function.
89
+ @pytest.fixture()
90
+ def listPathFilenames(request: FixtureRequest) -> tuple[Path, ...]:
91
+ listFilenames: Sequence[str] = cast('Sequence[str]', request.node.callspec.params['listPathFilenames']) # pyright: ignore[reportUnknownMemberType]
92
+ return tuple(map(pathDataSamples.joinpath, listFilenames))
93
+
94
+ @pytest.fixture()
95
+ def pathFilename(request: FixtureRequest) -> Path:
96
+ filename: str = request.param
97
+ return pathDataSamples / filename
98
+
99
+ @pytest.fixture()
100
+ def sampleRateSource(pathFilename: Path) -> float:
101
+ return float(one(filter(str.isdecimal, map(operator.itemgetter(slice(2, None)), filter(lambda string: string.startswith('Hz'), pathFilename.stem.split('_'))))))
102
+
103
+ @pytest.fixture()
104
+ def waveform(pathFilename: Path, sampleRateSource: float, dtype_str: Options_dtype_str | None) -> Waveform:
105
+ return readAudioFile(pathFilename, sampleRateSource, dtype_str)
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from itertools import starmap
4
+ from typing import TYPE_CHECKING
5
+ import numpy
6
+ import pytest
7
+
8
+ if TYPE_CHECKING:
9
+ from hunterHearsPy.theTypes import 个, 形ndarray
10
+ from typing import Any
11
+
12
+ #================== Assert scalar and built-in containers ========================================================================
13
+
14
+ def assertEqualTo(actual: 个, expected: 个, function: str, *arguments: Any, **keywordArguments: Any) -> None:
15
+ """Assert that two objects are equal, and if not, raise an AssertionError with a detailed message."""
16
+ assert actual == expected, messageTestFailure(actual, expected, function, *arguments, **keywordArguments)
17
+
18
+ def assert_approx(actual: 个, expected: 个, pytest_rel: float, pytest_abs: float, function: str, *arguments: Any, **keywordArguments: Any) -> None:
19
+ assert actual == pytest.approx(expected, pytest_rel, pytest_abs, nan_ok=True), messageTestFailure(actual, expected, function, *arguments, **keywordArguments)
20
+
21
+ def messageTestFailure(actual: Any, expected: Any, function: str, *arguments: Any, **keywordArguments: Any) -> str:
22
+ """Format assertion message for any test comparison."""
23
+ parameters: list[str] = [*map(repr, arguments), *starmap('{}={!r}'.format, keywordArguments.items())]
24
+ return f'{function}({", ".join(parameters)}) = {actual!r}, but {expected = }.'
25
+
26
+ #================== Assert NumPy arrays ========================================================================
27
+
28
+ def assert_array_equal(actual: 形ndarray, expected: 形ndarray, function: str, *arguments: Any, **keywordArguments: Any) -> None:
29
+ """Assert that two arrays are equal, and if not, raise an AssertionError with a detailed message."""
30
+ assert numpy.array_equal(actual, expected), messageTestFailure_ndarray(actual, expected, function, *arguments, **keywordArguments)
31
+
32
+ def assert_allclose(actual: 形ndarray, expected: 形ndarray, rtol: float, atol: float, function: str, *arguments: Any, **keywordArguments: Any) -> None:
33
+ assert numpy.allclose(actual, expected, rtol, atol), messageTestFailure_ndarray(actual, expected, function, *arguments, **keywordArguments)
34
+
35
+ def messageTestFailure_ndarray(actual: 形ndarray, expected: 形ndarray, function: str, *arguments: Any, **keywordArguments: Any) -> str:
36
+ """Printing arrays is absurd."""
37
+ parameters: list[str] = [*map(repr, arguments), *starmap('{}={!r}'.format, keywordArguments.items())]
38
+ return f'{function}({", ".join(parameters)}) = {actual.shape=},\t{actual.dtype=}, but {expected.shape=}, {expected.dtype=}.'
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import cache
4
+ from hunterHearsPy import readAudioFile
5
+ from pathlib import Path
6
+ from typing import ClassVar, TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from hunterHearsPy.theTypes import Waveform
10
+
11
+ class WaveformAndMetadata:
12
+ _cacheWaveforms: ClassVar[dict[Path, Waveform]] = {}
13
+
14
+ def __init__(self, pathFilename: Path, LUFS: float, sampleRate: float, channelsTotal: int, ID: str) -> None:
15
+ self.pathFilename: Path = pathFilename
16
+ self.LUFS: float = LUFS
17
+ self.sampleRate: float = sampleRate
18
+ self.channelsTotal: int = channelsTotal
19
+ self.ID: str = ID
20
+
21
+ @property
22
+ def waveform(self) -> Waveform:
23
+ if self.pathFilename not in self._cacheWaveforms:
24
+ self._cacheWaveforms[self.pathFilename] = readAudioFile(self.pathFilename, self.sampleRate)
25
+ return self._cacheWaveforms[self.pathFilename]
26
+
27
+ pathDataSamples_labeled = Path('tests/dataSamples/labeled')
28
+
29
+ def ingestSampleData() -> list[WaveformAndMetadata]:
30
+ """Parse LUFS*.wav filenames and create WaveformData objects without loading waveforms."""
31
+ listWaveformData: list[WaveformAndMetadata] = []
32
+ for pathFilename in pathDataSamples_labeled.glob('LUFS*.wav'):
33
+ LUFSAsStr, sampleRateAsStr, channelsTotalAsStr, ID = pathFilename.stem.split('_', maxsplit=3)
34
+ LUFS = -float(LUFSAsStr[len('LUFS') :])
35
+ sampleRate = float(sampleRateAsStr)
36
+ channelsTotal = int(channelsTotalAsStr[len('ch') :])
37
+ listWaveformData.append(WaveformAndMetadata(pathFilename, LUFS, sampleRate, channelsTotal, ID))
38
+ return listWaveformData
39
+
40
+ @cache
41
+ def sampleData() -> list[WaveformAndMetadata]:
42
+ return ingestSampleData()
@@ -10,8 +10,8 @@ if TYPE_CHECKING:
10
10
  from pathlib import Path
11
11
  from soundfile import dtype_str as Options_dtype_str
12
12
 
13
- # TODO add `None` to `sampleRateDesired`.
14
- @pytest.mark.parametrize('sampleRateDesired', (44100, 48000))
13
+ # TODO add `None` to `sampleRate`.
14
+ @pytest.mark.parametrize('sampleRate', (44100, 48000))
15
15
  @pytest.mark.parametrize('pathFilename'
16
16
  , indirect=['pathFilename']
17
17
  , argvalues=(
@@ -24,7 +24,7 @@ if TYPE_CHECKING:
24
24
  , ('Music_ch2_Hz44100_float32_sec20_RMS-20.wav')
25
25
  )
26
26
  )
27
- def test_readAudioFile(pathFilename: Path, sampleRateDesired: float, dtype_str: Options_dtype_str | None, expected: Waveform) -> None:
28
- actual: Waveform = readAudioFile(pathFilename, sampleRateDesired, dtype_str)
27
+ def test_readAudioFile(pathFilename: Path, sampleRate: float, dtype_str: Options_dtype_str | None, expected: Waveform) -> None:
28
+ actual: Waveform = readAudioFile(pathFilename, sampleRate, dtype_str)
29
29
 
30
- assert_array_equal(actual, expected, 'readAudioFile', pathFilename.name, sampleRateDesired=sampleRateDesired, dtype_str=dtype_str)
30
+ assert_array_equal(actual, expected, 'readAudioFile', pathFilename.name, sampleRate=sampleRate, dtype_str=dtype_str)
@@ -1,10 +1,10 @@
1
1
  # ty:ignore[unresolved-attribute]
2
- # ruff: noqa: DOC501
2
+ # ruff:file-ignore[docstring-missing-exception]
3
3
  from __future__ import annotations
4
4
 
5
5
  from hunterHearsPy import applyHardLimit, applyHardLimitComplexValued
6
6
  from numpy import float64
7
- from numpy._core._exceptions import _UFuncNoLoopError # noqa: PLC2701
7
+ from numpy._core._exceptions import _UFuncNoLoopError # ruff:ignore[import-private-name]
8
8
  from tests import messageTestFailure
9
9
  from typing import Any, Final, TYPE_CHECKING
10
10
  import numpy
@@ -1,6 +1,6 @@
1
1
  # pyright: reportUnknownArgumentType=false
2
2
  # pyright: reportUnknownLambdaType=false
3
- # ruff: noqa: DOC501
3
+ # ruff:file-ignore[docstring-missing-exception]
4
4
  # ty:ignore[unresolved-attribute]
5
5
  """test_waveform or test_spectrogram? if a spectrogram is involved at any point, then test_spectrogram."""
6
6
 
@@ -4,7 +4,7 @@
4
4
  # pyright: reportUnknownArgumentType=false
5
5
  # pyright: reportUnknownLambdaType=false
6
6
  # pyright: reportUnknownVariableType=false
7
- # ruff: noqa: T201, PERF203, RUF076
7
+ # ruff:file-ignore[print, try-except-in-loop]
8
8
  from __future__ import annotations
9
9
 
10
10
  from hunterHearsPy import getWaveformMetadata, writeWAV
@@ -1,95 +0,0 @@
1
- [project]
2
- name = "hunterHearsPy"
3
- version = "1.3.0"
4
- description = "Audio processing."
5
- readme = "README.md"
6
- requires-python = ">=3.10"
7
- license = "CC-BY-NC-4.0"
8
- license-files = ["LICEN[CS]E*"]
9
- authors = [{ name = "Hunter Hogan", email = "HunterHogan@pm.me" }]
10
- maintainers = [{ name = "Hunter Hogan", email = "HunterHogan@pm.me" }]
11
- keywords = []
12
- classifiers = [
13
- "Development Status :: 4 - Beta",
14
- "Environment :: Console",
15
- "Intended Audience :: Developers",
16
- "Intended Audience :: Science/Research",
17
- "Natural Language :: English",
18
- "Operating System :: OS Independent",
19
- "Programming Language :: Python :: 3.10",
20
- "Programming Language :: Python :: 3.11",
21
- "Programming Language :: Python :: 3.12",
22
- "Programming Language :: Python :: 3.13",
23
- "Programming Language :: Python :: 3.14",
24
- "Typing :: Typed",
25
- ]
26
- dependencies = [
27
- "Z0Z_tools>=2.1.0",
28
- "hunterMakesPy>=0.7.0",
29
- "numpy",
30
- "resampy",
31
- "scipy",
32
- "soundfile>=0.14.0",
33
- "tqdm",
34
- "typing-extensions>=4.0.0",
35
- ]
36
- optional-dependencies = { testing = [
37
- "pytest",
38
- "pytest-mock",
39
- "pytest-xdist",
40
- "torch"
41
- ], torch = ["torch"] }
42
-
43
- [project.urls]
44
- Context7 = "https://context7.com/hunterhogan/hunterhearspy"
45
- Donate = "https://www.patreon.com/integrated"
46
- Download = "https://pypi.org/project/hunterHearsPy"
47
- Homepage = "https://github.com/hunterhogan/hunterHearsPy"
48
- Issues = "https://github.com/hunterhogan/hunterHearsPy/issues"
49
- Repository = "https://github.com/hunterhogan/hunterHearsPy.git"
50
-
51
- [dependency-groups]
52
- dev = ["astToolkit", "pytest-cov", "scipy-stubs", "types-resampy"]
53
- pytorch = ["torch==2.10.0"]
54
-
55
- [build-system]
56
- requires = ["setuptools"]
57
- build-backend = "setuptools.build_meta"
58
-
59
- [tool.coverage]
60
- report = { exclude_lines = ["if TYPE_CHECKING:", "if __name__ == \"__main__\":"], ignore_errors = false }
61
- run = { branch = true, concurrency = ["multiprocessing"], data_file = "tests/coverage/.coverage", omit = [
62
- "tests/*"
63
- ], parallel = true, source = ["."] }
64
-
65
- [tool.pytest]
66
- addopts = ["--color=auto", "--tb=line", "-n 4"]
67
- log_auto_indent = "On"
68
- pythonpath = ["src"]
69
- testpaths = ["tests"]
70
-
71
- [tool.updateCitation]
72
- filenameCitationDOTcff = "CITATION.cff"
73
- pathFilenameCitationSSOT = "citations/CITATION.cff"
74
-
75
- [tool.uv]
76
- build-backend = { module-name = "hunterHearsPy", module-root = "src" }
77
- default-groups = ["dev"]
78
-
79
- [[tool.uv.index]]
80
- # NOTE I use this to install torch+cuda on my computer that doesn't have cuda. 2026-06-13
81
- # `uv sync --group pytorch` OR `uv sync --all-groups` will trigger the following.
82
- # dependency-groups.pytorch amends the dependencies: `torch`, limitation `==2.10.0`, group = "pytorch".
83
- # At this point, the default source, pypi, is the highest priority on the `torch` sources list. (LIFO)
84
- # tool.uv.sources sees '`torch`, group = "pytorch"' so pushes "indexPytorch" on top the `torch` sources list.
85
- # tool.uv.index translates "indexPytorch" to the url of a package index.
86
- # uv queries the package index for all `torch` that match all limitations.
87
- # If the package index returns any matches, uv will install the "best" match.
88
- # Because the package index only has cuda 12.8 versions, uv installs torch+cu128.
89
- # I picked torch==2.10.0 + cuda 12.8 because that is the pre-installed package on Google Colab T4.
90
- explicit = true
91
- name = "indexPytorch"
92
- url = "https://download.pytorch.org/whl/cu128"
93
-
94
- [tool.uv.sources]
95
- torch = { group = "pytorch", index = "indexPytorch" }
@@ -1,33 +0,0 @@
1
- LICENSE
2
- README.md
3
- pyproject.toml
4
- src/hunterHearsPy/__init__.py
5
- src/hunterHearsPy/_arrays.py
6
- src/hunterHearsPy/_fft.py
7
- src/hunterHearsPy/_io.py
8
- src/hunterHearsPy/_resample.py
9
- src/hunterHearsPy/amplitude.py
10
- src/hunterHearsPy/autoRevert.py
11
- src/hunterHearsPy/clippingArrays.py
12
- src/hunterHearsPy/dataBaskets.py
13
- src/hunterHearsPy/py.typed
14
- src/hunterHearsPy/theSSOT.py
15
- src/hunterHearsPy/theTypes.py
16
- src/hunterHearsPy/windowingFunctions.py
17
- src/hunterHearsPy/windowingFunctionsTensor.py
18
- src/hunterHearsPy.egg-info/PKG-INFO
19
- src/hunterHearsPy.egg-info/SOURCES.txt
20
- src/hunterHearsPy.egg-info/dependency_links.txt
21
- src/hunterHearsPy.egg-info/requires.txt
22
- src/hunterHearsPy.egg-info/top_level.txt
23
- tests/test_arrays.py
24
- tests/test_fft.py
25
- tests/test_io.py
26
- tests/test_old_amplitude.py
27
- tests/test_old_autoRevert.py
28
- tests/test_old_clippingArrays.py
29
- tests/test_old_spectrogram.py
30
- tests/test_old_waveform.py
31
- tests/test_resample.py
32
- tests/test_windowingFunctions.py
33
- tests/test_windowingFunctionsTensor.py
@@ -1 +0,0 @@
1
- hunterHearsPy
File without changes
File without changes