File size: 7,698 Bytes
642c07f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
---
license: apache-2.0
library_name: numpy
tags:
  - audio
  - symbolic
  - event-detection
  - onset-detection
  - signal-processing
  - heuristic
  - cpu
pipeline_tag: audio-classification
language:
  - en
---

# hv-note

**Symbolic audio event extraction.** No lexicon. No translation layer.

Given a WAV file, output a *score*: a sequence of `(onset_frame, symbol_id)`
pairs, where each symbol describes a discrete audio event by
`(pitch_band, duration_class, amplitude_class)`.

## The claim in one sentence

`hv-tempo` predicts *where* the reader slows down. `hv-core` finds the
cheapest edit that breaks an argument. `hv-fold` counts the passes.
`hv-note` extracts the score β€” the discrete events in a waveform, and
nothing else.

## What it produces

For a single WAV file:

- **score** β€” list of `(onset_frame, symbol_id)` pairs
- **events** β€” for each event: start/end/peak time, pitch Hz, band,
  duration class, amplitude class, symbol id
- **histogram** β€” symbol counts
- **entropy_bits** β€” Shannon entropy of the symbol distribution

For a directory:

- **corpus report** β€” file count, event count, unique symbols, entropy
- **per-group summary** β€” group key extracted from filename
- **file_scores** β€” the symbol sequence for every file

## Install

```bash
pip install numpy

Actually β€” no dependencies. Pure stdlib.

## Usage

### Single file

```bash
python hv_note.py --file ./esc50/audio/1-100032-A-0.wav
python hv_note.py --file ./fsdd/recordings/0_jackson_0.wav

### Directory

```bash
python hv_note.py --dir ./esc50/audio --limit 20
python hv_note.py --dir ./fsdd/recordings
```

### Python

```python
from hv_note import HVNote

m = HVNote()
report = m.score_file("sample.wav")

print(report.score)            # [(0, 44), (24, 44), (49, 44)]
print(report.entropy_bits)     # 0.0
print(report.histogram)        # {44: 3}

fp = m.fingerprint("sample.wav", k=8)
# (44, 44, 44)
```

### CLI flags

```bash
python hv_note.py                                  # synthesized demo
python hv_note.py --file x.wav                     # rendered report
python hv_note.py --file x.wav --json              # JSON
python hv_note.py --file x.wav --fingerprint       # just the fingerprint
python hv_note.py --dir ./audio --limit 100        # first 100 files
python hv_note.py --dir ./audio --json             # corpus JSON
python hv_note.py --save-to ./model                # save config
```

## The model

### Pipeline

```
WAV β†’ frames β†’ onsets β†’ events β†’ symbols β†’ score
```

1. **Frame** the signal. 20 ms window, 10 ms hop. Compute per-frame RMS.
2. **Detect onsets.** A frame is an onset when its RMS is above
   `onset_floor` (0.02) **and** either rises sharply from the previous
   frame (`Γ— onset_rise_mult`) or crosses up out of silence. The
   rising-edge rule prevents a decaying tail from triggering, and the
   silence-crossing rule catches slow attacks like chirps.
3. **Segment events.** The event's extent is the *connected region above
   `onset_floor`* starting at the onset. The peak is the argmax over that
   region only β€” never over a fixed search horizon, so a later, louder
   event cannot hijack the current one.
4. **Describe each event.**
   - **pitch band** β€” argmax of Goertzel power over 16 log-spaced bands
     (80 Hz – 6 kHz), on a 30 ms window centered on the peak frame.
   - **duration class** β€” `{0: <3 frames, 1: 3–7, 2: 8–19, 3: β‰₯20}`.
   - **amplitude class** β€” `{0: <0.3, 1: 0.3–0.7, 2: β‰₯0.7}` relative
     to the recording's own peak RMS.
5. **Encode.** `symbol = band Β· 12 + dur Β· 3 + amp`. 16 Γ— 4 Γ— 3 = **192
   symbols**.

### The alphabet

Fixed, unnamed, universal. Symbol `44` means *band 3, duration class 2,
amplitude class 2* β€” nothing more. Two recordings producing the same
symbol sequence contain the same pattern. No training, no labels, no
translation.

## Benchmarks

### Synthesized samples

| signal | events | unique symbols | entropy |
|---|---:|---:|---:|
| 3Γ— 220 Hz beeps | 3 | 1 | 0.000 b |
| 3 mixed beeps (880/660/440 Hz) | 3 | 3 | 1.585 b |
| 200β†’2000 Hz chirp | 1 | 1 | 0.000 b |
| 4 noise bursts (claps) | 4 | 4 | 2.000 b |

### On real corpora

**FSDD** (spoken digits, 8 kHz, 3000 recordings). Spoken digits are
single long events. Most recordings reduce to 1–3 symbols. The
distinguishing signal is *pitch band* and *duration class* β€” "one" is
short and mid-band; "seven" is longer and higher.

**ESC-50** (environmental sound, 44.1 kHz, 2000 recordings). Clips
average 8–20 events. The symbol histogram is a signature of the sound
category. Confusion appears between categories sharing a dominant pitch
band (dog barks and bird calls both light up bands 4–6).

Run either with `python hv_note.py --dir <path>`. Watch `PER-GROUP
SUMMARY`. Similar `unique_sym` counts across two groups mean the two
categories are symbolically indistinguishable in this 192-symbol
alphabet.

## When to use it

- **Matching without labels.** Compare two recordings by edit distance
  between their scores. No classifier needed.
- **Repetition detection.** Find repeated subsequences within a score.
- **Anomaly detection.** Train an n-gram model on a corpus of scores;
  flag low-likelihood scores.
- **Clustering.** Group recordings by symbol histogram.
- **Structural query.** "Find all files whose score contains
  `(5,2,1) β†’ (5,2,1) β†’ (3,1,0)`."

## When *not* to use it

- **For speech recognition.** No phonemes, no words, no letters.
- **For polyphonic music.** The model picks a single dominant band per
  event. Chords collapse.
- **For label prediction.** The output is symbolic, not semantic. If
  you want "this is a dog bark," you need a labeled reference set.
- **For cross-instrument matching.** Piano C4 and violin C4 have
  different timbres, hence different pitch-band energies, hence
  different symbols.
- **For continuous sound without onsets.** Sustained drones, tape hiss,
  and slow ambient textures produce zero events.

## Honest limitations

- **Pitch via Goertzel on 30 ms windows is coarse.** Frequency
  resolution at 80 Hz over 30 ms is ~33 Hz; at 6 kHz it's also ~33 Hz
  but the log bands are wider, so the top bands are tolerant. Fine for
  symbolic bucketing, hopeless for tuning.
- **No polyphony.** Single argmax per event.
- **Unpitched events get random bands.** White noise has a flat
  spectrum; the pitch-band argmax lands wherever the spectral noise
  happens to be loudest on that particular 30 ms window. A clap, a
  snare, and a coin drop all produce different symbols on different
  takes. If you need to match unpitched sounds, average over multiple
  takes rather than matching single recordings.
- **Amplitude collapses to a single class on uniform-loudness files.**
  Amplitude is relative to the recording's own peak RMS, so a file
  whose events are all similar in level produces only `amp_class = 2`.
  The amplitude axis is useful *within* a file that has both loud and
  quiet events, and carries no information otherwise.
- **Onsets are energy-based.** Spectral flux would be sharper, but
  costs more. This is the cheap version.
- **The alphabet is fixed.** A user cannot add custom symbol axes
  without changing the code. That's the point β€” the alphabet is small
  and universal.
- **No calibration against annotated corpora.** The thresholds are
  hand-tuned and honest.

## Reference

Part of the reader-model series, which has grown beyond reading.

Companion to `hv-tempo` (pace variation), `hv-core` (argument
robustness), `hv-fold` (reading passes).

`hv-note` is the one that doesn't model a reader at all. It models the
signal as a symbolic structure and stops there.

## License

Apache-2.0