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.
- phonipy-0.1.0/.github/workflows/publish.yml +30 -0
- phonipy-0.1.0/.github/workflows/test.yml +28 -0
- phonipy-0.1.0/.gitignore +14 -0
- phonipy-0.1.0/LICENSE +21 -0
- phonipy-0.1.0/PKG-INFO +214 -0
- phonipy-0.1.0/README.md +175 -0
- phonipy-0.1.0/benchmarks/README.md +42 -0
- phonipy-0.1.0/benchmarks/download.py +133 -0
- phonipy-0.1.0/benchmarks/run.py +160 -0
- phonipy-0.1.0/docs/benchmark-results.md +41 -0
- phonipy-0.1.0/docs/benchmark-summary.json +66 -0
- phonipy-0.1.0/docs/methodology.md +71 -0
- phonipy-0.1.0/docs/publishing.md +25 -0
- phonipy-0.1.0/examples/config.json +18 -0
- phonipy-0.1.0/examples/dashboard.png +0 -0
- phonipy-0.1.0/phonipy/__init__.py +26 -0
- phonipy-0.1.0/phonipy/__main__.py +3 -0
- phonipy-0.1.0/phonipy/analysis.py +200 -0
- phonipy-0.1.0/phonipy/app.py +92 -0
- phonipy-0.1.0/phonipy/audio.py +106 -0
- phonipy-0.1.0/phonipy/batch.py +158 -0
- phonipy-0.1.0/phonipy/cli.py +78 -0
- phonipy-0.1.0/phonipy/config.py +80 -0
- phonipy-0.1.0/phonipy/metrics.py +56 -0
- phonipy-0.1.0/phonipy/models.py +72 -0
- phonipy-0.1.0/phonipy/plotting.py +50 -0
- phonipy-0.1.0/pyproject.toml +52 -0
- phonipy-0.1.0/tests/test_analysis.py +121 -0
- phonipy-0.1.0/tests/test_app.py +49 -0
- phonipy-0.1.0/tests/test_io.py +194 -0
- phonipy-0.1.0/tests/test_metrics.py +62 -0
|
@@ -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/*
|
phonipy-0.1.0/.gitignore
ADDED
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.
|
phonipy-0.1.0/README.md
ADDED
|
@@ -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()
|