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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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