scoremill 0.3.0__py3-none-any.whl
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,331 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scoremill
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Text-notation MIDI composition library designed for language-model agents
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/CharlesCNorton/scoremill
|
|
7
|
+
Project-URL: Issues, https://github.com/CharlesCNorton/scoremill/issues
|
|
8
|
+
Keywords: midi,music,composition,notation,agents,llm
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: MIDI
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: mido>=1.3
|
|
16
|
+
Provides-Extra: play
|
|
17
|
+
Requires-Dist: python-rtmidi>=1.5; extra == "play"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# scoremill
|
|
21
|
+
|
|
22
|
+
Text-notation MIDI composition for language-model agents.
|
|
23
|
+
|
|
24
|
+
## The bet
|
|
25
|
+
|
|
26
|
+
Machine music today mostly means generation models: sample the weights,
|
|
27
|
+
keep the audio. What comes out can be striking, but it is a performance
|
|
28
|
+
without a score. There is nothing to read, nothing to revise, no theme
|
|
29
|
+
to develop, and no way for the author to verify its own work short of
|
|
30
|
+
listening, which an agent cannot do.
|
|
31
|
+
|
|
32
|
+
Scoremill is the other bet: that an agent which can reason should
|
|
33
|
+
compose the way literate musicians always have, in notation. A piece
|
|
34
|
+
here is a short text. Development is a function applied to a theme.
|
|
35
|
+
Correctness is checked before a note sounds, and the score can be
|
|
36
|
+
diffed, transposed, inverted, linted, and argued about, because it is
|
|
37
|
+
symbolic all the way down. As agents grow more capable this bet
|
|
38
|
+
compounds, since a model that writes scores can explain them, refactor
|
|
39
|
+
them, and build a style deliberately rather than sampling one. We
|
|
40
|
+
think this is the winning branch, and as far as we know scoremill is
|
|
41
|
+
the first library built for it.
|
|
42
|
+
|
|
43
|
+
The design grew from one observation: an agent composing in text
|
|
44
|
+
cannot hear its mistakes, so the notation layer must catch them
|
|
45
|
+
instead. Every bar is validated at parse time, errors come with
|
|
46
|
+
corrective hints, a counterpoint linter flags collisions and
|
|
47
|
+
parallels, and `report()` returns a structured summary the author can
|
|
48
|
+
assert on. The feedback loop stands in for ears. It was written by an
|
|
49
|
+
agent, for agents, composing on a real piano, and its details come
|
|
50
|
+
from the mistakes actually made along the way. Humans are welcome too.
|
|
51
|
+
|
|
52
|
+
Use it through the notation, or raw: `Song`, `Voice`, the transforms,
|
|
53
|
+
and the renderer are ordinary Python, importable a la carte, and the
|
|
54
|
+
tick-level event stream is available to any agent that prefers to
|
|
55
|
+
work below the notation.
|
|
56
|
+
|
|
57
|
+
## The demo score
|
|
58
|
+
|
|
59
|
+
`examples/saltarello_alla_chico.py` is the house demonstration: a 6/8
|
|
60
|
+
novelty saltarello with a staccato jump tune over an oom-pah left
|
|
61
|
+
hand, grace-note pickups, pistol-finger accents, a cadential trill, an
|
|
62
|
+
echo strain built with `variant()`, a coda that turns to A major by
|
|
63
|
+
switching the section key signature, and an accelerando through a
|
|
64
|
+
sixteenth run to one last plink at the top of the keyboard.
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
python examples/saltarello_alla_chico.py # renders the .mid
|
|
68
|
+
python examples/saltarello_alla_chico.py --play # performs it
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Install
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
pip install scoremill # library (pulls mido)
|
|
75
|
+
pip install scoremill[play] # + python-rtmidi for real-time ports
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Or copy `scoremill.py` into your project — it is a single file — or
|
|
79
|
+
`pip install -e .` from a clone.
|
|
80
|
+
|
|
81
|
+
## Sixty seconds
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from scoremill import Song, shift
|
|
85
|
+
|
|
86
|
+
s = Song(tempo=96, time="4/4", key="Am", humanize=1)
|
|
87
|
+
|
|
88
|
+
MOTIF = "a4e c5e e5q d5e c5e" # three beats of material
|
|
89
|
+
A = s.section("A")
|
|
90
|
+
A.voice("rh", vel=52).bars(
|
|
91
|
+
f"!mp {MOTIF} b4q | {shift(MOTIF, -1)} a4q |"
|
|
92
|
+
" e5q {d5 c5 b4}q a4h |" # triplet on beat two
|
|
93
|
+
" [a3 c4 e4]w^& |") # rolled final chord, fermata
|
|
94
|
+
A.voice("lh", vel=36).harmony(
|
|
95
|
+
"Am G Am E7", style="broken", voicing="smooth")
|
|
96
|
+
A.pedal("bar")
|
|
97
|
+
s.ritardando("A", 3, 4, 70)
|
|
98
|
+
s.arrange("A")
|
|
99
|
+
|
|
100
|
+
s.describe() # printed summary
|
|
101
|
+
s.lint() # counterpoint findings
|
|
102
|
+
s.save("evening.mid") # render
|
|
103
|
+
s.play() # or perform on the first MIDI output
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
If a bar does not add up, the parse fails immediately and says so:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
voice 'A.rh' bar 2: has 3.0 beats, expected 4.0 — short by 1.0 beats (a 'q').
|
|
110
|
+
bar was: d4e b4e c5q g4q
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Notation
|
|
114
|
+
|
|
115
|
+
| Element | Syntax | Notes |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| Pitch | `c d e f g a b` + `# b n` + octave | octave is sticky per voice; key signature applies (`key="F"` makes `b` mean B-flat, `bn` natural); minor keys (`Am`, `Dm`, ...) supported |
|
|
118
|
+
| Duration | trailing `w h q e s t`, optional `.` | sticky; `r` = rest |
|
|
119
|
+
| Chord | `[c4 e g]h` | shared duration |
|
|
120
|
+
| Tuplet | `{c4 d4 e4}q`, `{[c4 e4] d4}q` | members divide the span equally; a member may be a chord |
|
|
121
|
+
| Grace | `+d5` | sounds just before the next note; stackable |
|
|
122
|
+
| Tie | `c5h~` | the next note must repeat the pitch (validated); a tie on a voice's last note is laissez vibrer |
|
|
123
|
+
| Marks | `>` accent · `'` staccato · `_` legato · `^` fermata · `&` roll · `%` trill | after the duration; fermata length and trill rate are configurable on `Song` |
|
|
124
|
+
| Dynamics | `!ppp !pp !p !mp !mf !f !ff !fff`, `cresc`, `dim` | sticky; cresc/dim interpolate to the next mark, which must exist (validated) |
|
|
125
|
+
| Barline | `\|` | asserts the bar is exactly full |
|
|
126
|
+
|
|
127
|
+
## Motif transforms
|
|
128
|
+
|
|
129
|
+
Development as string-to-string functions. Write a subject once and
|
|
130
|
+
derive the rest:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
shift(frag, 2) # diatonic sequence up two steps
|
|
134
|
+
invert(frag, axis="g4") # mirror about an axis pitch
|
|
135
|
+
retro(frag) # retrograde
|
|
136
|
+
stretch(frag, 2) # augmentation (0.5 for diminution)
|
|
137
|
+
rebar(frag, 3) # re-insert barlines every 3 beats
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Explicit alterations travel with their scale degree under `shift` and
|
|
141
|
+
are mirrored under `invert` (a raised degree inverts to a lowered
|
|
142
|
+
one). `retro` insists the fragment contain no barlines, dynamics, or
|
|
143
|
+
ties; apply those around the result. A grace note travels with the
|
|
144
|
+
note it ornaments, tuplet members reverse within their tuplet, and
|
|
145
|
+
sticky octaves and durations are written out first so the reversal
|
|
146
|
+
cannot change what a token means. `stretch` changes durations and
|
|
147
|
+
therefore the barring, so pair it with `rebar`, which re-inserts
|
|
148
|
+
barlines at a chosen bar length and errors if a note would straddle
|
|
149
|
+
one.
|
|
150
|
+
|
|
151
|
+
To move a finished piece to another key, `song.transpose(semitones)`
|
|
152
|
+
shifts every entered note in place and relabels the keys, raising if a
|
|
153
|
+
note would leave the instrument range. This is chromatic transposition
|
|
154
|
+
of the whole song, distinct from the diatonic `shift` on fragments.
|
|
155
|
+
|
|
156
|
+
## Harmony
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
voice.harmony("C Am7 F G7", style="stride", voicing="smooth",
|
|
160
|
+
avoid=melody)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Twenty-six chord qualities (`m 7 maj7 m7 6 m6 dim dim7 m7b5 aug sus2
|
|
164
|
+
sus4 9 maj9 m9 add9 mmaj7 m11 7sus4 9sus4 7b5 7#5 7b9 7#9 11 13`),
|
|
165
|
+
slash basses (`C/G`), and eight accompaniment styles (`block root
|
|
166
|
+
fifth waltz alberti arp broken stride`; waltz, stride, and broken fill
|
|
167
|
+
fractional meters). `voicing=` chooses how each chord is spelled:
|
|
168
|
+
`plain` (close position), `smooth` (inversions that minimize movement
|
|
169
|
+
between chords), `shell` (root, third, and seventh), `rootless` (drop
|
|
170
|
+
the root for a comping color), or `drop2` (lower the second voice from
|
|
171
|
+
the top an octave, a wider open spread). A slash bass is never
|
|
172
|
+
disturbed. `harmony()` takes its own `octave` argument for the
|
|
173
|
+
register of the chord roots, independent of the octave the voice uses
|
|
174
|
+
for melodic input.
|
|
175
|
+
|
|
176
|
+
`avoid=<voice>` makes the accompaniment melody-aware: chord tones that
|
|
177
|
+
would double the named voice's pitch classes on a shared onset are
|
|
178
|
+
dropped, and single figure tones that would collide at the exact
|
|
179
|
+
unison move an octave away. When the song declares a pickup and the
|
|
180
|
+
accompaniment voice is still empty, `harmony()` inserts the pickup
|
|
181
|
+
rest itself.
|
|
182
|
+
|
|
183
|
+
## Expression
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
Song(swing=0.62, swing_unit="sixteenth", humanize=2, expressive=True,
|
|
187
|
+
fermata=1.6, trill_rate=0.125)
|
|
188
|
+
section.rubato(0.05, phrase=2, shape="arch") # or "cradle"
|
|
189
|
+
section.pedal("bar") # "half", or a number of beats
|
|
190
|
+
section.soft() # una corda for the section
|
|
191
|
+
s.tempo_change("A", bar=5, bpm=80) # step change
|
|
192
|
+
s.ritardando("A", 7, 8, 60) # linear ramp; a faster target
|
|
193
|
+
# produces an accelerando
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`expressive` adds downbeat lean, melodic-contour shading, and top-note
|
|
197
|
+
voicing inside chords; `humanize` adds slight timing and velocity
|
|
198
|
+
variation. `swing_unit` swings eighths or sixteenths; `fermata` sets
|
|
199
|
+
how far a `^` note overshoots its written length; `trill_rate` sets a
|
|
200
|
+
`%` trill's alternation speed. Rubato is an `"arch"` that presses
|
|
201
|
+
forward and relaxes, or a `"cradle"` that broadens mid-phrase.
|
|
202
|
+
|
|
203
|
+
## Analysis
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
s.lint() # collisions and parallels, located by bar and beat
|
|
207
|
+
s.report() # dict: sections, voices, ranges, density, duration, lint
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`lint()` reports two things, each located by bar and beat: collisions,
|
|
211
|
+
where two voices sound the same pitch at once, whether struck together
|
|
212
|
+
or struck against a held note; and consecutive parallel fifths or
|
|
213
|
+
octaves, checked on both the top and the bottom line of each voice
|
|
214
|
+
pair. `report()` exists so an agent can check its own work
|
|
215
|
+
programmatically, and its duration integrates the full tempo map:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
assert s.report()["duration_s"] < 180
|
|
219
|
+
assert not s.report()["lint"]
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The linter is advisory. Styles that double the tune and the
|
|
223
|
+
accompaniment on strong beats will trip the parallel checks on
|
|
224
|
+
purpose; read the findings, keep the ones that are idiom, fix the
|
|
225
|
+
ones that are accidents. When a texture doubles by design, pass
|
|
226
|
+
`lint(mode="homophonic")` to keep only the collisions.
|
|
227
|
+
|
|
228
|
+
## Raw access
|
|
229
|
+
|
|
230
|
+
Notation is the front door, not the only one. `song.events()` returns
|
|
231
|
+
the fully expressive event stream as sorted `(tick, kind, channel, a,
|
|
232
|
+
b)` tuples at 480 ticks per beat, exactly what `save()` and `play()`
|
|
233
|
+
render, for agents that prefer to work below the notation:
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
for tick, kind, ch, a, b in song.events():
|
|
237
|
+
... # kind in {"on", "off", "cc64", "cc67", "tempo"}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`Song`, `Voice`, the transforms, and the renderer are ordinary Python,
|
|
241
|
+
importable a la carte; a `Voice`'s `notes` list accepts hand-built
|
|
242
|
+
`Note` objects, which bypass notation validation.
|
|
243
|
+
|
|
244
|
+
Two query helpers answer "what notes are in this?" without a Song, so
|
|
245
|
+
an agent can reason about harmony directly: `chord_pitches("Cmaj9")`
|
|
246
|
+
returns the MIDI pitches of a chord symbol (slash bass first when
|
|
247
|
+
present), and `scale_pitches("Am")` returns the seven pitches of a
|
|
248
|
+
key's diatonic scale, ascending from the tonic.
|
|
249
|
+
|
|
250
|
+
## Playback
|
|
251
|
+
|
|
252
|
+
`play()` streams in real time through [mido](https://mido.readthedocs.io)
|
|
253
|
+
(requires `python-rtmidi`). It picks the first hardware output, or
|
|
254
|
+
match one by substring: `s.play(port="FluidSynth")`. `play(count_in=4)`
|
|
255
|
+
taps four beats before the music; `play(progress=fn)` calls `fn` with
|
|
256
|
+
each message as it goes out. Playback releases all notes and both
|
|
257
|
+
pedals on exit, so an interrupt leaves nothing hanging. Without
|
|
258
|
+
hardware, render with `save()` and use any soft synth, for example:
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
fluidsynth -a pulseaudio soundfont.sf2 piece.mid
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
A pitched-instrument range guard rejects notes a piano cannot play;
|
|
265
|
+
widen it for synths with `Song(pitch_range=(0, 127))`.
|
|
266
|
+
|
|
267
|
+
## Examples
|
|
268
|
+
|
|
269
|
+
| File | Demonstrates |
|
|
270
|
+
|---|---|
|
|
271
|
+
| `examples/saltarello_alla_chico.py` | the demo score: variants, per-section keys, grace notes, trill, accelerando |
|
|
272
|
+
| `examples/dynamo_rag.py` | a full multi-strain ragtime: stride bass, written syncopation, subdominant trio |
|
|
273
|
+
| `examples/silver_dollar_saloon.py` | a frontier two-step: oom-pah, honky-tonk grace slides, shave-and-a-haircut tag |
|
|
274
|
+
| `examples/nickelodeon.py` | a player-piano novelty roll: generated figuration, secondary-rag accents, whole-tone runs |
|
|
275
|
+
| `examples/player_piano_studies.py` | five Nancarrow-style studies built through the raw `Note` API: tempo canon, coprime ostinati, acceleration, full-keyboard cascades, tutti |
|
|
276
|
+
| `examples/minuet_small_computer.py` | sections, waltz harmony, arrangement |
|
|
277
|
+
| `examples/blues_416_megabytes.py` | swing, grace notes, stride, rubato |
|
|
278
|
+
| `examples/invention_two_processes.py` | motif transforms, two-voice counterpoint, lint |
|
|
279
|
+
| `examples/orrery.py` | process music: prime-period orbits, overtone pitches |
|
|
280
|
+
|
|
281
|
+
Running an example writes its `.mid` next to it; add `--play` to
|
|
282
|
+
perform it on a connected MIDI output.
|
|
283
|
+
|
|
284
|
+
## Jukebox
|
|
285
|
+
|
|
286
|
+
`jukebox.py` plays a whole folder of scores on a MIDI output. It
|
|
287
|
+
renders each script once (the "running a script writes its `.mid`"
|
|
288
|
+
contract, so a script that builds several songs contributes several
|
|
289
|
+
tracks) and plays the results.
|
|
290
|
+
|
|
291
|
+
With no flag it opens the GUI (tkinter): a searchable
|
|
292
|
+
track list, play/stop/auto/loop, tempo/volume/voice, and a Local/Remote
|
|
293
|
+
toggle that sends output to a port on this machine or, over the
|
|
294
|
+
forwarder, to an instrument on another host. The rest are headless, for
|
|
295
|
+
agents and automation:
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
python jukebox.py # GUI
|
|
299
|
+
python jukebox.py --list # print the playlist and exit
|
|
300
|
+
python jukebox.py --track 3 # play one track and exit
|
|
301
|
+
python jukebox.py --all # play the playlist in order
|
|
302
|
+
python jukebox.py --dir myscores --port "FluidSynth"
|
|
303
|
+
python jukebox.py --library ~/midi # a folder of MIDI files, not scripts
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
A `--library DIR` source browses a directory of existing MIDI files
|
|
307
|
+
instead of rendering scoremill scripts; its subfolders become named
|
|
308
|
+
playlists (the GUI's Genre and Category menus).
|
|
309
|
+
|
|
310
|
+
It prefers a real instrument port and warns when only a MIDI loopback
|
|
311
|
+
is available (which makes no sound). Real output needs `python-rtmidi`.
|
|
312
|
+
Re-launching is instant, since scores already rendered are not rebuilt.
|
|
313
|
+
|
|
314
|
+
To play an instrument attached to another machine, run the jukebox on
|
|
315
|
+
the far side too:
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
python jukebox.py --forward # on the host with the instrument
|
|
319
|
+
python jukebox.py --remote HOST # on the host driving playback
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
The `--remote` side streams each MIDI message over TCP to the
|
|
323
|
+
`--forward` side, which relays it to a local port; the driving machine
|
|
324
|
+
needs no MIDI hardware or backend, only `mido` to parse the scores.
|
|
325
|
+
The forwarder re-selects the instrument on each connection, so it may
|
|
326
|
+
start before the instrument is powered on, and it releases every note
|
|
327
|
+
if the client drops.
|
|
328
|
+
|
|
329
|
+
## License
|
|
330
|
+
|
|
331
|
+
MIT.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
scoremill.py,sha256=RPdNraa3q9Rze7OcrjlcYnxkk2kte5cajdG5z7wN7mU,82711
|
|
2
|
+
scoremill-0.3.0.dist-info/licenses/LICENSE,sha256=9sJIeUP6AOrfY794gPxnV4yvyJN5Tg5SIUIm-HYZ3qc,1079
|
|
3
|
+
scoremill-0.3.0.dist-info/METADATA,sha256=JIft4vityh-wpzcGnSkN7rAdIXJQOsAn6SEKZucz4KU,14670
|
|
4
|
+
scoremill-0.3.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
5
|
+
scoremill-0.3.0.dist-info/top_level.txt,sha256=27_qOk91jiAAVHxynSRPF4O9F7zEifMirpI3WSboj6A,10
|
|
6
|
+
scoremill-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Scoremill 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 @@
|
|
|
1
|
+
scoremill
|