sample-indexer 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.
- sample_indexer-0.1.0/LICENSE +21 -0
- sample_indexer-0.1.0/PKG-INFO +233 -0
- sample_indexer-0.1.0/README.md +214 -0
- sample_indexer-0.1.0/pyproject.toml +33 -0
- sample_indexer-0.1.0/sample_indexer/__init__.py +6 -0
- sample_indexer-0.1.0/sample_indexer/__main__.py +6 -0
- sample_indexer-0.1.0/sample_indexer/audio.py +423 -0
- sample_indexer-0.1.0/sample_indexer/audio_dsp.py +160 -0
- sample_indexer-0.1.0/sample_indexer/cli.py +343 -0
- sample_indexer-0.1.0/sample_indexer/config.py +43 -0
- sample_indexer-0.1.0/sample_indexer/db.py +132 -0
- sample_indexer-0.1.0/sample_indexer/hashing.py +18 -0
- sample_indexer-0.1.0/sample_indexer/labels.py +443 -0
- sample_indexer-0.1.0/sample_indexer/loops.py +574 -0
- sample_indexer-0.1.0/sample_indexer/loops_dsp.py +503 -0
- sample_indexer-0.1.0/sample_indexer/lowend.py +660 -0
- sample_indexer-0.1.0/sample_indexer/lowend_dsp.py +459 -0
- sample_indexer-0.1.0/sample_indexer/metadata.py +663 -0
- sample_indexer-0.1.0/sample_indexer/midi.py +133 -0
- sample_indexer-0.1.0/sample_indexer/midi_parser.py +284 -0
- sample_indexer-0.1.0/sample_indexer/migrations.py +323 -0
- sample_indexer-0.1.0/sample_indexer/reports.py +411 -0
- sample_indexer-0.1.0/sample_indexer/scanner.py +440 -0
- sample_indexer-0.1.0/sample_indexer.egg-info/PKG-INFO +233 -0
- sample_indexer-0.1.0/sample_indexer.egg-info/SOURCES.txt +40 -0
- sample_indexer-0.1.0/sample_indexer.egg-info/dependency_links.txt +1 -0
- sample_indexer-0.1.0/sample_indexer.egg-info/entry_points.txt +2 -0
- sample_indexer-0.1.0/sample_indexer.egg-info/requires.txt +1 -0
- sample_indexer-0.1.0/sample_indexer.egg-info/top_level.txt +1 -0
- sample_indexer-0.1.0/setup.cfg +4 -0
- sample_indexer-0.1.0/tests/test_audio.py +275 -0
- sample_indexer-0.1.0/tests/test_audio_dsp.py +148 -0
- sample_indexer-0.1.0/tests/test_labels.py +203 -0
- sample_indexer-0.1.0/tests/test_loops.py +281 -0
- sample_indexer-0.1.0/tests/test_loops_dsp.py +261 -0
- sample_indexer-0.1.0/tests/test_lowend.py +452 -0
- sample_indexer-0.1.0/tests/test_lowend_dsp.py +165 -0
- sample_indexer-0.1.0/tests/test_metadata.py +639 -0
- sample_indexer-0.1.0/tests/test_midi.py +81 -0
- sample_indexer-0.1.0/tests/test_midi_parser.py +188 -0
- sample_indexer-0.1.0/tests/test_query_technical.py +146 -0
- sample_indexer-0.1.0/tests/test_scanner.py +393 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adrián Cancio and contributors
|
|
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.
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sample-indexer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SQLite index of a sample and MIDI library with incremental reconciliation
|
|
5
|
+
Author: Adrián Cancio
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: numpy
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# sample-indexer
|
|
21
|
+
|
|
22
|
+
A SQLite index and technical analysis engine for sample and MIDI libraries. Built on Python standard library, system `ffprobe`/`ffmpeg` binaries, and `numpy` for digital signal processing (DSP).
|
|
23
|
+
|
|
24
|
+
The sample library is strictly **read-only**: files are opened `O_RDONLY` and never modified, moved, or deleted.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- **Python**: `>= 3.12`
|
|
31
|
+
- **System tools**: `ffmpeg` and `ffprobe` must be available in `PATH`.
|
|
32
|
+
- Fedora: `sudo dnf install ffmpeg-free` (or RPM Fusion `ffmpeg`)
|
|
33
|
+
- Ubuntu / Debian: `sudo apt install ffmpeg`
|
|
34
|
+
- Arch Linux: `sudo pacman -S ffmpeg`
|
|
35
|
+
- **Python dependencies**: `numpy` (installed automatically via `pip`).
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
Install directly from the source repository:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or for development (editable mode):
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install -e .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Once installed, the CLI tool is available as `sample-indexer` (or via `python3 -m sample_indexer`).
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Initialize or migrate database
|
|
61
|
+
sample-indexer init
|
|
62
|
+
|
|
63
|
+
# Scan library: compute SHA-256 identity and track paths
|
|
64
|
+
sample-indexer scan --root /path/to/samples
|
|
65
|
+
|
|
66
|
+
# Probe audio containers with ffprobe and extract name/folder labels
|
|
67
|
+
sample-indexer metadata --workers 4
|
|
68
|
+
|
|
69
|
+
# Run full decode & low-end DSP (sub-bass, pitch f0, decay) on kick/bass candidates
|
|
70
|
+
sample-indexer lowend --workers 2
|
|
71
|
+
|
|
72
|
+
# Run full decode & tempo/meter/key DSP on loop candidates
|
|
73
|
+
sample-indexer loops --workers 2
|
|
74
|
+
|
|
75
|
+
# General streaming timbre, true peak, and integrated loudness (LUFS)
|
|
76
|
+
sample-indexer audio --workers 2
|
|
77
|
+
|
|
78
|
+
# Parse Standard MIDI Files (SMF/RMID) into musical note metrics
|
|
79
|
+
sample-indexer midi
|
|
80
|
+
|
|
81
|
+
# Check coverage, health, and library statistics
|
|
82
|
+
sample-indexer stats
|
|
83
|
+
sample-indexer coverage
|
|
84
|
+
sample-indexer midi-coverage
|
|
85
|
+
|
|
86
|
+
# Query samples by technical criteria
|
|
87
|
+
sample-indexer query --folder "Samples/Kicks" --ext wav --limit 20
|
|
88
|
+
|
|
89
|
+
# Create a safe, online SQLite backup
|
|
90
|
+
sample-indexer backup index.backup.sqlite
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Global option `--db PATH` (placed before the subcommand) or the environment variable `SAMPLE_INDEXER_DB` selects the database location (default: `data/index.sqlite`).
|
|
94
|
+
`SAMPLE_INDEXER_ROOT` or `--root PATH` configures the library root.
|
|
95
|
+
|
|
96
|
+
Exit codes:
|
|
97
|
+
- `0`: success
|
|
98
|
+
- `1`: differences detected / file or probe errors
|
|
99
|
+
- `2`: usage error or database error
|
|
100
|
+
- `130`: interrupted (SIGINT / Ctrl-C)
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Analysis pipeline
|
|
105
|
+
|
|
106
|
+
| Stage | Subcommand | Reads | Writes |
|
|
107
|
+
|---|---|---|---|
|
|
108
|
+
| **Identity** | `scan` | File content (SHA-256), `stat` | `resource` (sha256, size, ext), `path` (rel_path, mtime) |
|
|
109
|
+
| **Probe** | `metadata` | Header via single `ffprobe` per audio file | duration, sample rate, channels, bit depth, codec |
|
|
110
|
+
| **Labels** | `metadata` | File path and name only | kind, category, bpm_name, key_name, label evidence |
|
|
111
|
+
| **Low-End** | `lowend` | Full decode via `ffmpeg -xerror` | `valid`/`valid_error`, peak, RMS, crest factor, bands, sub peak Hz, f0 pitch Hz, decay ms, stereo correlation |
|
|
112
|
+
| **Loops** | `loops` | Full decode via `ffmpeg -xerror` | `bpm_est`, `beats`, `bars_est`, `key_est`, confidence metrics, chroma analysis |
|
|
113
|
+
| **Audio** | `audio` | Complete streaming PCM | Integrated LUFS, true peak dBTP, spectral centroid, rolloff, flatness, structured descriptors |
|
|
114
|
+
| **MIDI** | `midi` | SMF / RMID events (stdlib parser) | Note counts, pitch bounds, note density, tempo, bars |
|
|
115
|
+
|
|
116
|
+
### Identity and reconciliation (`scan`)
|
|
117
|
+
- Identity is determined by the content SHA-256 (`resource`), while file paths are recorded as attributes (`path`).
|
|
118
|
+
- Paths with unchanged `(rel_path, size, mtime_ns)` are not re-hashed.
|
|
119
|
+
- Relocated or renamed files are recognized by matching their SHA-256 with vanished records.
|
|
120
|
+
- Deleted or missing files are marked with `missing=1`; database records are never hard-deleted.
|
|
121
|
+
|
|
122
|
+
### Container probing and heuristics (`metadata`)
|
|
123
|
+
- Fast, non-destructive header inspection using `ffprobe` captures duration, sample rate, bit depth, and codec.
|
|
124
|
+
- Heuristic classification extracts provisional `kind` (`one_shot`, `loop`, `midi`, `unknown`), `category`, and filename BPM/key hints.
|
|
125
|
+
- Audio validity is left `NULL` until a stage actually performs a conclusive full decode.
|
|
126
|
+
|
|
127
|
+
### Low-End & Kick analysis (`lowend`)
|
|
128
|
+
- Focuses on kick drums, 808s, and bass candidates.
|
|
129
|
+
- Measures fundamental pitch ($f_0$) via time-domain autocorrelation, sub-bass peak frequency via high-resolution zero-padded FFT, decay time ($T_{60}$ equivalent), and low-frequency stereo correlation ($< 150$ Hz).
|
|
130
|
+
|
|
131
|
+
### Loop & Tempo analysis (`loops`)
|
|
132
|
+
- Targets loop candidates (folder conventions or loop labels).
|
|
133
|
+
- Measures tempo via onset spectral flux autocorrelation across candidate BPM ranges.
|
|
134
|
+
- Estimates musical key using 12-bin chroma profiling correlated against Krumhansl-Schmuckler key profiles.
|
|
135
|
+
|
|
136
|
+
### Comprehensive timbre & loudness (`audio`)
|
|
137
|
+
- Streams full audio signals through chunked spectral analysis.
|
|
138
|
+
- Computes integrated loudness (LUFS) and true peak (dBTP) using EBU R128 metering standards.
|
|
139
|
+
- Evaluates spectral centroid, 85% spectral rolloff, and spectral flatness.
|
|
140
|
+
|
|
141
|
+
### MIDI parsing (`midi`)
|
|
142
|
+
- Pure Python Standard MIDI File (SMF format 0/1/2) and RMID parser without external dependencies.
|
|
143
|
+
- Calculates total notes, pitch range (`pitch_min`, `pitch_max`), note density (notes/beat), bar counts, and tempo events.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Querying the index
|
|
148
|
+
|
|
149
|
+
### CLI examples
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# 1. Kick one-shots with sub frequency peak in 45-65 Hz range
|
|
153
|
+
sample-indexer query --folder "Samples/Drums - One Shots/Kicks" --sub-peak-min 45 --sub-peak-max 65 --limit 10
|
|
154
|
+
|
|
155
|
+
# 2. Fast loops (>= 140 BPM) with measured tempo confidence >= 0.20
|
|
156
|
+
sample-indexer query --bpm-min 140 --bpm-confidence-min 0.20 --limit 10
|
|
157
|
+
|
|
158
|
+
# 3. Stereo loops with high mono compatibility in the low end (stereo_corr_low >= 0.85)
|
|
159
|
+
sample-indexer query --stereo-corr-low-min 0.85 --limit 10
|
|
160
|
+
|
|
161
|
+
# 4. Short percussion/hi-hat one-shots (duration <= 0.15s, sr=44100)
|
|
162
|
+
sample-indexer query --duration-max 0.15 --sr 44100 --limit 10
|
|
163
|
+
|
|
164
|
+
# 5. Long atmospheric textures (duration >= 10.0s)
|
|
165
|
+
sample-indexer query --duration-min 10.0 --limit 10
|
|
166
|
+
|
|
167
|
+
# 6. Dense MIDI files (density >= 6.0 notes/beat)
|
|
168
|
+
sample-indexer query --ext mid --midi-density-min 6.0 --limit 10
|
|
169
|
+
|
|
170
|
+
# 7. Long MIDI arrangements with 100+ note events
|
|
171
|
+
sample-indexer query --ext mid --midi-notes-min 100 --limit 10
|
|
172
|
+
|
|
173
|
+
# 8. Samples matching musical key 'F min' (from filename hint or measured chroma)
|
|
174
|
+
sample-indexer query --key "F min" --limit 10
|
|
175
|
+
|
|
176
|
+
# 9. Basslines with fundamental pitch f0 between 40 and 90 Hz
|
|
177
|
+
sample-indexer query --f0-min 40 --f0-max 90 --limit 10
|
|
178
|
+
|
|
179
|
+
# 10. Locate known corrupted or unparseable files
|
|
180
|
+
sample-indexer query --only-errors
|
|
181
|
+
|
|
182
|
+
# 11. Output structured JSON including technical audio descriptors
|
|
183
|
+
sample-indexer query --folder "Samples/Loops/Bass Loops" --bpm-min 130 --bpm-max 140 --json --limit 5
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Python API
|
|
187
|
+
|
|
188
|
+
`sample-indexer` can be imported directly into Python scripts:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
import sqlite3
|
|
192
|
+
from sample_indexer import query
|
|
193
|
+
|
|
194
|
+
conn = sqlite3.connect("data/index.sqlite")
|
|
195
|
+
conn.row_factory = sqlite3.Row
|
|
196
|
+
|
|
197
|
+
# Query kicks tuned between 48 Hz and 55 Hz
|
|
198
|
+
rows = query(
|
|
199
|
+
conn,
|
|
200
|
+
folder="Samples/Drums - One Shots/Kicks",
|
|
201
|
+
sub_peak_min=48.0,
|
|
202
|
+
sub_peak_max=55.0,
|
|
203
|
+
limit=25,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
for row in rows:
|
|
207
|
+
print(f"{row['rel_path']} - {row['sub_peak_hz']:.1f} Hz")
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Safety and execution semantics
|
|
213
|
+
|
|
214
|
+
- **Resumable execution**: Each stage tracks progress in `resource_stage`. Interrupted runs (Ctrl-C / `--max-seconds`) commit finished checkpoints and exit cleanly.
|
|
215
|
+
- **Error handling**: Corrupted files or failed decodes record the failure status in the index and count as processed to prevent retry storms. Use `--retry-errors` to retry them or `--force` to reprocess.
|
|
216
|
+
- **Database backups**: Schema migrations automatically create timestamped SQLite backups before applying changes. Manual backups can be created at any time using `sample-indexer backup <dest>`.
|
|
217
|
+
- **Subprocess security**: All external calls to `ffmpeg` and `ffprobe` use explicit argument lists (`file:<path>`) without shell interpolation.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Running tests
|
|
222
|
+
|
|
223
|
+
The test suite runs against temporary file trees and scratch SQLite databases:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
python3 -m unittest discover
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# sample-indexer
|
|
2
|
+
|
|
3
|
+
A SQLite index and technical analysis engine for sample and MIDI libraries. Built on Python standard library, system `ffprobe`/`ffmpeg` binaries, and `numpy` for digital signal processing (DSP).
|
|
4
|
+
|
|
5
|
+
The sample library is strictly **read-only**: files are opened `O_RDONLY` and never modified, moved, or deleted.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- **Python**: `>= 3.12`
|
|
12
|
+
- **System tools**: `ffmpeg` and `ffprobe` must be available in `PATH`.
|
|
13
|
+
- Fedora: `sudo dnf install ffmpeg-free` (or RPM Fusion `ffmpeg`)
|
|
14
|
+
- Ubuntu / Debian: `sudo apt install ffmpeg`
|
|
15
|
+
- Arch Linux: `sudo pacman -S ffmpeg`
|
|
16
|
+
- **Python dependencies**: `numpy` (installed automatically via `pip`).
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Install directly from the source repository:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or for development (editable mode):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install -e .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Once installed, the CLI tool is available as `sample-indexer` (or via `python3 -m sample_indexer`).
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Initialize or migrate database
|
|
42
|
+
sample-indexer init
|
|
43
|
+
|
|
44
|
+
# Scan library: compute SHA-256 identity and track paths
|
|
45
|
+
sample-indexer scan --root /path/to/samples
|
|
46
|
+
|
|
47
|
+
# Probe audio containers with ffprobe and extract name/folder labels
|
|
48
|
+
sample-indexer metadata --workers 4
|
|
49
|
+
|
|
50
|
+
# Run full decode & low-end DSP (sub-bass, pitch f0, decay) on kick/bass candidates
|
|
51
|
+
sample-indexer lowend --workers 2
|
|
52
|
+
|
|
53
|
+
# Run full decode & tempo/meter/key DSP on loop candidates
|
|
54
|
+
sample-indexer loops --workers 2
|
|
55
|
+
|
|
56
|
+
# General streaming timbre, true peak, and integrated loudness (LUFS)
|
|
57
|
+
sample-indexer audio --workers 2
|
|
58
|
+
|
|
59
|
+
# Parse Standard MIDI Files (SMF/RMID) into musical note metrics
|
|
60
|
+
sample-indexer midi
|
|
61
|
+
|
|
62
|
+
# Check coverage, health, and library statistics
|
|
63
|
+
sample-indexer stats
|
|
64
|
+
sample-indexer coverage
|
|
65
|
+
sample-indexer midi-coverage
|
|
66
|
+
|
|
67
|
+
# Query samples by technical criteria
|
|
68
|
+
sample-indexer query --folder "Samples/Kicks" --ext wav --limit 20
|
|
69
|
+
|
|
70
|
+
# Create a safe, online SQLite backup
|
|
71
|
+
sample-indexer backup index.backup.sqlite
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Global option `--db PATH` (placed before the subcommand) or the environment variable `SAMPLE_INDEXER_DB` selects the database location (default: `data/index.sqlite`).
|
|
75
|
+
`SAMPLE_INDEXER_ROOT` or `--root PATH` configures the library root.
|
|
76
|
+
|
|
77
|
+
Exit codes:
|
|
78
|
+
- `0`: success
|
|
79
|
+
- `1`: differences detected / file or probe errors
|
|
80
|
+
- `2`: usage error or database error
|
|
81
|
+
- `130`: interrupted (SIGINT / Ctrl-C)
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Analysis pipeline
|
|
86
|
+
|
|
87
|
+
| Stage | Subcommand | Reads | Writes |
|
|
88
|
+
|---|---|---|---|
|
|
89
|
+
| **Identity** | `scan` | File content (SHA-256), `stat` | `resource` (sha256, size, ext), `path` (rel_path, mtime) |
|
|
90
|
+
| **Probe** | `metadata` | Header via single `ffprobe` per audio file | duration, sample rate, channels, bit depth, codec |
|
|
91
|
+
| **Labels** | `metadata` | File path and name only | kind, category, bpm_name, key_name, label evidence |
|
|
92
|
+
| **Low-End** | `lowend` | Full decode via `ffmpeg -xerror` | `valid`/`valid_error`, peak, RMS, crest factor, bands, sub peak Hz, f0 pitch Hz, decay ms, stereo correlation |
|
|
93
|
+
| **Loops** | `loops` | Full decode via `ffmpeg -xerror` | `bpm_est`, `beats`, `bars_est`, `key_est`, confidence metrics, chroma analysis |
|
|
94
|
+
| **Audio** | `audio` | Complete streaming PCM | Integrated LUFS, true peak dBTP, spectral centroid, rolloff, flatness, structured descriptors |
|
|
95
|
+
| **MIDI** | `midi` | SMF / RMID events (stdlib parser) | Note counts, pitch bounds, note density, tempo, bars |
|
|
96
|
+
|
|
97
|
+
### Identity and reconciliation (`scan`)
|
|
98
|
+
- Identity is determined by the content SHA-256 (`resource`), while file paths are recorded as attributes (`path`).
|
|
99
|
+
- Paths with unchanged `(rel_path, size, mtime_ns)` are not re-hashed.
|
|
100
|
+
- Relocated or renamed files are recognized by matching their SHA-256 with vanished records.
|
|
101
|
+
- Deleted or missing files are marked with `missing=1`; database records are never hard-deleted.
|
|
102
|
+
|
|
103
|
+
### Container probing and heuristics (`metadata`)
|
|
104
|
+
- Fast, non-destructive header inspection using `ffprobe` captures duration, sample rate, bit depth, and codec.
|
|
105
|
+
- Heuristic classification extracts provisional `kind` (`one_shot`, `loop`, `midi`, `unknown`), `category`, and filename BPM/key hints.
|
|
106
|
+
- Audio validity is left `NULL` until a stage actually performs a conclusive full decode.
|
|
107
|
+
|
|
108
|
+
### Low-End & Kick analysis (`lowend`)
|
|
109
|
+
- Focuses on kick drums, 808s, and bass candidates.
|
|
110
|
+
- Measures fundamental pitch ($f_0$) via time-domain autocorrelation, sub-bass peak frequency via high-resolution zero-padded FFT, decay time ($T_{60}$ equivalent), and low-frequency stereo correlation ($< 150$ Hz).
|
|
111
|
+
|
|
112
|
+
### Loop & Tempo analysis (`loops`)
|
|
113
|
+
- Targets loop candidates (folder conventions or loop labels).
|
|
114
|
+
- Measures tempo via onset spectral flux autocorrelation across candidate BPM ranges.
|
|
115
|
+
- Estimates musical key using 12-bin chroma profiling correlated against Krumhansl-Schmuckler key profiles.
|
|
116
|
+
|
|
117
|
+
### Comprehensive timbre & loudness (`audio`)
|
|
118
|
+
- Streams full audio signals through chunked spectral analysis.
|
|
119
|
+
- Computes integrated loudness (LUFS) and true peak (dBTP) using EBU R128 metering standards.
|
|
120
|
+
- Evaluates spectral centroid, 85% spectral rolloff, and spectral flatness.
|
|
121
|
+
|
|
122
|
+
### MIDI parsing (`midi`)
|
|
123
|
+
- Pure Python Standard MIDI File (SMF format 0/1/2) and RMID parser without external dependencies.
|
|
124
|
+
- Calculates total notes, pitch range (`pitch_min`, `pitch_max`), note density (notes/beat), bar counts, and tempo events.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Querying the index
|
|
129
|
+
|
|
130
|
+
### CLI examples
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# 1. Kick one-shots with sub frequency peak in 45-65 Hz range
|
|
134
|
+
sample-indexer query --folder "Samples/Drums - One Shots/Kicks" --sub-peak-min 45 --sub-peak-max 65 --limit 10
|
|
135
|
+
|
|
136
|
+
# 2. Fast loops (>= 140 BPM) with measured tempo confidence >= 0.20
|
|
137
|
+
sample-indexer query --bpm-min 140 --bpm-confidence-min 0.20 --limit 10
|
|
138
|
+
|
|
139
|
+
# 3. Stereo loops with high mono compatibility in the low end (stereo_corr_low >= 0.85)
|
|
140
|
+
sample-indexer query --stereo-corr-low-min 0.85 --limit 10
|
|
141
|
+
|
|
142
|
+
# 4. Short percussion/hi-hat one-shots (duration <= 0.15s, sr=44100)
|
|
143
|
+
sample-indexer query --duration-max 0.15 --sr 44100 --limit 10
|
|
144
|
+
|
|
145
|
+
# 5. Long atmospheric textures (duration >= 10.0s)
|
|
146
|
+
sample-indexer query --duration-min 10.0 --limit 10
|
|
147
|
+
|
|
148
|
+
# 6. Dense MIDI files (density >= 6.0 notes/beat)
|
|
149
|
+
sample-indexer query --ext mid --midi-density-min 6.0 --limit 10
|
|
150
|
+
|
|
151
|
+
# 7. Long MIDI arrangements with 100+ note events
|
|
152
|
+
sample-indexer query --ext mid --midi-notes-min 100 --limit 10
|
|
153
|
+
|
|
154
|
+
# 8. Samples matching musical key 'F min' (from filename hint or measured chroma)
|
|
155
|
+
sample-indexer query --key "F min" --limit 10
|
|
156
|
+
|
|
157
|
+
# 9. Basslines with fundamental pitch f0 between 40 and 90 Hz
|
|
158
|
+
sample-indexer query --f0-min 40 --f0-max 90 --limit 10
|
|
159
|
+
|
|
160
|
+
# 10. Locate known corrupted or unparseable files
|
|
161
|
+
sample-indexer query --only-errors
|
|
162
|
+
|
|
163
|
+
# 11. Output structured JSON including technical audio descriptors
|
|
164
|
+
sample-indexer query --folder "Samples/Loops/Bass Loops" --bpm-min 130 --bpm-max 140 --json --limit 5
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Python API
|
|
168
|
+
|
|
169
|
+
`sample-indexer` can be imported directly into Python scripts:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
import sqlite3
|
|
173
|
+
from sample_indexer import query
|
|
174
|
+
|
|
175
|
+
conn = sqlite3.connect("data/index.sqlite")
|
|
176
|
+
conn.row_factory = sqlite3.Row
|
|
177
|
+
|
|
178
|
+
# Query kicks tuned between 48 Hz and 55 Hz
|
|
179
|
+
rows = query(
|
|
180
|
+
conn,
|
|
181
|
+
folder="Samples/Drums - One Shots/Kicks",
|
|
182
|
+
sub_peak_min=48.0,
|
|
183
|
+
sub_peak_max=55.0,
|
|
184
|
+
limit=25,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
for row in rows:
|
|
188
|
+
print(f"{row['rel_path']} - {row['sub_peak_hz']:.1f} Hz")
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Safety and execution semantics
|
|
194
|
+
|
|
195
|
+
- **Resumable execution**: Each stage tracks progress in `resource_stage`. Interrupted runs (Ctrl-C / `--max-seconds`) commit finished checkpoints and exit cleanly.
|
|
196
|
+
- **Error handling**: Corrupted files or failed decodes record the failure status in the index and count as processed to prevent retry storms. Use `--retry-errors` to retry them or `--force` to reprocess.
|
|
197
|
+
- **Database backups**: Schema migrations automatically create timestamped SQLite backups before applying changes. Manual backups can be created at any time using `sample-indexer backup <dest>`.
|
|
198
|
+
- **Subprocess security**: All external calls to `ffmpeg` and `ffprobe` use explicit argument lists (`file:<path>`) without shell interpolation.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Running tests
|
|
203
|
+
|
|
204
|
+
The test suite runs against temporary file trees and scratch SQLite databases:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
python3 -m unittest discover
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sample-indexer"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "SQLite index of a sample and MIDI library with incremental reconciliation"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"numpy",
|
|
14
|
+
]
|
|
15
|
+
authors = [
|
|
16
|
+
{ name = "Adrián Cancio" },
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"Intended Audience :: End Users/Desktop",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Topic :: Multimedia :: Sound/Audio :: Analysis",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
sample-indexer = "sample_indexer.cli:main"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["."]
|
|
33
|
+
include = ["sample_indexer*"]
|