@vosjs/cli 0.73.0 → 0.75.0
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.
- package/dist/{assetManifest-3QYCLT2H.js → assetManifest-LOPDTP47.js} +2 -2
- package/dist/{chunk-Y5E672GT.js → chunk-M3QIN457.js} +21 -6
- package/dist/chunk-M3QIN457.js.map +1 -0
- package/dist/{chunk-EYF3WJIC.js → chunk-X6NQNBDF.js} +281 -196
- package/dist/chunk-X6NQNBDF.js.map +1 -0
- package/dist/cli.js +13 -14
- package/dist/cli.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{run-FK5HWX4X.js → run-XZ2UPHMO.js} +3 -3
- package/package.json +6 -6
- package/skills/VERSION +1 -1
- package/skills/vos-score/SKILL.md +225 -0
- package/skills/vos-score/references/arranging.md +92 -0
- package/skills/vos-score/references/new-jack-swing-124.json +38 -0
- package/skills/vos-score/references/spec.md +79 -0
- package/skills/vos-score/scripts/analyze.py +225 -0
- package/skills/vos-score/scripts/compose.py +634 -0
- package/skills/vos-score/scripts/master.py +74 -0
- package/dist/chunk-EYF3WJIC.js.map +0 -1
- package/dist/chunk-Y5E672GT.js.map +0 -1
- /package/dist/{assetManifest-3QYCLT2H.js.map → assetManifest-LOPDTP47.js.map} +0 -0
- /package/dist/{run-FK5HWX4X.js.map → run-XZ2UPHMO.js.map} +0 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vos-score
|
|
3
|
+
description: Compose an original score (background music, BGM, a soundtrack, a beat) for a vos program, written as code. The music is locked to the program's own beat grid (BPM, bars, beat 1 at t = 0) and arranged section by section to what is on screen (a build, stop-time hits on the frames where poses snap, fills under transitions, the biggest moment, an ending that lands), synthesized to a normalized MP3, verified by numbers because an agent cannot listen (a tempo and phase fit against the grid, per-section loudness against the intended shape), and put on the program's doc.json as its track. Use when asked for music, a soundtrack, a score or BGM for a vos, when a catalog track does not fit the picture or does not land on its beats, or when a film dances, cuts or animates on a beat grid.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Compose a score for a vos program
|
|
8
|
+
|
|
9
|
+
A catalog track (`GET https://vos.so/api/music`: CC0, a slug, a mood, a
|
|
10
|
+
length) carries no tempo and no downbeats, and you cannot listen to it. Under
|
|
11
|
+
an ambient picture that is fine. Under a picture that moves on a beat, it is
|
|
12
|
+
wrong in ways you will not hear: the kick lands between the poses, the drop
|
|
13
|
+
arrives two bars early, the loud part sits under the quiet scene.
|
|
14
|
+
|
|
15
|
+
So write the music instead. The score is a spec (tempo, chords, a riff, a
|
|
16
|
+
hook, sections by bar) that `scripts/compose.py` synthesizes into a WAV at
|
|
17
|
+
exactly the program's length, beat 1 of bar 0 at t = 0. Because the grid is
|
|
18
|
+
the program's own, every hit lands on the frame it was written for, and
|
|
19
|
+
because the arrangement is data, a fix is an edit and a re-render.
|
|
20
|
+
|
|
21
|
+
The bundled instruments are a funk and new jack swing palette: an 808-style
|
|
22
|
+
kick with a pitch sweep, a layered gated clap, swung 16th hats, a crash,
|
|
23
|
+
snare rolls, noise risers, a two-filter funk pluck bass with a sub under it,
|
|
24
|
+
an FM electric piano, detuned saw brass, orchestra hits, a square/saw lead
|
|
25
|
+
with vibrato, a pad, a room reverb and a bus saturation. Swing is a number
|
|
26
|
+
(0 is straight), kicks, claps and hats are per-section patterns, so the same
|
|
27
|
+
kit covers straight funk, four-on-the-floor dance and hip hop pocket. A
|
|
28
|
+
genre it cannot play (strings, guitar, a choir) is a new instrument written
|
|
29
|
+
beside the others in the same file, never a sample.
|
|
30
|
+
|
|
31
|
+
## Setup
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python3 -m venv .score-venv && .score-venv/bin/pip install numpy scipy
|
|
35
|
+
PY=.score-venv/bin/python
|
|
36
|
+
SKILL=<the directory holding this SKILL.md>
|
|
37
|
+
ffmpeg -version | head -1 # with libmp3lame
|
|
38
|
+
npm i -D @vosjs/cli # render the preview, push
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Python 3.9 or newer. Work in the program's directory; the scripts take
|
|
42
|
+
their inputs as paths.
|
|
43
|
+
|
|
44
|
+
## The procedure
|
|
45
|
+
|
|
46
|
+
### 1. Read the grid
|
|
47
|
+
|
|
48
|
+
The music's grid is the program's grid. Find it in the program:
|
|
49
|
+
|
|
50
|
+
- a tempo constant in a function string (`const P = 60 / 124`, `BAR = P * 4`)
|
|
51
|
+
or in `data` (`bpm`, `beat`);
|
|
52
|
+
- the sections: a plan table by bar (`[startBar, endBar, move, ...]`), the
|
|
53
|
+
scenes from `tl.addLabel('<scene>', t)`, or the times the picture changes
|
|
54
|
+
(render stills with `vos still config.json s.png --time <t>` and look);
|
|
55
|
+
- the length: `config.duration`, which is the score's length to the sample
|
|
56
|
+
(a program that ends mid-bar sets the spec's `length` to it).
|
|
57
|
+
|
|
58
|
+
Beat 1 of bar 0 is t = 0. If the program has scenes but no beat (a product
|
|
59
|
+
reveal timed by eye), pick the BPM whose bar lines fall nearest its scene
|
|
60
|
+
times, and, when you own the program, retime its labels onto those bars: a
|
|
61
|
+
picture and a score on one grid is the point. See
|
|
62
|
+
`references/arranging.md`.
|
|
63
|
+
|
|
64
|
+
### 2. Write the section map
|
|
65
|
+
|
|
66
|
+
Before any notes, write down what the music does under each section, in the
|
|
67
|
+
spec's `sections` with a `picture` line each (the composer ignores it; it is
|
|
68
|
+
for you and the next reader):
|
|
69
|
+
|
|
70
|
+
- where the energy rises, and where it drops back to breathe;
|
|
71
|
+
- where it STOPS: stop-time hits on the frames where poses snap or the cut
|
|
72
|
+
lands, with silence between;
|
|
73
|
+
- the fills under transitions (a spin, a wipe, a title): a roll and a riser
|
|
74
|
+
that carry into the next downbeat, never a hole;
|
|
75
|
+
- the biggest moment, ONE section, louder than everything else;
|
|
76
|
+
- the ending: a pickup, the last hit on the final pose, and the room ringing
|
|
77
|
+
out to the last frame.
|
|
78
|
+
|
|
79
|
+
`references/arranging.md` holds the craft and the measured level targets.
|
|
80
|
+
|
|
81
|
+
### 3. Pick a genre and a harmony
|
|
82
|
+
|
|
83
|
+
Fit the picture's era and mood: a 1990 dance film wants new jack swing (an
|
|
84
|
+
808, swung 16ths, brass stabs, a funk bass), a 1977 one disco (straight,
|
|
85
|
+
four on the floor, open hats on the off-beats). Pick a mode and two to four
|
|
86
|
+
chords that vamp (E dorian's Em9 to A13 does a whole groove), a darker pair
|
|
87
|
+
for the breakdown, and a lift for the chorus.
|
|
88
|
+
|
|
89
|
+
**Everything is original.** Write a new riff, a new hook, new changes. Never
|
|
90
|
+
a recognisable melody, riff, bassline or sample from a real record. A film
|
|
91
|
+
made after a famous record is the most tempting case: the score is in that
|
|
92
|
+
record's genre and era, and quotes nothing of it.
|
|
93
|
+
|
|
94
|
+
### 4. Compose
|
|
95
|
+
|
|
96
|
+
Copy `$SKILL/references/new-jack-swing-124.json` to `score.json` (a 124
|
|
97
|
+
BPM new jack swing dance film, 30 bars) and change everything that belongs
|
|
98
|
+
to that piece: the tempo, bars, chords, the riff, the hook, the sections.
|
|
99
|
+
`references/spec.md` is the spec's full contract and every texture's parts.
|
|
100
|
+
Composed as it ships, the example is a reference: it renders the score it
|
|
101
|
+
was written as, sample for sample.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
$PY $SKILL/scripts/compose.py score.json score.wav
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
It prints the section map in seconds: hold it against the picture's times.
|
|
108
|
+
A spec error is said in words (an undefined chord, a gap between sections,
|
|
109
|
+
a drop with no riff) before anything renders.
|
|
110
|
+
|
|
111
|
+
### 5. Verify by numbers
|
|
112
|
+
|
|
113
|
+
You cannot listen, so measure:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
$PY $SKILL/scripts/analyze.py score.wav --spec score.json
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- **grid**: the kick-band (30 to 150 Hz) onset fit must read the spec's BPM
|
|
120
|
+
(within 0.05) with beat 1 within 0.02 s of t = 0. A fit at half or double
|
|
121
|
+
tempo, or a phase off by half a beat, means the kick pattern is not
|
|
122
|
+
saying the downbeat: put a kick on every beat 1.
|
|
123
|
+
- **order**: a section with more `energy` must be at least 0.5 dB louder
|
|
124
|
+
than one with less.
|
|
125
|
+
- **biggest**: the highest-energy section must clear every other by 1 dB.
|
|
126
|
+
- **holes**: no bar may fall more than 10 dB under the bar before it. A fill
|
|
127
|
+
is not a hole.
|
|
128
|
+
- **ring**: the bar after the last hit is quieter than the hit's bar.
|
|
129
|
+
|
|
130
|
+
Read the per-section table too: it prints each section against the main
|
|
131
|
+
groove, the numbers `references/arranging.md` gives targets for. A failed
|
|
132
|
+
check is fixed with RELATIVE levels: raise or lower that section's `energy`
|
|
133
|
+
and compose again. Never fix a section's level with the master gain, which
|
|
134
|
+
moves every section together. Three faults these checks were built from,
|
|
135
|
+
all in one real first draft: the drop was not louder than the groove, the
|
|
136
|
+
chorus was quieter than the drop, and the spin fills were holes (-29 dB).
|
|
137
|
+
|
|
138
|
+
### 6. Master
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
$PY $SKILL/scripts/master.py score.wav score.mp3 # -16 LUFS, 192k
|
|
142
|
+
$PY $SKILL/scripts/analyze.py score.mp3 --spec score.json --lufs -16
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`master.py` reaches -16 LUFS (the catalog's loudness) with ONE constant
|
|
146
|
+
gain. Do not run a single-pass `ffmpeg -af loudnorm` instead: it rides the
|
|
147
|
+
gain over a few seconds and flattens the arrangement (measured on a real
|
|
148
|
+
score: the build came out as loud as the groove, and the chorus only 0.5 dB
|
|
149
|
+
over the drop). The MP3 must pass the same checks as the WAV.
|
|
150
|
+
|
|
151
|
+
### 7. Put it on the program and preview
|
|
152
|
+
|
|
153
|
+
The score is a track in `doc.json` beside `config.json`, keyed by the MP3's
|
|
154
|
+
file name in the same directory (`vos push` uploads it and keys the hosted
|
|
155
|
+
file):
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"docSchemaVersion": 6,
|
|
160
|
+
"audio": [
|
|
161
|
+
{
|
|
162
|
+
"id": "score",
|
|
163
|
+
"key": "score.mp3",
|
|
164
|
+
"name": "<title>, composed for this film",
|
|
165
|
+
"start": 0, "in": 0, "out": 58.0645, "duration": 58.0645,
|
|
166
|
+
"gain": 0.95, "fadeIn": 0, "fadeOut": 2.2
|
|
167
|
+
}
|
|
168
|
+
],
|
|
169
|
+
"program": {}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`start: 0` and `in: 0` keep beat 1 on frame 0; `out` and `duration` are the
|
|
174
|
+
program's length. No fade in (it would soften the first downbeat); a fade out
|
|
175
|
+
about one bar long under the ring-out. If the program already has a
|
|
176
|
+
`doc.json`, keep its other fields and replace the old track in `audio`.
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
npx vos render config.json preview.mp4 --width 640 --height 360 # mixes doc.json's audio
|
|
180
|
+
ffmpeg -ss <a second inside the biggest moment> -i preview.mp4 -frames:v 1 f.png
|
|
181
|
+
$PY $SKILL/scripts/analyze.py preview.mp4 --bpm <bpm> --bars <bars>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Look at the frame. The preview's analysis confirms the score is in the
|
|
185
|
+
render at the right tempo; its beat 1 reads about 0.05 s late because the
|
|
186
|
+
MP4's AAC track keeps its encoder delay, so the score file, not the preview,
|
|
187
|
+
is the grid check.
|
|
188
|
+
|
|
189
|
+
### 8. Credit and push
|
|
190
|
+
|
|
191
|
+
Credit it honestly, as music made for this film and by no one else: the
|
|
192
|
+
track's `name` is `<title>, composed for this film`, and a description that
|
|
193
|
+
credits music says `music: <title>, composed for this film`. Never an
|
|
194
|
+
artist's name, never "by" anyone who did not write it.
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
vos push config.json --label "score: <title>" \
|
|
198
|
+
--note "an original score at <bpm> BPM, <bars> bars, <genre>, written to the sections: <one line>" --wait
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
In a fetched or tracked directory this versions the same vos (pass
|
|
202
|
+
`--vos <id>` otherwise); a new program takes the usual `--title`, `--folder`
|
|
203
|
+
and `--prompt` (the person's words, as they wrote them, which the watch page
|
|
204
|
+
shows by default: say so). Keep `score.json` beside the program so the next
|
|
205
|
+
round edits the spec, not the MP3: the same spec renders the same WAV,
|
|
206
|
+
sample for sample.
|
|
207
|
+
|
|
208
|
+
### 9. Hand it over, and ask them to listen
|
|
209
|
+
|
|
210
|
+
Give the person the preview (and the vos's watch link), and ask them to
|
|
211
|
+
LISTEN before it is promoted or published. Say it plainly: the numbers show
|
|
212
|
+
the score is on the grid and shaped as intended; they cannot say whether it
|
|
213
|
+
is good music. Ears decide. Their words go back into the spec (a section's
|
|
214
|
+
energy, a chord, the hook) and steps 4 to 8 run again.
|
|
215
|
+
|
|
216
|
+
## Rules
|
|
217
|
+
|
|
218
|
+
- The grid is the program's. Never fit a picture to a track's tempo when
|
|
219
|
+
you can write a track at the picture's.
|
|
220
|
+
- Original or nothing: no quoted melody, riff, bassline, chord hook or
|
|
221
|
+
sample from a real record, and no artist named in the credit.
|
|
222
|
+
- One biggest moment. A score where everything is loud has no drop.
|
|
223
|
+
- Fix shape with section levels, never the master.
|
|
224
|
+
- Normalize with one gain (`master.py`), never a dynamic normalizer.
|
|
225
|
+
- A score is not finished until a person has listened to it.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Arranging to picture
|
|
2
|
+
|
|
3
|
+
A score for a program is written to the picture the way a choreographer
|
|
4
|
+
counts a dance: by bar. The picture already has its sections; the music's
|
|
5
|
+
job is to make each one feel like what it is.
|
|
6
|
+
|
|
7
|
+
## The section map
|
|
8
|
+
|
|
9
|
+
Write it before any notes, one line per section: the bars, what is on
|
|
10
|
+
screen, and what the music does there. The questions it answers:
|
|
11
|
+
|
|
12
|
+
1. **Where is the downbeat?** Bar 0 at t = 0. Everything else is counted
|
|
13
|
+
from it. If the picture's sections do not sit on bar lines, they are not
|
|
14
|
+
on a grid yet: fix that first (step 1 of the skill).
|
|
15
|
+
2. **What does each section DO?** Introduce, groove, stop, push, release,
|
|
16
|
+
peak, end. Two neighbouring sections that do the same thing should
|
|
17
|
+
sound different anyway (a new texture, a new chord pair, the hook
|
|
18
|
+
entering).
|
|
19
|
+
3. **Where does it stop?** A pose that snaps, a cut, a word slamming in:
|
|
20
|
+
these are stop-time hits. The whole band hits on that 16th and is silent
|
|
21
|
+
until the next. Read the picture's own timing for them (a pose table
|
|
22
|
+
keyed every half bar means beats 1 and 3), never guess.
|
|
23
|
+
4. **What carries the transitions?** A spin, a wipe, a scene change gets a
|
|
24
|
+
fill: a snare roll rising in velocity, a riser, kicks keeping the grid,
|
|
25
|
+
and ideally one empty beat before the next downbeat so it lands.
|
|
26
|
+
5. **What is the biggest moment?** One section, usually two thirds to
|
|
27
|
+
three quarters through. It gets the most parts, the hook an octave up,
|
|
28
|
+
the gang claps, and the most energy.
|
|
29
|
+
6. **How does it end?** A pickup into the final pose, the last hit ON the
|
|
30
|
+
pose, and the room ringing out under the last frames. A score that is
|
|
31
|
+
still grooving when the picture stops was not written to it.
|
|
32
|
+
|
|
33
|
+
## Energy contrast, measured
|
|
34
|
+
|
|
35
|
+
These are the steps of a real score that was approved, per section, against
|
|
36
|
+
its main groove (the first `groove` section), all bands. Use them as
|
|
37
|
+
targets, not laws: what matters is the order and the gaps.
|
|
38
|
+
|
|
39
|
+
| Section | Against the groove |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| intro (a hit and a riser) | about -7 dB |
|
|
42
|
+
| build | -1.5 dB |
|
|
43
|
+
| groove | 0 (the reference) |
|
|
44
|
+
| stop-time | +0.5 to +1 dB (fewer events, each one bigger) |
|
|
45
|
+
| a pumping section | +2.5 to +3 dB |
|
|
46
|
+
| the drop | +4.5 to +5 dB |
|
|
47
|
+
| a darker breakdown after the drop | +1.5 to +2 dB (under the drop, over the groove) |
|
|
48
|
+
| the biggest moment | +6 to +7 dB, and at least +1 dB over the drop |
|
|
49
|
+
| a fill bar | about -1.5 dB; never 10 dB or more under the bar before it |
|
|
50
|
+
| the last hit's bar, then the ring-out | about the pumping section, then 5 dB or more under it |
|
|
51
|
+
|
|
52
|
+
## The faults the numbers catch
|
|
53
|
+
|
|
54
|
+
All three were in one real first draft, and each was invisible without
|
|
55
|
+
listening, which an agent cannot do:
|
|
56
|
+
|
|
57
|
+
- **The drop was not louder than the groove.** The release had nothing to
|
|
58
|
+
release. The `order` check catches it.
|
|
59
|
+
- **The chorus was quieter than the drop.** The biggest moment was not the
|
|
60
|
+
biggest. The `biggest` check catches it.
|
|
61
|
+
- **The fills were holes.** The spin bars read -29 dB: the music vanished
|
|
62
|
+
exactly where the picture moved most. The `holes` check catches it.
|
|
63
|
+
|
|
64
|
+
The fix for all three was the same: relative levels per section (`energy`),
|
|
65
|
+
never the master gain. And the shape must survive mastering: a single-pass
|
|
66
|
+
dynamic loudness normalizer moved that same score's sections by up to 3.5 dB
|
|
67
|
+
against each other (the build rose to the groove, the chorus fell to 0.5 dB
|
|
68
|
+
over the drop), which is why `master.py` applies one constant gain.
|
|
69
|
+
|
|
70
|
+
## Writing the riff and the hook
|
|
71
|
+
|
|
72
|
+
- **The riff** is one bar of intervals over the chord's root, so it follows
|
|
73
|
+
the changes: the root on 1, the octave and the seventh, a chromatic
|
|
74
|
+
passing note into beat 3, a run up into the next bar. Leave a 16th or two
|
|
75
|
+
of air; a bass that never rests has no groove.
|
|
76
|
+
- **The hook** is two one-bar phrases, a call and an answer, in the mode's
|
|
77
|
+
pentatonic, with long notes on chord tones and short pickups between. It
|
|
78
|
+
enters with the drop and goes up an octave, with a harmony, in the
|
|
79
|
+
biggest moment, so the peak is the same idea, bigger.
|
|
80
|
+
- **Originality.** Hold what you wrote against every famous song in the
|
|
81
|
+
genre you know; if it is close to one of them, change the rhythm and the
|
|
82
|
+
contour until it is not. Never write the riff, hook or bassline of the
|
|
83
|
+
record a film is made after, and never a sample.
|
|
84
|
+
|
|
85
|
+
## When the picture has no beat
|
|
86
|
+
|
|
87
|
+
Pick the BPM whose bar lines fall nearest the scene times (a scene change
|
|
88
|
+
every 3.9 s suggests 124 BPM, two bars each). Then either retime the
|
|
89
|
+
program's labels onto those bars, which is better when you own it, or
|
|
90
|
+
write fills across the gaps and accept that a change lands up to a 16th
|
|
91
|
+
off the bar. Never stretch the score to the picture: a tempo change you
|
|
92
|
+
cannot hear is still one you cannot verify.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "A 124 BPM new jack swing dance film",
|
|
3
|
+
"bpm": 124,
|
|
4
|
+
"bars": 30,
|
|
5
|
+
"swing": 0.24,
|
|
6
|
+
"seed": 1990,
|
|
7
|
+
"harmony": "E dorian: Em9 and A13 vamps, a descending stop-time line (Em9 D C B7), Am9 to B7 for the dark section, C D Em for the chorus",
|
|
8
|
+
"chords": {
|
|
9
|
+
"Em9": { "root": "E1", "voicing": ["G3", "B3", "D4", "F#4"] },
|
|
10
|
+
"A13": { "root": "A1", "voicing": ["G3", "C#4", "F#4"] },
|
|
11
|
+
"Cmaj7": { "root": "C2", "voicing": ["G3", "B3", "E4"] },
|
|
12
|
+
"D6": { "root": "D2", "voicing": ["A3", "D4", "F#4", "B4"] },
|
|
13
|
+
"Am9": { "root": "A1", "voicing": ["G3", "C4", "E4", "B4"] },
|
|
14
|
+
"B7": { "root": "B1", "voicing": ["F#3", "B3", "D#4", "A4"] },
|
|
15
|
+
"Dmaj": { "root": "D2", "voicing": ["F#3", "A3", "D4"] }
|
|
16
|
+
},
|
|
17
|
+
"riff": [[0, 12, 2], [3, 12, 1], [4, 0, 1], [6, 15, 2], [8, 17, 1], [10, 19, 2], [13, 22, 1], [14, 24, 1]],
|
|
18
|
+
"hook": [
|
|
19
|
+
[[0, "B4", 2], [2, "D5", 1], [3, "E5", 3], [6, "G5", 1], [7, "E5", 2], [10, "D5", 4], [14, "B4", 2]],
|
|
20
|
+
[[0, "A4", 2], [2, "B4", 2], [4, "D5", 3], [8, "E5", 2], [11, "D5", 1], [12, "B4", 4]]
|
|
21
|
+
],
|
|
22
|
+
"sections": [
|
|
23
|
+
{ "name": "intro", "bars": [0, 2], "texture": "intro", "chords": ["Em9"], "picture": "the title card" },
|
|
24
|
+
{ "name": "build", "bars": [2, 4], "texture": "build", "energy": 0.65, "chords": ["Em9", "A13"], "picture": "a half-strength shuffle" },
|
|
25
|
+
{ "name": "i. the shuffle", "bars": [4, 8], "texture": "groove", "energy": 0.72, "chords": ["Em9", "A13"], "crash": 0.25, "picture": "the side-to-side shuffle on the eighths" },
|
|
26
|
+
{ "name": "ii. the palm stop", "bars": [8, 12], "texture": "stop", "energy": 0.75, "chords": ["Em9", "Dmaj", "Cmaj7", "B7"], "hits": [0, 8], "picture": "poses snap on beats 1 and 3" },
|
|
27
|
+
{ "name": "iii. the arm pump", "bars": [12, 15], "texture": "pump", "energy": 1.0, "chords": ["A13", "A13", "Em9"], "crash": 0.22, "picture": "a fist driven up on every beat" },
|
|
28
|
+
{ "name": "spin 1", "bars": [15, 16], "texture": "spin", "picture": "a full spin into the drop" },
|
|
29
|
+
{ "name": "iv. the drop", "bars": [16, 20], "texture": "drop", "energy": 1.05, "chords": ["Em9", "A13"], "crash": 0.35, "hit": "Em9", "picture": "the shuffle again, bigger" },
|
|
30
|
+
{ "name": "v. the split drop", "bars": [20, 24], "texture": "dark", "energy": 0.8, "chords": ["Am9", "Am9", "B7", "B7"], "crash": 0.28, "picture": "down into a split and up, on every beat, the night palette" },
|
|
31
|
+
{ "name": "vi. the chorus line", "bars": [24, 27], "texture": "chorus", "energy": 1.25, "chords": ["Cmaj7", "D6", "Em9"], "crash": 0.38, "hit": "Cmaj7", "picture": "the biggest moment: the pump, everyone" },
|
|
32
|
+
{ "name": "the final pose", "bars": [27, 30], "texture": "ending", "pickup": 0.5, "hits": [{ "at": 27.5, "chord": "B7" }, { "at": 28, "chord": "Em9" }], "picture": "a half-bar spin, then the pose held to the end" }
|
|
33
|
+
],
|
|
34
|
+
"fills": [
|
|
35
|
+
{ "bar": 11, "from": 12, "to": 16, "level": 0.75 },
|
|
36
|
+
{ "bar": 23, "from": 8, "to": 16, "level": 0.6 }
|
|
37
|
+
]
|
|
38
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# The score spec
|
|
2
|
+
|
|
3
|
+
`scripts/compose.py spec.json score.wav` reads one JSON object. The worked
|
|
4
|
+
example is `new-jack-swing-124.json` beside this file.
|
|
5
|
+
|
|
6
|
+
## Positions
|
|
7
|
+
|
|
8
|
+
- A **bar** is four beats; bar 0 starts at t = 0. `bars: [4, 8]` is bars 4,
|
|
9
|
+
5, 6 and 7.
|
|
10
|
+
- A **16th** position inside a bar is 0 to 15: beat 1 is 0, beat 2 is 4,
|
|
11
|
+
beat 3 is 8, beat 4 is 12; the "and" of beat 1 is 2. Odd 16ths are swung
|
|
12
|
+
late by `swing` (a fraction of a 16th).
|
|
13
|
+
- A **bar position** as a number is bar plus a fraction: `27.5` is bar 27,
|
|
14
|
+
16th 8 (beat 3). Used by ending hits.
|
|
15
|
+
- **Notes** are names or MIDI numbers: `"E1"` (28), `"F#3"`, `"Bb2"`, `64`.
|
|
16
|
+
C4 is 60.
|
|
17
|
+
|
|
18
|
+
## Top level
|
|
19
|
+
|
|
20
|
+
| Field | Required | Meaning |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| `bpm` | yes | The program's tempo. |
|
|
23
|
+
| `bars` | yes | The piece's length in bars; the sections must cover exactly 0 to `bars`. |
|
|
24
|
+
| `length` | no | The output length in seconds, when the program's duration is not a whole number of bars (default `bars x 4 x 60 / bpm`). |
|
|
25
|
+
| `swing` | no | How late odd 16ths fall, as a fraction of a 16th: 0 is straight, 0.24 (the default) is about 62 % swing, the new jack and hip hop pocket. |
|
|
26
|
+
| `seed` | no | The noise seed (drums, risers, the room). Same spec, same seed, same file. |
|
|
27
|
+
| `chords` | yes | `{ "<name>": { "root": <bass note>, "voicing": [<comp notes>] } }`. Roots sit low (E1 to D2), voicings around G3 to B4 (three or four notes, close, the 7th and 9th rather than doubled roots). |
|
|
28
|
+
| `riff` | for `drop`, `chorus` | The bass riff, one bar: `[[16th, semitones over the chord's root, length in 16ths], ...]`. It follows the chord changes because it is intervals. |
|
|
29
|
+
| `hook` | for `drop`, `chorus` | The melody: a list of one-bar phrases, each `[[16th, note, length in 16ths], ...]`, cycled bar by bar through a section. Two phrases make a call and an answer. |
|
|
30
|
+
| `sub` | no | The plain bass pattern the build, groove and pump play (default the root, an octave up, on 1, on 3 and on the "and" of 4). Same shape as `riff`. |
|
|
31
|
+
| `held` | no | The held bass the `dark` texture plays (default long notes on 1 and 3, a fifth on 4). |
|
|
32
|
+
| `sections` | yes | The arrangement, in order, no gaps (below). |
|
|
33
|
+
| `fills` | no | Snare rolls over the end of a bar: `{ "bar": 11, "from": 12, "to": 16, "level": 0.75 }` (16ths, straight, rising). A fill is written into the section it sits in. |
|
|
34
|
+
| `title`, `harmony` | no | Words for the next reader. |
|
|
35
|
+
|
|
36
|
+
## Sections
|
|
37
|
+
|
|
38
|
+
Every section has `name` (unique), `bars: [start, end]` and `texture`, plus
|
|
39
|
+
`picture` (free text: what is on screen, ignored by the composer). Groove
|
|
40
|
+
textures take `chords` (one per bar, cycled) and `energy`.
|
|
41
|
+
|
|
42
|
+
`energy` scales every level in the texture by `energy / reference`, so the
|
|
43
|
+
reference plays it exactly as tuned. Textures differ in density, so equal
|
|
44
|
+
energy is not equal loudness: compose, read `analyze.py`, adjust.
|
|
45
|
+
|
|
46
|
+
| Texture | Reference energy | What plays |
|
|
47
|
+
| --- | --- | --- |
|
|
48
|
+
| `intro` | none | An orchestra hit on the first chord (`ring`, default 1.8 s), a riser over the last bar, a hat pickup and one kick into the next section. |
|
|
49
|
+
| `build` | 0.65 | Kick on 1 and 3, claps on 2 and 4, 8th hats, the plain sub bass, a pad. |
|
|
50
|
+
| `groove` | 0.72 | The main groove: a syncopated kick (1, the "and" of 2, 3, the "a" of 3), claps on 2 and 4, 8th hats with two open, the funk pluck on the `sub` pattern, the electric piano comping off the beat. |
|
|
51
|
+
| `stop` | 0.75 | Stop-time: on each 16th in `hits` (default `[0, 8]`, beats 1 and 3) the kick, an orchestra hit, a bass note and a clap together, silence between. |
|
|
52
|
+
| `pump` | 1.0 | Four on the floor, open hats on the off-beats, the funk pluck, brass on every beat. |
|
|
53
|
+
| `spin` | none | A fill section: a rising snare roll and a riser to one beat before the end, kicks on the beats, the last beat empty so the next downbeat lands. `level` (default 0.95) is the roll's. |
|
|
54
|
+
| `drop` | 1.05 | The riff, the hook, brass stabs on the "and" of 1 and 3, 16th hats, the kick adding a pickup every other bar. |
|
|
55
|
+
| `dark` | 0.8 | A breakdown: kick on 1, 3 and its "and", the `held` bass, a pad every two bars, a soft snare on every beat. |
|
|
56
|
+
| `chorus` | 1.25 | Everything: the riff, the hook up `octave` (default 1) with a harmony a fourth under, gang claps, a pad, brass stabs, 16th tambourine. |
|
|
57
|
+
| `ending` | none | `pickup` (bars, a roll and a riser from the section start), then `hits: [{ "at": <bar position>, "chord": "<name>" }, ...]`. Every hit but the last is a short orchestra hit with a kick; the last is the big one (`ring`, default 3.6 s), with a falling brass chord, a held bass note and a crash, and soft hats carry the room out through the bars after it. |
|
|
58
|
+
| `silence` | none | Nothing. The `holes` check skips these bars: a silence is a choice you declare. |
|
|
59
|
+
|
|
60
|
+
Per-section options on groove textures:
|
|
61
|
+
|
|
62
|
+
| Option | Meaning |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| `kicks` | 16ths for the kick: `[0, 4, 8, 12]`, or a list of lists cycled bar by bar. |
|
|
65
|
+
| `claps` | 16ths for the claps (default `[4, 12]`). |
|
|
66
|
+
| `hats` | `"16"`, `"8"` or `""` for none. |
|
|
67
|
+
| `open_hats` | 16ths where the hat opens. |
|
|
68
|
+
| `crash` | A crash on the section's downbeat, at this level (0.2 to 0.4). |
|
|
69
|
+
| `hit` | A chord name: an orchestra hit on the section's downbeat. |
|
|
70
|
+
|
|
71
|
+
## Changing the sound
|
|
72
|
+
|
|
73
|
+
The instruments are methods on `Score` (`kick`, `clap`, `bass_note`,
|
|
74
|
+
`brass_chord`, ...), the textures are `tex_<name>` methods, and the mix
|
|
75
|
+
(room reverb, bus saturation, the cut to length) is `render`. A new texture
|
|
76
|
+
is a `tex_<name>` method plus its reference in `TEXTURE_ENERGY` (and in
|
|
77
|
+
`analyze.py`'s copy when it is a groove texture the order check should
|
|
78
|
+
rank). A new instrument is a method beside the others, synthesized, never
|
|
79
|
+
a sample of a record.
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Verify a score by numbers, because an agent cannot listen.
|
|
4
|
+
|
|
5
|
+
python analyze.py score.mp3 --spec spec.json [--lufs -16]
|
|
6
|
+
python analyze.py score.mp3 --bpm 124 --bars 30
|
|
7
|
+
|
|
8
|
+
Prints the tempo and first-beat fit (kick-band onset flux), the length, the
|
|
9
|
+
integrated loudness, per-bar loudness (all bands and the 30 to 150 Hz low
|
|
10
|
+
band), and, with a spec, per-section loudness against the reference section
|
|
11
|
+
and the checks below. Exits 1 when a check fails. Needs ffmpeg, numpy, scipy.
|
|
12
|
+
|
|
13
|
+
The checks (with --spec):
|
|
14
|
+
grid the tempo fit is the spec's BPM within 0.05, beat 1 within 0.02 s of t = 0
|
|
15
|
+
length the file is the program's length within 0.05 s
|
|
16
|
+
order a section with more energy is at least 0.5 dB louder than one with less
|
|
17
|
+
biggest the highest-energy section is at least 1 dB louder than every other
|
|
18
|
+
holes no bar falls more than 10 dB under the bar before it (a fill is not a hole)
|
|
19
|
+
ring the bar after the last hit is quieter than the hit's bar (the ending rings out)
|
|
20
|
+
lufs integrated loudness within 1 LU of --lufs, when given
|
|
21
|
+
"""
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import re
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
from scipy import signal
|
|
30
|
+
|
|
31
|
+
SR = 11025
|
|
32
|
+
# The energy each groove texture plays at by default (compose.py's TEXTURE_ENERGY).
|
|
33
|
+
TEXTURE_ENERGY = {'build': 0.65, 'groove': 0.72, 'stop': 0.75, 'pump': 1.0, 'drop': 1.05, 'dark': 0.8, 'chorus': 1.25}
|
|
34
|
+
GROOVES = tuple(TEXTURE_ENERGY)
|
|
35
|
+
ORDER_GAP = 0.03 # energies closer than this are not ordered
|
|
36
|
+
ORDER_STEP = 0.5 # dB a higher-energy section must clear
|
|
37
|
+
BIGGEST_STEP = 1.0 # dB the biggest moment must clear
|
|
38
|
+
HOLE = 10.0 # dB a bar may fall under the bar before it
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def decode(path):
|
|
42
|
+
out = subprocess.run(
|
|
43
|
+
['ffmpeg', '-v', 'error', '-i', path, '-ac', '1', '-ar', str(SR), '-f', 'f32le', '-'],
|
|
44
|
+
capture_output=True,
|
|
45
|
+
)
|
|
46
|
+
if out.returncode != 0:
|
|
47
|
+
sys.exit(f'ffmpeg could not read {path}: {out.stderr.decode().strip()}')
|
|
48
|
+
return np.frombuffer(out.stdout, dtype=np.float32).astype(np.float64)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def integrated_lufs(path):
|
|
52
|
+
out = subprocess.run(
|
|
53
|
+
['ffmpeg', '-v', 'info', '-i', path, '-af', 'loudnorm=print_format=json', '-f', 'null', '-'],
|
|
54
|
+
capture_output=True, text=True,
|
|
55
|
+
)
|
|
56
|
+
m = re.search(r'"input_i"\s*:\s*"(-?[\d.]+|-inf)"', out.stderr)
|
|
57
|
+
return float(m.group(1)) if m and m.group(1) != '-inf' else None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def tempo_fit(flux, fps, lo, hi, step):
|
|
61
|
+
"""The BPM and first-beat phase whose beat grid lands on the most kick onsets."""
|
|
62
|
+
dur = len(flux) / fps
|
|
63
|
+
best = (-1.0, None, None)
|
|
64
|
+
for bpm in np.arange(lo, hi + step / 2, step):
|
|
65
|
+
P = 60.0 / bpm
|
|
66
|
+
offs = np.arange(0, P, 0.002)
|
|
67
|
+
beats = np.arange(0, dur, P)
|
|
68
|
+
idx = ((offs[:, None] + beats[None, :]) * fps).astype(int)
|
|
69
|
+
valid = idx < len(flux)
|
|
70
|
+
scores = np.where(valid, flux[np.minimum(idx, len(flux) - 1)], 0).sum(axis=1)
|
|
71
|
+
k = int(np.argmax(scores))
|
|
72
|
+
if scores[k] > best[0]:
|
|
73
|
+
best = (scores[k], bpm, offs[k])
|
|
74
|
+
return best[1], best[2]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def db(x):
|
|
78
|
+
return 20 * np.log10(np.sqrt(np.mean(x ** 2)) + 1e-9) if len(x) else -180.0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main():
|
|
82
|
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
83
|
+
ap.add_argument('audio')
|
|
84
|
+
ap.add_argument('--spec', help='the compose spec: BPM, bars and the sections to check')
|
|
85
|
+
ap.add_argument('--bpm', type=float, help='the grid tempo, without a spec')
|
|
86
|
+
ap.add_argument('--bars', type=int, help='the number of bars, without a spec')
|
|
87
|
+
ap.add_argument('--lufs', type=float, help='check integrated loudness against this target (the catalog is -16)')
|
|
88
|
+
a = ap.parse_args()
|
|
89
|
+
|
|
90
|
+
spec = None
|
|
91
|
+
if a.spec:
|
|
92
|
+
with open(a.spec) as f:
|
|
93
|
+
spec = json.load(f)
|
|
94
|
+
bpm = float(spec['bpm']) if spec else a.bpm
|
|
95
|
+
bars = int(spec['bars']) if spec else a.bars
|
|
96
|
+
|
|
97
|
+
x = decode(a.audio)
|
|
98
|
+
sos = signal.butter(4, [30, 150], 'bandpass', fs=SR, output='sos')
|
|
99
|
+
low = signal.sosfilt(sos, x)
|
|
100
|
+
hop = 64
|
|
101
|
+
fps = SR / hop
|
|
102
|
+
n = (len(low) - hop) // hop
|
|
103
|
+
env = np.sqrt(np.mean(low[: n * hop].reshape(n, hop) ** 2, axis=1))
|
|
104
|
+
flux = np.maximum(0, np.diff(env, prepend=env[0]))
|
|
105
|
+
|
|
106
|
+
if bpm:
|
|
107
|
+
fit_bpm, phase = tempo_fit(flux, fps, bpm - 3, bpm + 3, 0.01)
|
|
108
|
+
else:
|
|
109
|
+
coarse, _ = tempo_fit(flux, fps, 70, 180, 0.1)
|
|
110
|
+
fit_bpm, phase = tempo_fit(flux, fps, coarse - 0.3, coarse + 0.3, 0.01)
|
|
111
|
+
P = 60.0 / fit_bpm
|
|
112
|
+
off = min(phase, P - phase) # distance of the beat grid from t = 0
|
|
113
|
+
print(f'tempo fit: {fit_bpm:.2f} BPM, first beat at {phase:.3f} s')
|
|
114
|
+
length = len(x) / SR
|
|
115
|
+
print(f'length: {length:.3f} s')
|
|
116
|
+
lufs = integrated_lufs(a.audio)
|
|
117
|
+
if lufs is not None:
|
|
118
|
+
print(f'integrated loudness: {lufs:.1f} LUFS')
|
|
119
|
+
|
|
120
|
+
checks = []
|
|
121
|
+
|
|
122
|
+
def check(name, ok, line):
|
|
123
|
+
checks.append(ok)
|
|
124
|
+
print(f' {"pass" if ok else "FAIL"} {name:<8} {line}')
|
|
125
|
+
|
|
126
|
+
if not bpm:
|
|
127
|
+
print('(no --spec or --bpm: the fit is a search from 70 to 180 BPM; half and double tempo are possible)')
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
BAR = 4 * 60.0 / bpm
|
|
131
|
+
bars = bars or int(round(length / BAR))
|
|
132
|
+
per_bar = []
|
|
133
|
+
for b in range(bars):
|
|
134
|
+
i0, i1 = int(b * BAR * SR), int((b + 1) * BAR * SR)
|
|
135
|
+
per_bar.append((db(x[i0:i1]), db(low[i0:i1])))
|
|
136
|
+
print('\nbar start s all dB low dB')
|
|
137
|
+
for b, (al, lo) in enumerate(per_bar):
|
|
138
|
+
print(f'{b:3d} {b * BAR:8.3f} {al:6.1f} {lo:6.1f}')
|
|
139
|
+
|
|
140
|
+
print('\nchecks:')
|
|
141
|
+
check('grid', abs(fit_bpm - bpm) <= 0.05 and off <= 0.02,
|
|
142
|
+
f'{fit_bpm:.2f} BPM against {bpm:g}, beat 1 {off:.3f} s from t = 0 (limits 0.05 BPM, 0.02 s)')
|
|
143
|
+
expect_len = float(spec.get('length', bars * BAR)) if spec else bars * BAR
|
|
144
|
+
check('length', abs(length - expect_len) <= 0.05, f'{length:.3f} s against {expect_len:.3f} s')
|
|
145
|
+
if a.lufs is not None:
|
|
146
|
+
ok = lufs is not None and abs(lufs - a.lufs) <= 1.0
|
|
147
|
+
check('lufs', ok, f'{lufs if lufs is not None else "unreadable"} against {a.lufs:g} (within 1 LU)')
|
|
148
|
+
|
|
149
|
+
if spec:
|
|
150
|
+
sections = spec['sections']
|
|
151
|
+
loud = {}
|
|
152
|
+
print('\nsection bars texture energy all dB low dB vs ref')
|
|
153
|
+
ref = next((s for s in sections if s['texture'] == 'groove'), None) or \
|
|
154
|
+
next((s for s in sections if s['texture'] in GROOVES), None)
|
|
155
|
+
for s in sections:
|
|
156
|
+
b0, b1 = s['bars']
|
|
157
|
+
i0, i1 = int(b0 * BAR * SR), int(b1 * BAR * SR)
|
|
158
|
+
loud[s['name']] = (db(x[i0:i1]), db(low[i0:i1]))
|
|
159
|
+
ref_db = loud[ref['name']][0] if ref else 0.0
|
|
160
|
+
for s in sections:
|
|
161
|
+
al, lo = loud[s['name']]
|
|
162
|
+
if s['texture'] in GROOVES:
|
|
163
|
+
s.setdefault('energy', TEXTURE_ENERGY[s['texture']])
|
|
164
|
+
e = s.get('energy')
|
|
165
|
+
es = f'{e:6.2f}' if e is not None else ' '
|
|
166
|
+
mark = ' (ref)' if ref is s else ''
|
|
167
|
+
print(f'{s["name"][:22]:<22} {s["bars"][0]:>4g}-{s["bars"][1]:<4g} {s["texture"]:<7} {es} '
|
|
168
|
+
f'{al:6.1f} {lo:6.1f} {al - ref_db:+5.1f}{mark}')
|
|
169
|
+
|
|
170
|
+
print('\nshape:')
|
|
171
|
+
grooves = [s for s in sections if s['texture'] in GROOVES]
|
|
172
|
+
pairs, closest, order_failed = 0, None, False
|
|
173
|
+
for i, s in enumerate(grooves):
|
|
174
|
+
for t in grooves[i + 1:]:
|
|
175
|
+
hi_s, lo_s = (s, t) if s['energy'] > t['energy'] else (t, s)
|
|
176
|
+
if abs(s['energy'] - t['energy']) < ORDER_GAP:
|
|
177
|
+
continue
|
|
178
|
+
pairs += 1
|
|
179
|
+
step = loud[hi_s['name']][0] - loud[lo_s['name']][0]
|
|
180
|
+
line = (f'{hi_s["name"]} (energy {hi_s["energy"]:g}) is {step:+.1f} dB over {lo_s["name"]} '
|
|
181
|
+
f'(energy {lo_s["energy"]:g}), needs +{ORDER_STEP:g}')
|
|
182
|
+
if step < ORDER_STEP:
|
|
183
|
+
order_failed = True
|
|
184
|
+
check('order', False, line)
|
|
185
|
+
elif closest is None or step < closest[0]:
|
|
186
|
+
closest = (step, line)
|
|
187
|
+
if closest and not order_failed:
|
|
188
|
+
check('order', True, f'{pairs} pairs in energy order; the closest: {closest[1]}')
|
|
189
|
+
if grooves:
|
|
190
|
+
top = max(grooves, key=lambda s: s['energy'])
|
|
191
|
+
others = [s for s in sections if s is not top]
|
|
192
|
+
if others:
|
|
193
|
+
runner = max(others, key=lambda s: loud[s['name']][0])
|
|
194
|
+
step = loud[top['name']][0] - loud[runner['name']][0]
|
|
195
|
+
check('biggest', step >= BIGGEST_STEP,
|
|
196
|
+
f'{top["name"]} is {step:+.1f} dB over the next loudest, {runner["name"]} (needs +{BIGGEST_STEP:g})')
|
|
197
|
+
silent = set()
|
|
198
|
+
for s in sections:
|
|
199
|
+
if s['texture'] == 'silence':
|
|
200
|
+
silent.update(range(int(s['bars'][0]), int(np.ceil(s['bars'][1]))))
|
|
201
|
+
worst = (0.0, None)
|
|
202
|
+
for b in range(1, bars):
|
|
203
|
+
if b in silent:
|
|
204
|
+
continue
|
|
205
|
+
fall = per_bar[b - 1][0] - per_bar[b][0]
|
|
206
|
+
if fall > worst[0]:
|
|
207
|
+
worst = (fall, b)
|
|
208
|
+
check('holes', worst[0] <= HOLE,
|
|
209
|
+
f'the deepest fall is {worst[0]:.1f} dB, into bar {worst[1]} (limit {HOLE:g} dB)' if worst[1] is not None
|
|
210
|
+
else 'no bar falls under the bar before it')
|
|
211
|
+
ending = next((s for s in sections if s['texture'] == 'ending'), None)
|
|
212
|
+
if ending and ending.get('hits'):
|
|
213
|
+
hb = int(np.floor(max(h['at'] for h in ending['hits'])))
|
|
214
|
+
if hb + 1 < bars:
|
|
215
|
+
check('ring', per_bar[hb + 1][0] < per_bar[hb][0],
|
|
216
|
+
f'bar {hb + 1} is {per_bar[hb + 1][0] - per_bar[hb][0]:+.1f} dB against the last hit\'s bar {hb}')
|
|
217
|
+
|
|
218
|
+
failed = checks.count(False)
|
|
219
|
+
print(f'\n{len(checks) - failed} of {len(checks)} checks pass' + ('' if not failed else f', {failed} FAIL'))
|
|
220
|
+
if failed:
|
|
221
|
+
sys.exit(1)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
if __name__ == '__main__':
|
|
225
|
+
main()
|