phonipy 0.1.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.
@@ -0,0 +1,30 @@
1
+ name: Publish to PyPI
2
+ on:
3
+ release:
4
+ types: [published]
5
+ permissions:
6
+ contents: read
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ environment: pypi
11
+ permissions:
12
+ id-token: write
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: '3.12'
18
+ - run: python -m pip install --upgrade pip certifi
19
+ - run: python -m pip install -e '.[test,app]'
20
+ - run: ruff check .
21
+ - run: pytest
22
+ - run: python -m build
23
+ - run: python -m twine check dist/*
24
+ - name: Check release tag matches package version
25
+ env:
26
+ RELEASE_TAG: ${{ github.event.release.tag_name }}
27
+ run: python -c "import os, phonipy; assert os.environ['RELEASE_TAG'] == 'v' + phonipy.__version__"
28
+ - uses: pypa/gh-action-pypi-publish@release/v1
29
+ with:
30
+ attestations: true
@@ -0,0 +1,28 @@
1
+ name: Test and build
2
+ on:
3
+ push:
4
+ pull_request:
5
+ permissions:
6
+ contents: read
7
+ jobs:
8
+ test:
9
+ strategy:
10
+ fail-fast: false
11
+ matrix:
12
+ os: [ubuntu-latest, macos-latest, windows-latest]
13
+ python: ['3.10', '3.12', '3.14']
14
+ runs-on: ${{ matrix.os }}
15
+ env:
16
+ MPLBACKEND: Agg
17
+ STREAMLIT_BROWSER_GATHER_USAGE_STATS: 'false'
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python }}
23
+ - run: python -m pip install --upgrade pip certifi
24
+ - run: python -m pip install -e '.[test,app]'
25
+ - run: ruff check .
26
+ - run: pytest --cov=phonipy --cov-report=term-missing
27
+ - run: python -m build
28
+ - run: python -m twine check dist/*
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .coverage
7
+ htmlcov/
8
+ dist/
9
+ *.egg-info/
10
+ benchmarks/data/
11
+ benchmarks/results/
12
+ .DS_Store
13
+ .env
14
+ .pypirc
phonipy-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Muhammad Umair Danish
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
phonipy-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,214 @@
1
+ Metadata-Version: 2.5
2
+ Name: phonipy
3
+ Version: 0.1.0
4
+ Summary: Reproducible musical-note stability and harmonic-purity analysis of voice recordings
5
+ Project-URL: Homepage, https://github.com/udanish50/PhoniPy
6
+ Project-URL: Issues, https://github.com/udanish50/PhoniPy/issues
7
+ Author: Muhammad Umair Danish
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: audio,music-therapy,phonation,pitch,voice
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
14
+ Classifier: Topic :: Scientific/Engineering
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: numpy>=1.24
17
+ Requires-Dist: scipy>=1.10
18
+ Requires-Dist: soundfile>=0.12
19
+ Provides-Extra: app
20
+ Requires-Dist: matplotlib>=3.7; extra == 'app'
21
+ Requires-Dist: pandas>=2; extra == 'app'
22
+ Requires-Dist: streamlit>=1.40; extra == 'app'
23
+ Provides-Extra: benchmark
24
+ Requires-Dist: certifi>=2026.10.19; extra == 'benchmark'
25
+ Requires-Dist: wfdb>=4.1; extra == 'benchmark'
26
+ Provides-Extra: excel
27
+ Requires-Dist: openpyxl>=3.1; extra == 'excel'
28
+ Provides-Extra: plot
29
+ Requires-Dist: matplotlib>=3.7; extra == 'plot'
30
+ Provides-Extra: test
31
+ Requires-Dist: build>=1.2; extra == 'test'
32
+ Requires-Dist: matplotlib>=3.7; extra == 'test'
33
+ Requires-Dist: openpyxl>=3.1; extra == 'test'
34
+ Requires-Dist: pytest-cov>=5; extra == 'test'
35
+ Requires-Dist: pytest>=8; extra == 'test'
36
+ Requires-Dist: ruff>=0.9; extra == 'test'
37
+ Requires-Dist: twine>=6; extra == 'test'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # PhoniPy
41
+
42
+ Reproducible musical-note stability and harmonic-purity analysis for solo voice recordings.
43
+ PhoniPy reads audio, detects stable note intervals, computes all ten parameters from
44
+ *Musical-Key Stability Analysis of Continuous Voice*, and exports recording summaries,
45
+ frame data, segment data, and composite plots.
46
+
47
+ **Research software, version 0.1.0.** The detector is an explicitly documented implementation
48
+ of the manuscript's framework, not a verified reproduction of its unavailable prototype.
49
+ The manuscript's clinical claims and thresholds are not validated by this package.
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ pip install phonipy
55
+ # Optional exports, plots, dashboard:
56
+ pip install 'phonipy[excel,plot,app]'
57
+ ```
58
+
59
+ Python 3.10 or newer. Core dependencies: NumPy, SciPy, and SoundFile/libsndfile.
60
+ WAV, FLAC, OGG, AIFF and other libsndfile-supported formats are accepted. MP3 support
61
+ depends on the installed libsndfile build; unsupported codecs produce a per-file error.
62
+ For compressed video/audio containers such as M4A, convert to PCM WAV first.
63
+
64
+ ## Python API
65
+
66
+ ```python
67
+ from phonipy import AnalysisConfig, analyze_file, analyze_directory
68
+
69
+ config = AnalysisConfig.preset("speech", reference_key=57) # A3, MIDI numbering
70
+ result = analyze_file("voice.wav", config)
71
+ print(result.metrics)
72
+ result.to_json("voice-results.json", include_frames=True)
73
+ result.plot("voice-composite.png") # requires phonipy[plot]
74
+
75
+ batch = analyze_directory("recordings", config, recursive=True)
76
+ batch.save("results.csv") # also JSON or XLSX; failures are retained
77
+ print(batch.errors)
78
+ ```
79
+
80
+ The package also accepts NumPy arrays with `analyze_samples(samples, sample_rate, config)`.
81
+ Arrays use samples-by-channels order; integer PCM is scaled automatically. Floating PCM
82
+ should be approximately within -1 to 1. No automatic gain normalization is performed.
83
+
84
+ ### Familiar midipy-style batch API
85
+
86
+ ```python
87
+ from phonipy import parser
88
+
89
+ table = parser(
90
+ "recordings",
91
+ metrics=["source", "valid_time", "average_energy", "breakouts"],
92
+ output_format="csv",
93
+ save_path="VoiceOutput",
94
+ )
95
+ ```
96
+
97
+ `parser` requires pandas (included with `phonipy[app]`). `metrics` selects returned DataFrame
98
+ columns; the saved analysis summary always retains all ten metrics and failures for auditing.
99
+ Prefer the core batch API when pandas is not needed. No outputs are created by analysis
100
+ functions until an explicit save/export call.
101
+
102
+ ## Command line
103
+
104
+ ```bash
105
+ phonipy voice.wav -o result.json --reference-key 57 --details details --plot
106
+ phonipy recordings -o results.csv --recursive --preset speech
107
+ python -m phonipy recordings -o results.xlsx --config examples/config.json
108
+ ```
109
+
110
+ Exit codes: **0** complete success, **1** batch completed with failed files, **2** invalid
111
+ input/settings/output or a single-file failure. Batch processing continues after corrupt
112
+ files by default; `--strict` stops on the first failure. JSON includes settings, warnings,
113
+ and failures; CSV/XLSX include failure rows plus a `.metadata.json` sidecar. `--details`
114
+ adds full JSON and frame/segment CSVs. Duplicate filenames receive distinct numeric prefixes.
115
+
116
+ ## Local dashboard
117
+
118
+ ```bash
119
+ pip install 'phonipy[app]'
120
+ streamlit run "$(python -c 'import phonipy.app; print(phonipy.app.__file__)')"
121
+ ```
122
+
123
+ Upload multiple files, adjust settings, view composite plots, and download CSV/JSON.
124
+ Analysis runs locally without uploads to an analysis service or telemetry from PhoniPy.
125
+ The dashboard limits each recording to five minutes and 100 MB.
126
+
127
+ ## The ten metrics
128
+
129
+ Let `d_i = end_i - start_i` for each valid stable-note segment; `N` is the segment count.
130
+
131
+ | Metric | Definition | Unit |
132
+ | --- | --- | --- |
133
+ | `raw_duration` | Original sample count / original sample rate | s |
134
+ | `active_time` | Last valid offset − first valid onset | s |
135
+ | `valid_time` | Sum of valid segment durations | s |
136
+ | `average_offset` | Unweighted segment mean of fractional pitch − reference note | semitones |
137
+ | `average_energy` | Duration-weighted harmonic-energy ratio across valid intervals | 0–1 |
138
+ | `hoarseness` | `(1 - average_energy) * 100` | % |
139
+ | `average_duration` | `valid_time / N` | s |
140
+ | `continuity` | `average_duration / valid_time` | dimensionless |
141
+ | `sparsity` | `valid_time / active_time` | 0–1 |
142
+ | `breakouts` | `N`, including the initial acquisition | count |
143
+
144
+ No valid detections: time totals, breakouts, continuity and sparsity are zero; undefined
145
+ means and hoarseness are `null` in JSON and blank in CSV/XLSX. **Continuity equals `1/N`**
146
+ whenever N > 0; it is retained exactly as specified, not presented as an independent metric.
147
+ The manuscript uses two inconsistent descriptions of breakouts; this package follows
148
+ Appendix A's segment count. Signed pitch offsets can cancel. `hoarseness` is the manuscript's
149
+ name for an acoustic residual-energy transform, not a clinical hoarseness rating.
150
+
151
+ ## Detector and reproducibility
152
+
153
+ - Default analysis rate: 16 kHz; hop: 17.25 ms (276 samples); Hann window: 69 ms.
154
+ - Normalized autocorrelation supplies candidate periods; the earliest sufficiently strong
155
+ local peak is refined with parabolic interpolation. This makes the fundamental candidate
156
+ stage explicit where the manuscript only says "strongest harmonic series."
157
+ - Harmonic-series energy is measured from a one-sided Hann-windowed power spectrum in
158
+ nonoverlapping frequency bands around up to 12 harmonics; total energy includes all bins.
159
+ - RMS, periodicity, spectral flatness, pitch range, and harmonic ratio gate detections.
160
+ - The identical rounded MIDI note must persist for at least eight frames. Accepted runs are
161
+ backdated to their first frame, not the eighth. Invalid/unvoiced frames end a run.
162
+ - Each frame represents a hop interval. Durations use disjoint intervals and the last interval
163
+ is clipped to the original duration. Forward-looking windows are zero-padded at the end.
164
+ Overlapping spectral windows do not create overlapping measured durations.
165
+ - Fractional pitch is retained separately for pitch offset. A4=440 Hz is MIDI note 69;
166
+ traditional 88-key piano index = MIDI number minus 20. Differences are identical.
167
+ - All effective settings, sample rates, channel selection, and warnings are exported.
168
+
169
+ See [methodology](docs/methodology.md) for equations and limitations.
170
+
171
+ ## Input handling and limits
172
+
173
+ Stereo/multichannel recordings select the loudest channel by default to avoid phase cancellation.
174
+ Choose `channel="mix"`, `"left"`, or `"right"` explicitly if desired. Resampling is polyphase.
175
+ NaN/infinite samples raise an error unless `nonfinite="zero"` explicitly enables replacement.
176
+ Empty/corrupt inputs, absent channels, invalid ranges, unavailable codecs, and excessive
177
+ durations produce actionable errors. Clipping/DC offset and short input produce warnings.
178
+ The default one-hour duration cap is configurable. Audio and frame results are held in memory;
179
+ split very large recordings into tasks. Output failures are surfaced rather than silently ignored.
180
+
181
+ Use quiet, solo voice recordings with consistent microphones and levels. Stable vowels and
182
+ single-note holds are the clearest tasks. Speech has natural unvoiced intervals and singing
183
+ has intentional pitch changes; their scores should not be directly compared with vowel scores.
184
+ Noise, reverberation, vibrato, note-boundary quantization, and octave errors can affect results.
185
+ No automatic recovery can reconstruct information absent from a recording.
186
+
187
+ ## Tests and benchmarks
188
+
189
+ ```bash
190
+ pip install -e '.[test,app,benchmark]'
191
+ pytest --cov=phonipy --cov-report=term-missing
192
+ ruff check .
193
+ python benchmarks/download.py --data-dir benchmarks/data
194
+ python benchmarks/run.py --data-dir benchmarks/data --output benchmarks/results
195
+ python -m build
196
+ python -m twine check dist/*
197
+ ```
198
+
199
+ The benchmark scripts analyze synthetic signals with known pitch/timing, all 3,000 recordings
200
+ in a pinned Free Spoken Digit Dataset snapshot, and all 208 VOICED sustained-vowel records.
201
+ They record source hashes, settings, dependency versions, wall time, failures, and per-file metrics.
202
+ Real recordings have no ground-truth values for these novel metrics: those runs demonstrate
203
+ processing coverage, not clinical accuracy or disease classification.
204
+ See [benchmark protocol](benchmarks/README.md) and [results](docs/benchmark-results.md).
205
+
206
+ ## Attribution and license
207
+
208
+ Implementation: Muhammad Umair Danish. Scientific framework: the user-supplied manuscript
209
+ *Musical-Key Stability Analysis of Continuous Voice: A Methodology for Quantifying Phonatory
210
+ Continuity and Purity in Neurological Disorders with Applications in Music Therapy*, by
211
+ Demian Kogutek and Danish Umair Muhammad. The manuscript itself is not redistributed.
212
+
213
+ Code: MIT. Benchmark recordings retain their upstream licenses and are not bundled in
214
+ the wheel/source distribution. See the benchmark protocol for dataset citations.
@@ -0,0 +1,175 @@
1
+ # PhoniPy
2
+
3
+ Reproducible musical-note stability and harmonic-purity analysis for solo voice recordings.
4
+ PhoniPy reads audio, detects stable note intervals, computes all ten parameters from
5
+ *Musical-Key Stability Analysis of Continuous Voice*, and exports recording summaries,
6
+ frame data, segment data, and composite plots.
7
+
8
+ **Research software, version 0.1.0.** The detector is an explicitly documented implementation
9
+ of the manuscript's framework, not a verified reproduction of its unavailable prototype.
10
+ The manuscript's clinical claims and thresholds are not validated by this package.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install phonipy
16
+ # Optional exports, plots, dashboard:
17
+ pip install 'phonipy[excel,plot,app]'
18
+ ```
19
+
20
+ Python 3.10 or newer. Core dependencies: NumPy, SciPy, and SoundFile/libsndfile.
21
+ WAV, FLAC, OGG, AIFF and other libsndfile-supported formats are accepted. MP3 support
22
+ depends on the installed libsndfile build; unsupported codecs produce a per-file error.
23
+ For compressed video/audio containers such as M4A, convert to PCM WAV first.
24
+
25
+ ## Python API
26
+
27
+ ```python
28
+ from phonipy import AnalysisConfig, analyze_file, analyze_directory
29
+
30
+ config = AnalysisConfig.preset("speech", reference_key=57) # A3, MIDI numbering
31
+ result = analyze_file("voice.wav", config)
32
+ print(result.metrics)
33
+ result.to_json("voice-results.json", include_frames=True)
34
+ result.plot("voice-composite.png") # requires phonipy[plot]
35
+
36
+ batch = analyze_directory("recordings", config, recursive=True)
37
+ batch.save("results.csv") # also JSON or XLSX; failures are retained
38
+ print(batch.errors)
39
+ ```
40
+
41
+ The package also accepts NumPy arrays with `analyze_samples(samples, sample_rate, config)`.
42
+ Arrays use samples-by-channels order; integer PCM is scaled automatically. Floating PCM
43
+ should be approximately within -1 to 1. No automatic gain normalization is performed.
44
+
45
+ ### Familiar midipy-style batch API
46
+
47
+ ```python
48
+ from phonipy import parser
49
+
50
+ table = parser(
51
+ "recordings",
52
+ metrics=["source", "valid_time", "average_energy", "breakouts"],
53
+ output_format="csv",
54
+ save_path="VoiceOutput",
55
+ )
56
+ ```
57
+
58
+ `parser` requires pandas (included with `phonipy[app]`). `metrics` selects returned DataFrame
59
+ columns; the saved analysis summary always retains all ten metrics and failures for auditing.
60
+ Prefer the core batch API when pandas is not needed. No outputs are created by analysis
61
+ functions until an explicit save/export call.
62
+
63
+ ## Command line
64
+
65
+ ```bash
66
+ phonipy voice.wav -o result.json --reference-key 57 --details details --plot
67
+ phonipy recordings -o results.csv --recursive --preset speech
68
+ python -m phonipy recordings -o results.xlsx --config examples/config.json
69
+ ```
70
+
71
+ Exit codes: **0** complete success, **1** batch completed with failed files, **2** invalid
72
+ input/settings/output or a single-file failure. Batch processing continues after corrupt
73
+ files by default; `--strict` stops on the first failure. JSON includes settings, warnings,
74
+ and failures; CSV/XLSX include failure rows plus a `.metadata.json` sidecar. `--details`
75
+ adds full JSON and frame/segment CSVs. Duplicate filenames receive distinct numeric prefixes.
76
+
77
+ ## Local dashboard
78
+
79
+ ```bash
80
+ pip install 'phonipy[app]'
81
+ streamlit run "$(python -c 'import phonipy.app; print(phonipy.app.__file__)')"
82
+ ```
83
+
84
+ Upload multiple files, adjust settings, view composite plots, and download CSV/JSON.
85
+ Analysis runs locally without uploads to an analysis service or telemetry from PhoniPy.
86
+ The dashboard limits each recording to five minutes and 100 MB.
87
+
88
+ ## The ten metrics
89
+
90
+ Let `d_i = end_i - start_i` for each valid stable-note segment; `N` is the segment count.
91
+
92
+ | Metric | Definition | Unit |
93
+ | --- | --- | --- |
94
+ | `raw_duration` | Original sample count / original sample rate | s |
95
+ | `active_time` | Last valid offset − first valid onset | s |
96
+ | `valid_time` | Sum of valid segment durations | s |
97
+ | `average_offset` | Unweighted segment mean of fractional pitch − reference note | semitones |
98
+ | `average_energy` | Duration-weighted harmonic-energy ratio across valid intervals | 0–1 |
99
+ | `hoarseness` | `(1 - average_energy) * 100` | % |
100
+ | `average_duration` | `valid_time / N` | s |
101
+ | `continuity` | `average_duration / valid_time` | dimensionless |
102
+ | `sparsity` | `valid_time / active_time` | 0–1 |
103
+ | `breakouts` | `N`, including the initial acquisition | count |
104
+
105
+ No valid detections: time totals, breakouts, continuity and sparsity are zero; undefined
106
+ means and hoarseness are `null` in JSON and blank in CSV/XLSX. **Continuity equals `1/N`**
107
+ whenever N > 0; it is retained exactly as specified, not presented as an independent metric.
108
+ The manuscript uses two inconsistent descriptions of breakouts; this package follows
109
+ Appendix A's segment count. Signed pitch offsets can cancel. `hoarseness` is the manuscript's
110
+ name for an acoustic residual-energy transform, not a clinical hoarseness rating.
111
+
112
+ ## Detector and reproducibility
113
+
114
+ - Default analysis rate: 16 kHz; hop: 17.25 ms (276 samples); Hann window: 69 ms.
115
+ - Normalized autocorrelation supplies candidate periods; the earliest sufficiently strong
116
+ local peak is refined with parabolic interpolation. This makes the fundamental candidate
117
+ stage explicit where the manuscript only says "strongest harmonic series."
118
+ - Harmonic-series energy is measured from a one-sided Hann-windowed power spectrum in
119
+ nonoverlapping frequency bands around up to 12 harmonics; total energy includes all bins.
120
+ - RMS, periodicity, spectral flatness, pitch range, and harmonic ratio gate detections.
121
+ - The identical rounded MIDI note must persist for at least eight frames. Accepted runs are
122
+ backdated to their first frame, not the eighth. Invalid/unvoiced frames end a run.
123
+ - Each frame represents a hop interval. Durations use disjoint intervals and the last interval
124
+ is clipped to the original duration. Forward-looking windows are zero-padded at the end.
125
+ Overlapping spectral windows do not create overlapping measured durations.
126
+ - Fractional pitch is retained separately for pitch offset. A4=440 Hz is MIDI note 69;
127
+ traditional 88-key piano index = MIDI number minus 20. Differences are identical.
128
+ - All effective settings, sample rates, channel selection, and warnings are exported.
129
+
130
+ See [methodology](docs/methodology.md) for equations and limitations.
131
+
132
+ ## Input handling and limits
133
+
134
+ Stereo/multichannel recordings select the loudest channel by default to avoid phase cancellation.
135
+ Choose `channel="mix"`, `"left"`, or `"right"` explicitly if desired. Resampling is polyphase.
136
+ NaN/infinite samples raise an error unless `nonfinite="zero"` explicitly enables replacement.
137
+ Empty/corrupt inputs, absent channels, invalid ranges, unavailable codecs, and excessive
138
+ durations produce actionable errors. Clipping/DC offset and short input produce warnings.
139
+ The default one-hour duration cap is configurable. Audio and frame results are held in memory;
140
+ split very large recordings into tasks. Output failures are surfaced rather than silently ignored.
141
+
142
+ Use quiet, solo voice recordings with consistent microphones and levels. Stable vowels and
143
+ single-note holds are the clearest tasks. Speech has natural unvoiced intervals and singing
144
+ has intentional pitch changes; their scores should not be directly compared with vowel scores.
145
+ Noise, reverberation, vibrato, note-boundary quantization, and octave errors can affect results.
146
+ No automatic recovery can reconstruct information absent from a recording.
147
+
148
+ ## Tests and benchmarks
149
+
150
+ ```bash
151
+ pip install -e '.[test,app,benchmark]'
152
+ pytest --cov=phonipy --cov-report=term-missing
153
+ ruff check .
154
+ python benchmarks/download.py --data-dir benchmarks/data
155
+ python benchmarks/run.py --data-dir benchmarks/data --output benchmarks/results
156
+ python -m build
157
+ python -m twine check dist/*
158
+ ```
159
+
160
+ The benchmark scripts analyze synthetic signals with known pitch/timing, all 3,000 recordings
161
+ in a pinned Free Spoken Digit Dataset snapshot, and all 208 VOICED sustained-vowel records.
162
+ They record source hashes, settings, dependency versions, wall time, failures, and per-file metrics.
163
+ Real recordings have no ground-truth values for these novel metrics: those runs demonstrate
164
+ processing coverage, not clinical accuracy or disease classification.
165
+ See [benchmark protocol](benchmarks/README.md) and [results](docs/benchmark-results.md).
166
+
167
+ ## Attribution and license
168
+
169
+ Implementation: Muhammad Umair Danish. Scientific framework: the user-supplied manuscript
170
+ *Musical-Key Stability Analysis of Continuous Voice: A Methodology for Quantifying Phonatory
171
+ Continuity and Purity in Neurological Disorders with Applications in Music Therapy*, by
172
+ Demian Kogutek and Danish Umair Muhammad. The manuscript itself is not redistributed.
173
+
174
+ Code: MIT. Benchmark recordings retain their upstream licenses and are not bundled in
175
+ the wheel/source distribution. See the benchmark protocol for dataset citations.
@@ -0,0 +1,42 @@
1
+ # Benchmark protocol
2
+
3
+ Run from the repository root after `pip install -e '.[test,benchmark]'`.
4
+
5
+ ```bash
6
+ python benchmarks/download.py --data-dir benchmarks/data
7
+ python benchmarks/run.py --data-dir benchmarks/data --output benchmarks/results
8
+ ```
9
+
10
+ Use `--datasets synthetic fsdd` or `--datasets voiced` to run selected corpora. Runs in the
11
+ same output directory merge summaries only when configuration and dependency versions match.
12
+
13
+ ## Sources
14
+
15
+ 1. **Free Spoken Digit Dataset (FSDD)**, Jakobovski and contributors:
16
+ https://github.com/Jakobovski/free-spoken-digit-dataset
17
+ Snapshot: `26eb9aaf76e81b692f806f9140c2d2777410d7a1`.
18
+ CC BY-SA 4.0; 3,000 8-kHz English digit recordings from six speakers.
19
+ All files are processed; no training or classification is performed.
20
+ 2. **VOICED Database**, version 1.0.0:
21
+ https://physionet.org/content/voiced/1.0.0/
22
+ DOI: https://doi.org/10.13026/C25Q2N
23
+ License: Open Data Commons Attribution License v1.0.
24
+ 208 sustained-vowel records, including healthy and pathological voices.
25
+ All records are processed as a technical coverage benchmark; no disease labels are predicted.
26
+ WFDB `p_signal` is decoded using the record's physical gain/baseline. It is not
27
+ per-recording peak-normalized, so amplitude gating remains an explicit limitation.
28
+ 3. **PhoniPy synthetic-v1**: generated sines, harmonic tones, interrupted holds, noise,
29
+ and silence with fixed RNG seed. Code is MIT, with known pitch for periodic cases.
30
+
31
+ Downloads use HTTPS with verification, timeout/retry, safe selective FSDD archive extraction,
32
+ and SHA-256 provenance manifests. VOICED signals/headers are checked against PhysioNet's
33
+ official SHA256SUMS. Raw third-party recordings are never included in package
34
+ distributions or committed. Dataset hashes and per-file result hashes are saved with results.
35
+ The benchmark writes all ten metrics for each recording, failure manifests, summaries,
36
+ dependency/platform metadata, and synthetic pitch-error measurements. Low/zero detection
37
+ coverage is reported, never hidden as a successful clinical measurement.
38
+
39
+ No ground-truth values for the ten manuscript metrics are supplied by these corpora. Real-data
40
+ results measure executability and detector coverage, not pitch-estimation accuracy, clinical
41
+ validity, or sensitivity/specificity. Use a separate annotated pitch corpus and clinical study
42
+ before making those claims.
@@ -0,0 +1,133 @@
1
+ """Download pinned public benchmarks; no credentials needed and no raw data committed."""
2
+
3
+ import argparse
4
+ import hashlib
5
+ import json
6
+ import re
7
+ import shutil
8
+ import ssl
9
+ import time
10
+ import zipfile
11
+ from concurrent.futures import ThreadPoolExecutor
12
+ from pathlib import Path, PurePosixPath
13
+ from urllib.error import URLError
14
+ from urllib.request import urlopen
15
+
16
+ import certifi
17
+
18
+ FSDD_COMMIT = "26eb9aaf76e81b692f806f9140c2d2777410d7a1"
19
+ FSDD_URL = f"https://codeload.github.com/Jakobovski/free-spoken-digit-dataset/zip/{FSDD_COMMIT}"
20
+ VOICED_URL = "https://physionet.org/files/voiced/1.0.0/"
21
+
22
+
23
+ def sha256_file(path):
24
+ digest = hashlib.sha256()
25
+ with Path(path).open("rb") as handle:
26
+ for block in iter(lambda: handle.read(1024 * 1024), b""):
27
+ digest.update(block)
28
+ return digest.hexdigest()
29
+
30
+
31
+ def download(url, path, expected=None):
32
+ if path.exists() and (expected is None or sha256_file(path) == expected):
33
+ return
34
+ partial = path.with_name(path.name + ".part")
35
+ for attempt in range(3):
36
+ try:
37
+ context = ssl.create_default_context(cafile=certifi.where())
38
+ with urlopen(url, timeout=60, context=context) as response, partial.open("wb") as out:
39
+ shutil.copyfileobj(response, out)
40
+ if expected is not None and sha256_file(partial) != expected:
41
+ raise ValueError(f"Official checksum mismatch: {path.name}")
42
+ partial.replace(path)
43
+ return
44
+ except (URLError, OSError, ValueError):
45
+ if attempt == 2:
46
+ raise
47
+ time.sleep(2**attempt)
48
+
49
+
50
+ def file_manifest(output, root):
51
+ return [
52
+ {"file": path.relative_to(root).as_posix(), "sha256": sha256_file(path)}
53
+ for path in sorted(output.rglob("*"))
54
+ if path.is_file()
55
+ ]
56
+
57
+
58
+ def fetch_fsdd(root):
59
+ archive = root / "fsdd.zip"
60
+ print("Downloading/extracting pinned FSDD...", flush=True)
61
+ download(FSDD_URL, archive)
62
+ output = root / "fsdd"
63
+ output.mkdir(exist_ok=True)
64
+ with zipfile.ZipFile(archive) as handle:
65
+ for member in handle.infolist():
66
+ path = PurePosixPath(member.filename)
67
+ if member.is_dir() or ".." in path.parts or path.is_absolute():
68
+ continue
69
+ if "recordings" not in path.parts and path.name not in ("README.md", "LICENSE"):
70
+ continue
71
+ relative = (
72
+ Path("recordings") / path.name if "recordings" in path.parts else Path(path.name)
73
+ )
74
+ destination = output / relative
75
+ destination.parent.mkdir(parents=True, exist_ok=True)
76
+ with handle.open(member) as src, destination.open("wb") as dst:
77
+ shutil.copyfileobj(src, dst)
78
+ return {
79
+ "dataset": "fsdd",
80
+ "url": FSDD_URL,
81
+ "snapshot": FSDD_COMMIT,
82
+ "archive_sha256": sha256_file(archive),
83
+ "license": "CC-BY-SA-4.0",
84
+ "files": file_manifest(output, root),
85
+ }
86
+
87
+
88
+ def fetch_voiced(root):
89
+ output = root / "voiced"
90
+ output.mkdir(exist_ok=True)
91
+ download(VOICED_URL + "RECORDS", output / "RECORDS")
92
+ download(VOICED_URL + "SHA256SUMS.txt", output / "SHA256SUMS.txt")
93
+ expected = {}
94
+ for line in (output / "SHA256SUMS.txt").read_text().splitlines():
95
+ digest, filename = line.split(maxsplit=1)
96
+ expected[PurePosixPath(filename.lstrip("*")).name] = digest
97
+ records = (output / "RECORDS").read_text().splitlines()
98
+ if len(records) != 208 or not all(re.fullmatch(r"voice\d{3}", r) for r in records):
99
+ raise ValueError("Unexpected VOICED record manifest")
100
+ files = [r + suffix for r in records for suffix in (".hea", ".dat")]
101
+ if not all(name in expected for name in files):
102
+ raise ValueError("VOICED is missing official signal/header checksums")
103
+
104
+ def fetch(name):
105
+ download(VOICED_URL + name, output / name, expected[name])
106
+
107
+ print("Downloading 208 VOICED signals and headers (four connections)...", flush=True)
108
+ with ThreadPoolExecutor(max_workers=4) as pool:
109
+ for index, _ in enumerate(pool.map(fetch, files), 1):
110
+ if index % 50 == 0:
111
+ print(f"VOICED: {index}/{len(files)} files", flush=True)
112
+ return {
113
+ "dataset": "voiced",
114
+ "url": VOICED_URL,
115
+ "snapshot": "1.0.0",
116
+ "license": "Open Data Commons Attribution License v1.0",
117
+ "doi": "10.13026/C25Q2N",
118
+ "files": file_manifest(output, root),
119
+ }
120
+
121
+
122
+ def main():
123
+ parser = argparse.ArgumentParser()
124
+ parser.add_argument("--data-dir", type=Path, default=Path("benchmarks/data"))
125
+ args = parser.parse_args()
126
+ args.data_dir.mkdir(parents=True, exist_ok=True)
127
+ manifest = [fetch_fsdd(args.data_dir), fetch_voiced(args.data_dir)]
128
+ (args.data_dir / "provenance.json").write_text(json.dumps(manifest, indent=2) + "\n")
129
+ print("All benchmark downloads complete; official VOICED checksums verified", flush=True)
130
+
131
+
132
+ if __name__ == "__main__":
133
+ main()