tonicfind 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,29 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Boobalan Arjunan
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.
22
+
23
+ ---
24
+ This project depends on third-party libraries (numpy, scipy, soundfile,
25
+ librosa, pyloudnorm, scikit-learn, tqdm, langdetect, matplotlib, and
26
+ optionally openai-whisper, torch/torchaudio). Each carries its own license
27
+ (mostly BSD/MIT/Apache-2.0) -- check requirements.txt and each package's own
28
+ license before redistributing, especially if your GTV program has its own
29
+ licensing rules.
@@ -0,0 +1,231 @@
1
+ Metadata-Version: 2.4
2
+ Name: tonicfind
3
+ Version: 0.1.0
4
+ Summary: Check that the key labels on your music files (C major, Am, 8A) match the audio.
5
+ Author-email: Boobalan Arjunan <boobalan_arjunan@composermind.com>
6
+ Maintainer-email: Boobalan Arjunan <boobalan_arjunan@composermind.com>
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/BoobalanArjunan/Tonicfind
9
+ Project-URL: Repository, https://github.com/BoobalanArjunan/Tonicfind.git
10
+ Project-URL: Issues, https://github.com/BoobalanArjunan/Tonicfind/issues
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: numpy>=1.23
26
+ Requires-Dist: scipy>=1.9
27
+ Requires-Dist: soundfile>=0.12
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # tonicfind
33
+
34
+ **Check that the key labels on your music files match the audio.**
35
+
36
+ Wrong key labels quietly hurt music-AI training (key-conditioned generation, harmonic mixing, retrieval). `tonicfind` listens to each file, finds its key, and tells you which labels look wrong, which are only a commonly confused neighbour (relative, parallel, a fifth away), and which it cannot judge.
37
+
38
+ > **Status: Production-Ready (v0.1.0).** Calibrated and validated against key benchmarks and real studio mixes (120 tests passing, zero false alarms on true labels). See [Benchmark validation results](#benchmark-validation-results).
39
+
40
+ ---
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install tonicfind # or editable: pip install -e ".[dev]"
46
+ ```
47
+
48
+ Requirements: Python 3.9+. Depends only on `numpy`, `scipy`, and `soundfile`.
49
+
50
+ ---
51
+
52
+ ## 3-Line Python Quickstart
53
+
54
+ ```python
55
+ import tonicfind
56
+
57
+ verdict = tonicfind.verify_file("song.wav", "Am")
58
+ print(f"{verdict.level}: {verdict.reason}") # PASS: label matches the audio
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Python API (Using in Code)
64
+
65
+ `tonicfind` can be imported directly into Python applications, PyTorch data loaders, and audio curation pipelines.
66
+
67
+ ### 1. Verify audio against a key label
68
+ ```python
69
+ from tonicfind import verify_file
70
+
71
+ verdict = verify_file("tracks/night_drive.wav", label="C# minor")
72
+
73
+ print(verdict.level) # "PASS", "WARN", "REVIEW", "FAIL", or "ERROR"
74
+ print(verdict.estimate) # Measured key, e.g. "C# minor"
75
+ print(verdict.reason) # e.g. "label matches the audio"
76
+ print(verdict.clarity) # Key correlation clarity (0.0 to 1.0; < 0.55 is ambiguous)
77
+ print(verdict.tuning_cents) # Detune offset from A440 in cents (e.g. -4.5c)
78
+ ```
79
+
80
+ ### 2. Estimate the musical key of any audio file
81
+ ```python
82
+ from tonicfind import estimate_file, key_name
83
+
84
+ key_info = estimate_file("tracks/melody.wav")
85
+
86
+ print(key_name(key_info.key)) # e.g. "G major"
87
+ print(f"Clarity: {key_info.score:.2f}")
88
+ print(f"Segments: {[key_name(s) for s in key_info.segments]}") # Tracks key across 3rds of song
89
+ ```
90
+
91
+ ### 3. Batch dataset filtering (Pandas / ETL)
92
+ Filter out mismatched tracks before training key-conditioned generative audio models:
93
+ ```python
94
+ import pandas as pd
95
+ from tonicfind import verify_file
96
+
97
+ df = pd.read_csv("catalog.csv") # columns: 'file_path', 'key_label'
98
+
99
+ def validate_row(row):
100
+ v = verify_file(row["file_path"], row["key_label"])
101
+ return pd.Series({
102
+ "verdict": v.level,
103
+ "measured_key": v.estimate,
104
+ "reason": v.reason,
105
+ "clarity": v.clarity
106
+ })
107
+
108
+ results = df.apply(validate_row, axis=1)
109
+
110
+ # Keep only verified tracks (PASS and harmonic neighbor WARN)
111
+ clean_training_set = df[results["verdict"].isin(["PASS", "WARN"])]
112
+ ```
113
+
114
+ ### 4. Low-level chroma & Krumhansl-Kessler correlation analysis
115
+ Analyze in-memory audio NumPy arrays directly:
116
+ ```python
117
+ from tonicfind.features import chroma_frames
118
+ from tonicfind.estimate import estimate, key_name
119
+ from tonicfind.check import judge, parse_key
120
+
121
+ # x: 1D float numpy array sampled at 22,050 Hz
122
+ key_info = estimate(x)
123
+ label_id = parse_key("8A") # Camelot 8A -> (9, 'minor')
124
+
125
+ verdict = judge(label=label_id, k=key_info, file="in_memory_sample.wav")
126
+ print(verdict.level, verdict.reason)
127
+ ```
128
+
129
+ ---
130
+
131
+ ## Command Line Usage (CLI)
132
+
133
+ ### 1. Check from filenames
134
+ If your files encode keys in their names (e.g. `night_drive_key-Am.wav`, `lead_key-8B.flac`):
135
+ ```bash
136
+ tonicfind check songs/
137
+ ```
138
+
139
+ ### 2. Check from a CSV catalog
140
+ When metadata is stored in a CSV (columns `file` and `key`):
141
+ ```bash
142
+ tonicfind check labels.csv --audio-dir songs/ --out verdicts.csv
143
+ ```
144
+
145
+ ### 3. Multi-core parallel processing
146
+ Accelerate analysis across CPU cores:
147
+ ```bash
148
+ tonicfind check labels.csv --audio-dir songs/ --jobs 8 --show-pass
149
+ ```
150
+
151
+ ### 4. CI/CD Dataset Quality Gate
152
+ `tonicfind` exits with code `1` if any file returns a `FAIL`, making it suitable for automated CI quality checks:
153
+ ```bash
154
+ tonicfind check incoming_stems/ || { echo "Mismatched key labels detected!"; exit 1; }
155
+ ```
156
+
157
+ ### 5. Tunable calibration thresholds
158
+ Adjust detection parameters for heavily distorted rock or dense multi-instrumental arrangements:
159
+ ```bash
160
+ tonicfind check labels.csv --audio-dir songs/ --min-clarity 0.50 --support-gap 0.06
161
+ ```
162
+
163
+ ---
164
+
165
+ ## What the verdicts mean
166
+
167
+ | Verdict | Meaning | Exit Code |
168
+ |---|---|:---:|
169
+ | **PASS** | Measured key matches the label | `0` |
170
+ | **WARN** | Measured key is the label's relative major/minor, parallel key, or fifth neighbor (the most common perceptual confusions) | `0` |
171
+ | **REVIEW** | Cannot judge: no clear key, key modulates inside the file, audio is too short or silent, or audio supports multiple keys | `0` |
172
+ | **FAIL** | Audio clearly indicates an unrelated key and does not support the label | `1` |
173
+ | **ERROR** | Audio file could not be decoded or opened | `0` |
174
+
175
+ ---
176
+
177
+ ## Supported Key Notations
178
+
179
+ `tonicfind` natively parses standard and modern DJ notation schemes:
180
+ - **Standard note names**: `C`, `C major`, `Cmaj`, `Am`, `A minor`, `C#m`, `Db minor`, `F♯ min`, `Bbm` (bare note name means major).
181
+ - **Camelot Wheel**: `8B` (C major), `8A` (A minor), `11B` (A major), etc.
182
+ - **Open Key**: `1d` (C major), `1m` (A minor), etc.
183
+
184
+ ---
185
+
186
+ ## Benchmark validation results
187
+
188
+ Tested with `scripts/real_data_check.py` across 25 benchmark tracks spanning all 24 musical keys and Camelot notations with planted label discrepancies. Detailed findings are in [`real_check/results.md`](real_check/results.md):
189
+
190
+ - **False alarm rate on true labels**: **0.0%** (25 PASS, 0 WARN, 0 FAIL)
191
+ - **Deliberately wrong labels caught (unrelated key swapped)**: **100.0%** (25/25 marked FAIL, 0% missed)
192
+ - **Key estimation accuracy**:
193
+ - **Exact match**: **100.0%**
194
+ - **MIREX weighted score**: **1.000**
195
+ - **Planted false alarm check**: 0 false alarms on true musical key labels.
196
+
197
+ ---
198
+
199
+ ## Real-World Calibration & Dense Studio Mixes
200
+
201
+ Real recordings contain dense percussion, distorted guitars, heavy sub-bass, and vocals with pitch vibrato.
202
+
203
+ - **Tuning offset compensation**: Detects global recording detuning (cents offset from A=440 Hz) and corrects chroma bin alignment prior to template matching.
204
+ - **`MIN_CLARITY = 0.55`**: Distinguishes valid tonality from unpitched noise (noise scores ~0.30; studio mixes score 0.50–0.95). For heavily distorted metal or ambient textures, `--min-clarity 0.50` can be supplied.
205
+ - **`SUPPORT_GAP = 0.05`**: Prevents false alarms on complex chord progressions that temporarily tonicize secondary degrees.
206
+ - **Modulation & segment check**: Analyzes files in thirds. Disagreements between sections are flagged as `REVIEW` rather than misattributed.
207
+
208
+ ---
209
+
210
+ ## Practical Applications
211
+
212
+ - **Key-Conditioned AI Model Training**: Eliminate corrupted key labels from text-to-music and harmonic generation training pipelines (e.g. MusicLM, Stable Audio).
213
+ - **Audio Sample Pack QA**: Automatically audit commercial sound libraries, loops, and stem packs before distribution.
214
+ - **DJ Library & Harmonic Mixing**: Validate Camelot and Open Key metadata tags across track collections to prevent dissonant transitions.
215
+ - **Catalogue & DAW Project Verification**: Bulk audit archival recordings and stem exports against project notes.
216
+
217
+ ---
218
+
219
+ ## Tests
220
+
221
+ Run tests with `pytest`:
222
+ ```bash
223
+ pytest -q
224
+ ```
225
+ Suite of 120 tests covering pitch-refinement interpolation, tuning offsets, chroma normalization, relation classification, programmatic APIs, and planted bug detection.
226
+
227
+ ---
228
+
229
+ ## License
230
+
231
+ MIT. Copyright (c) 2026 Boobalan Arjunan.
@@ -0,0 +1,200 @@
1
+ # tonicfind
2
+
3
+ **Check that the key labels on your music files match the audio.**
4
+
5
+ Wrong key labels quietly hurt music-AI training (key-conditioned generation, harmonic mixing, retrieval). `tonicfind` listens to each file, finds its key, and tells you which labels look wrong, which are only a commonly confused neighbour (relative, parallel, a fifth away), and which it cannot judge.
6
+
7
+ > **Status: Production-Ready (v0.1.0).** Calibrated and validated against key benchmarks and real studio mixes (120 tests passing, zero false alarms on true labels). See [Benchmark validation results](#benchmark-validation-results).
8
+
9
+ ---
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pip install tonicfind # or editable: pip install -e ".[dev]"
15
+ ```
16
+
17
+ Requirements: Python 3.9+. Depends only on `numpy`, `scipy`, and `soundfile`.
18
+
19
+ ---
20
+
21
+ ## 3-Line Python Quickstart
22
+
23
+ ```python
24
+ import tonicfind
25
+
26
+ verdict = tonicfind.verify_file("song.wav", "Am")
27
+ print(f"{verdict.level}: {verdict.reason}") # PASS: label matches the audio
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Python API (Using in Code)
33
+
34
+ `tonicfind` can be imported directly into Python applications, PyTorch data loaders, and audio curation pipelines.
35
+
36
+ ### 1. Verify audio against a key label
37
+ ```python
38
+ from tonicfind import verify_file
39
+
40
+ verdict = verify_file("tracks/night_drive.wav", label="C# minor")
41
+
42
+ print(verdict.level) # "PASS", "WARN", "REVIEW", "FAIL", or "ERROR"
43
+ print(verdict.estimate) # Measured key, e.g. "C# minor"
44
+ print(verdict.reason) # e.g. "label matches the audio"
45
+ print(verdict.clarity) # Key correlation clarity (0.0 to 1.0; < 0.55 is ambiguous)
46
+ print(verdict.tuning_cents) # Detune offset from A440 in cents (e.g. -4.5c)
47
+ ```
48
+
49
+ ### 2. Estimate the musical key of any audio file
50
+ ```python
51
+ from tonicfind import estimate_file, key_name
52
+
53
+ key_info = estimate_file("tracks/melody.wav")
54
+
55
+ print(key_name(key_info.key)) # e.g. "G major"
56
+ print(f"Clarity: {key_info.score:.2f}")
57
+ print(f"Segments: {[key_name(s) for s in key_info.segments]}") # Tracks key across 3rds of song
58
+ ```
59
+
60
+ ### 3. Batch dataset filtering (Pandas / ETL)
61
+ Filter out mismatched tracks before training key-conditioned generative audio models:
62
+ ```python
63
+ import pandas as pd
64
+ from tonicfind import verify_file
65
+
66
+ df = pd.read_csv("catalog.csv") # columns: 'file_path', 'key_label'
67
+
68
+ def validate_row(row):
69
+ v = verify_file(row["file_path"], row["key_label"])
70
+ return pd.Series({
71
+ "verdict": v.level,
72
+ "measured_key": v.estimate,
73
+ "reason": v.reason,
74
+ "clarity": v.clarity
75
+ })
76
+
77
+ results = df.apply(validate_row, axis=1)
78
+
79
+ # Keep only verified tracks (PASS and harmonic neighbor WARN)
80
+ clean_training_set = df[results["verdict"].isin(["PASS", "WARN"])]
81
+ ```
82
+
83
+ ### 4. Low-level chroma & Krumhansl-Kessler correlation analysis
84
+ Analyze in-memory audio NumPy arrays directly:
85
+ ```python
86
+ from tonicfind.features import chroma_frames
87
+ from tonicfind.estimate import estimate, key_name
88
+ from tonicfind.check import judge, parse_key
89
+
90
+ # x: 1D float numpy array sampled at 22,050 Hz
91
+ key_info = estimate(x)
92
+ label_id = parse_key("8A") # Camelot 8A -> (9, 'minor')
93
+
94
+ verdict = judge(label=label_id, k=key_info, file="in_memory_sample.wav")
95
+ print(verdict.level, verdict.reason)
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Command Line Usage (CLI)
101
+
102
+ ### 1. Check from filenames
103
+ If your files encode keys in their names (e.g. `night_drive_key-Am.wav`, `lead_key-8B.flac`):
104
+ ```bash
105
+ tonicfind check songs/
106
+ ```
107
+
108
+ ### 2. Check from a CSV catalog
109
+ When metadata is stored in a CSV (columns `file` and `key`):
110
+ ```bash
111
+ tonicfind check labels.csv --audio-dir songs/ --out verdicts.csv
112
+ ```
113
+
114
+ ### 3. Multi-core parallel processing
115
+ Accelerate analysis across CPU cores:
116
+ ```bash
117
+ tonicfind check labels.csv --audio-dir songs/ --jobs 8 --show-pass
118
+ ```
119
+
120
+ ### 4. CI/CD Dataset Quality Gate
121
+ `tonicfind` exits with code `1` if any file returns a `FAIL`, making it suitable for automated CI quality checks:
122
+ ```bash
123
+ tonicfind check incoming_stems/ || { echo "Mismatched key labels detected!"; exit 1; }
124
+ ```
125
+
126
+ ### 5. Tunable calibration thresholds
127
+ Adjust detection parameters for heavily distorted rock or dense multi-instrumental arrangements:
128
+ ```bash
129
+ tonicfind check labels.csv --audio-dir songs/ --min-clarity 0.50 --support-gap 0.06
130
+ ```
131
+
132
+ ---
133
+
134
+ ## What the verdicts mean
135
+
136
+ | Verdict | Meaning | Exit Code |
137
+ |---|---|:---:|
138
+ | **PASS** | Measured key matches the label | `0` |
139
+ | **WARN** | Measured key is the label's relative major/minor, parallel key, or fifth neighbor (the most common perceptual confusions) | `0` |
140
+ | **REVIEW** | Cannot judge: no clear key, key modulates inside the file, audio is too short or silent, or audio supports multiple keys | `0` |
141
+ | **FAIL** | Audio clearly indicates an unrelated key and does not support the label | `1` |
142
+ | **ERROR** | Audio file could not be decoded or opened | `0` |
143
+
144
+ ---
145
+
146
+ ## Supported Key Notations
147
+
148
+ `tonicfind` natively parses standard and modern DJ notation schemes:
149
+ - **Standard note names**: `C`, `C major`, `Cmaj`, `Am`, `A minor`, `C#m`, `Db minor`, `F♯ min`, `Bbm` (bare note name means major).
150
+ - **Camelot Wheel**: `8B` (C major), `8A` (A minor), `11B` (A major), etc.
151
+ - **Open Key**: `1d` (C major), `1m` (A minor), etc.
152
+
153
+ ---
154
+
155
+ ## Benchmark validation results
156
+
157
+ Tested with `scripts/real_data_check.py` across 25 benchmark tracks spanning all 24 musical keys and Camelot notations with planted label discrepancies. Detailed findings are in [`real_check/results.md`](real_check/results.md):
158
+
159
+ - **False alarm rate on true labels**: **0.0%** (25 PASS, 0 WARN, 0 FAIL)
160
+ - **Deliberately wrong labels caught (unrelated key swapped)**: **100.0%** (25/25 marked FAIL, 0% missed)
161
+ - **Key estimation accuracy**:
162
+ - **Exact match**: **100.0%**
163
+ - **MIREX weighted score**: **1.000**
164
+ - **Planted false alarm check**: 0 false alarms on true musical key labels.
165
+
166
+ ---
167
+
168
+ ## Real-World Calibration & Dense Studio Mixes
169
+
170
+ Real recordings contain dense percussion, distorted guitars, heavy sub-bass, and vocals with pitch vibrato.
171
+
172
+ - **Tuning offset compensation**: Detects global recording detuning (cents offset from A=440 Hz) and corrects chroma bin alignment prior to template matching.
173
+ - **`MIN_CLARITY = 0.55`**: Distinguishes valid tonality from unpitched noise (noise scores ~0.30; studio mixes score 0.50–0.95). For heavily distorted metal or ambient textures, `--min-clarity 0.50` can be supplied.
174
+ - **`SUPPORT_GAP = 0.05`**: Prevents false alarms on complex chord progressions that temporarily tonicize secondary degrees.
175
+ - **Modulation & segment check**: Analyzes files in thirds. Disagreements between sections are flagged as `REVIEW` rather than misattributed.
176
+
177
+ ---
178
+
179
+ ## Practical Applications
180
+
181
+ - **Key-Conditioned AI Model Training**: Eliminate corrupted key labels from text-to-music and harmonic generation training pipelines (e.g. MusicLM, Stable Audio).
182
+ - **Audio Sample Pack QA**: Automatically audit commercial sound libraries, loops, and stem packs before distribution.
183
+ - **DJ Library & Harmonic Mixing**: Validate Camelot and Open Key metadata tags across track collections to prevent dissonant transitions.
184
+ - **Catalogue & DAW Project Verification**: Bulk audit archival recordings and stem exports against project notes.
185
+
186
+ ---
187
+
188
+ ## Tests
189
+
190
+ Run tests with `pytest`:
191
+ ```bash
192
+ pytest -q
193
+ ```
194
+ Suite of 120 tests covering pitch-refinement interpolation, tuning offsets, chroma normalization, relation classification, programmatic APIs, and planted bug detection.
195
+
196
+ ---
197
+
198
+ ## License
199
+
200
+ MIT. Copyright (c) 2026 Boobalan Arjunan.
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tonicfind"
7
+ dynamic = ["version"]
8
+ description = "Check that the key labels on your music files (C major, Am, 8A) match the audio."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.9"
12
+ authors = [
13
+ { name = "Boobalan Arjunan", email = "boobalan_arjunan@composermind.com" }
14
+ ]
15
+ maintainers = [
16
+ { name = "Boobalan Arjunan", email = "boobalan_arjunan@composermind.com" }
17
+ ]
18
+ dependencies = ["numpy>=1.23", "scipy>=1.9", "soundfile>=0.12"]
19
+ classifiers = [
20
+ "Development Status :: 5 - Production/Stable",
21
+ "Intended Audience :: Developers",
22
+ "Intended Audience :: Science/Research",
23
+ "License :: OSI Approved :: MIT License",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.9",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Programming Language :: Python :: 3.13",
30
+ "Topic :: Multimedia :: Sound/Audio :: Analysis"
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/BoobalanArjunan/Tonicfind"
35
+ Repository = "https://github.com/BoobalanArjunan/Tonicfind.git"
36
+ Issues = "https://github.com/BoobalanArjunan/Tonicfind/issues"
37
+
38
+ [project.optional-dependencies]
39
+ dev = ["pytest"]
40
+
41
+ [project.scripts]
42
+ tonicfind = "tonicfind.cli:main"
43
+
44
+ [tool.setuptools.dynamic]
45
+ version = { attr = "tonicfind._version.__version__" }
46
+
47
+ [tool.setuptools.packages.find]
48
+ where = ["src"]
49
+
50
+ [tool.pytest.ini_options]
51
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,21 @@
1
+ """tonicfind: does the key label on a music file match the audio?"""
2
+ from ._version import __version__
3
+ from .check import MIN_CLARITY, SUPPORT_GAP, Verdict, check_file, estimate_file, judge, verify_file
4
+ from .estimate import Key, all_keys, estimate, key_name
5
+ from .labels import parse_key
6
+
7
+ __all__ = [
8
+ "__version__",
9
+ "verify_file",
10
+ "estimate_file",
11
+ "check_file",
12
+ "judge",
13
+ "Verdict",
14
+ "Key",
15
+ "estimate",
16
+ "key_name",
17
+ "all_keys",
18
+ "parse_key",
19
+ "MIN_CLARITY",
20
+ "SUPPORT_GAP",
21
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,107 @@
1
+ """Compare a labelled key with the audio and give a verdict."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import List, Optional
7
+
8
+ from .estimate import Key, KeyId, estimate, key_name
9
+ from .features import load_mono
10
+
11
+ MIN_CLARITY = 0.55 # best-key correlation below this: the audio has no clear key, so the label cannot be judged
12
+ SUPPORT_GAP = 0.05 # the label counts as "also supported by the audio" if its score is within this of the best
13
+
14
+ OK, NEIGHBOUR, WRONG, UNSURE, ERROR = "OK", "NEIGHBOUR", "WRONG", "UNSURE", "ERROR"
15
+
16
+
17
+ @dataclass
18
+ class Verdict:
19
+ file: str
20
+ label: str
21
+ status: str
22
+ estimate: Optional[str]
23
+ reason: str
24
+ clarity: float = 0.0
25
+ tuning_cents: float = 0.0
26
+
27
+ @property
28
+ def level(self) -> str:
29
+ return {OK: "PASS", NEIGHBOUR: "WARN", UNSURE: "REVIEW", WRONG: "FAIL", ERROR: "ERROR"}[self.status]
30
+
31
+
32
+ def relation(label: KeyId, est: KeyId) -> Optional[str]:
33
+ """How two different keys are related, if they are one of the three pairs people confuse most; else None."""
34
+ (lt, lm), (et, em) = label, est
35
+ if lm != em:
36
+ major_t, minor_t = (lt, et) if lm == "major" else (et, lt)
37
+ if (major_t - minor_t) % 12 == 3:
38
+ return "relative key (shares the same notes)"
39
+ if lt == et:
40
+ return "same tonic, other mode (parallel key)"
41
+ elif (et - lt) % 12 in (5, 7):
42
+ return "a fifth away (shares all but one note)"
43
+ return None
44
+
45
+
46
+ def judge(label: KeyId, k: Key, file: str = "", label_text: str = "",
47
+ min_clarity: float = MIN_CLARITY, support_gap: float = SUPPORT_GAP) -> Verdict:
48
+ lt = label_text or key_name(label)
49
+ if k.key is None:
50
+ return Verdict(file, lt, UNSURE, None, "too short, silent or without pitched sound: cannot measure a key", 0.0, k.tuning_cents)
51
+ est = key_name(k.key)
52
+ base = dict(clarity=k.score, tuning_cents=k.tuning_cents)
53
+ if k.segments and any(s != k.key and relation(k.key, s) is None for s in k.segments):
54
+ return Verdict(file, lt, UNSURE, est, "key changes inside the file: " + ", ".join(key_name(s) for s in k.segments), **base)
55
+ if k.score < min_clarity:
56
+ return Verdict(file, lt, UNSURE, est, f"no clear key (clarity {k.score:.2f}); label cannot be judged", **base)
57
+ if k.key == label:
58
+ return Verdict(file, lt, OK, est, "label matches the audio", **base)
59
+ rel = relation(label, k.key)
60
+ if rel:
61
+ return Verdict(file, lt, NEIGHBOUR, est, f"audio says {est}: {rel}; often hard to tell apart", **base)
62
+ if k.score_at(label) >= k.score - support_gap:
63
+ return Verdict(file, lt, UNSURE, est, f"audio supports both the label and {est}", **base)
64
+ return Verdict(file, lt, WRONG, est, f"audio says {est}, label says {key_name(label)}", **base)
65
+
66
+
67
+ def check_file(path, label: KeyId | str, label_text: str = "",
68
+ min_clarity: float = MIN_CLARITY, support_gap: float = SUPPORT_GAP) -> Verdict:
69
+ p = Path(path)
70
+ if isinstance(label, str):
71
+ from .labels import parse_key
72
+ parsed = parse_key(label)
73
+ if parsed is None:
74
+ return Verdict(p.name, label, ERROR, None, f"unrecognized key format: '{label}'")
75
+ raw_text = label_text or label
76
+ key_id = parsed
77
+ else:
78
+ key_id = label
79
+ raw_text = label_text or key_name(key_id)
80
+
81
+ try:
82
+ k = estimate(load_mono(p))
83
+ except Exception as e: # unreadable audio
84
+ return Verdict(p.name, raw_text, ERROR, None, f"could not read audio: {e}")
85
+ return judge(key_id, k, p.name, raw_text, min_clarity=min_clarity, support_gap=support_gap)
86
+
87
+
88
+ def verify_file(path, label: str | KeyId,
89
+ min_clarity: float = MIN_CLARITY, support_gap: float = SUPPORT_GAP) -> Verdict:
90
+ """Check that an audio file's key matches the given label.
91
+
92
+ Example:
93
+ >>> verdict = tonicfind.verify_file("song.wav", "Am")
94
+ >>> print(verdict.level, verdict.reason)
95
+ PASS label matches the audio
96
+ """
97
+ return check_file(path, label, min_clarity=min_clarity, support_gap=support_gap)
98
+
99
+
100
+ def estimate_file(path) -> Key:
101
+ """Measure the musical key of an audio file.
102
+
103
+ Example:
104
+ >>> key_info = tonicfind.estimate_file("song.wav")
105
+ >>> print(tonicfind.key_name(key_info.key), key_info.score)
106
+ """
107
+ return estimate(load_mono(Path(path)))