head_music 22.0.0 → 23.0.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.
Files changed (582) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/release/SKILL.md +21 -1
  3. data/.github/workflows/ci.yml +37 -0
  4. data/.gitignore +1 -0
  5. data/.ruby-version +1 -1
  6. data/.rubycritic.yml +7 -0
  7. data/CHANGELOG.md +258 -0
  8. data/CLAUDE.md +6 -4
  9. data/Gemfile +1 -1
  10. data/Gemfile.lock +16 -16
  11. data/README.md +12 -2
  12. data/Rakefile +83 -4
  13. data/bin/validate +5 -0
  14. data/head_music.gemspec +1 -1
  15. data/lib/head_music/analysis/diatonic_interval/naming.rb +19 -17
  16. data/lib/head_music/analysis/key_finder/histograms.rb +55 -0
  17. data/lib/head_music/analysis/key_finder/key_path.rb +39 -0
  18. data/lib/head_music/analysis/key_finder.rb +105 -0
  19. data/lib/head_music/analysis/key_profile.rb +82 -0
  20. data/lib/head_music/analysis/key_region.rb +5 -0
  21. data/lib/head_music/analysis/line_of_fifths.rb +52 -0
  22. data/lib/head_music/analysis/note_input/flow_reader.rb +86 -0
  23. data/lib/head_music/analysis/note_input.rb +121 -0
  24. data/lib/head_music/analysis/pitch_speller/compactness.rb +53 -0
  25. data/lib/head_music/analysis/pitch_speller/key_center.rb +73 -0
  26. data/lib/head_music/analysis/pitch_speller/key_centers.rb +65 -0
  27. data/lib/head_music/analysis/pitch_speller/respelling.rb +75 -0
  28. data/lib/head_music/analysis/pitch_speller/sounding.rb +21 -0
  29. data/lib/head_music/analysis/pitch_speller/successors.rb +58 -0
  30. data/lib/head_music/analysis/pitch_speller.rb +107 -0
  31. data/lib/head_music/analysis/unspelled_note.rb +16 -0
  32. data/lib/head_music/content/bar.rb +69 -10
  33. data/lib/head_music/content/cantus_firmus/example.rb +2 -2
  34. data/lib/head_music/content/chain_link.rb +67 -0
  35. data/lib/head_music/content/comment.rb +1 -16
  36. data/lib/head_music/content/credits.rb +0 -4
  37. data/lib/head_music/content/dynamic_event.rb +35 -0
  38. data/lib/head_music/content/dynamic_events.rb +38 -0
  39. data/lib/head_music/content/flow/bars.rb +56 -0
  40. data/lib/head_music/content/flow/deserializer.rb +123 -25
  41. data/lib/head_music/content/flow/hash_deserializer.rb +29 -7
  42. data/lib/head_music/content/flow/hash_serializer.rb +47 -0
  43. data/lib/head_music/content/flow/navigation.rb +80 -0
  44. data/lib/head_music/content/flow/performance_order.rb +128 -0
  45. data/lib/head_music/content/flow/schema_validation.rb +53 -0
  46. data/lib/head_music/content/flow/schema_values.rb +67 -143
  47. data/lib/head_music/content/flow/timeline.rb +90 -58
  48. data/lib/head_music/content/flow/timeline_serializer.rb +58 -0
  49. data/lib/head_music/content/flow/timeline_spans.rb +68 -0
  50. data/lib/head_music/content/flow/timeline_tempos.rb +169 -0
  51. data/lib/head_music/content/flow/timeline_values.rb +139 -0
  52. data/lib/head_music/content/flow/v4_upgrade.rb +46 -0
  53. data/lib/head_music/content/flow/voice_event_values.rb +165 -0
  54. data/lib/head_music/content/flow.rb +114 -62
  55. data/lib/head_music/content/grace_note.rb +72 -0
  56. data/lib/head_music/content/jump.rb +53 -0
  57. data/lib/head_music/content/layout/realization.rb +20 -13
  58. data/lib/head_music/content/layout/rendering.rb +82 -0
  59. data/lib/head_music/content/layout.rb +3 -58
  60. data/lib/head_music/content/note.rb +9 -8
  61. data/lib/head_music/content/note_event.rb +154 -0
  62. data/lib/head_music/content/part.rb +39 -0
  63. data/lib/head_music/content/player.rb +1 -2
  64. data/lib/head_music/content/position.rb +47 -15
  65. data/lib/head_music/content/project.rb +57 -21
  66. data/lib/head_music/content/publication.rb +3 -27
  67. data/lib/head_music/content/rest_event.rb +45 -0
  68. data/lib/head_music/content/sound_resolver.rb +9 -3
  69. data/lib/head_music/content/span.rb +81 -0
  70. data/lib/head_music/content/spans.rb +50 -0
  71. data/lib/head_music/content/syllable.rb +3 -3
  72. data/lib/head_music/content/tuplet_group/bracketing.rb +77 -0
  73. data/lib/head_music/content/tuplet_group.rb +120 -0
  74. data/lib/head_music/content/tuplet_groups.rb +36 -0
  75. data/lib/head_music/content/voice/continuity.rb +15 -15
  76. data/lib/head_music/content/voice/dynamic_resolver.rb +49 -0
  77. data/lib/head_music/content/voice/melodic_line.rb +3 -3
  78. data/lib/head_music/content/voice/span_anchors.rb +37 -0
  79. data/lib/head_music/content/voice/staff_assignments.rb +56 -0
  80. data/lib/head_music/content/voice.rb +119 -92
  81. data/lib/head_music/content/voice_event/run.rb +50 -0
  82. data/lib/head_music/content/voice_event.rb +230 -0
  83. data/lib/head_music/content/voice_events.rb +60 -0
  84. data/lib/head_music/content/work.rb +3 -21
  85. data/lib/head_music/instruments/instrument.rb +12 -0
  86. data/lib/head_music/instruments/instruments.yml +115 -0
  87. data/lib/head_music/locales/de.yml +29 -0
  88. data/lib/head_music/locales/en.yml +51 -0
  89. data/lib/head_music/locales/es.yml +29 -0
  90. data/lib/head_music/locales/fr.yml +29 -0
  91. data/lib/head_music/locales/it.yml +29 -0
  92. data/lib/head_music/locales/ru.yml +29 -0
  93. data/lib/head_music/midi/bar_layout.rb +150 -0
  94. data/lib/head_music/midi/bar_pieces.rb +41 -0
  95. data/lib/head_music/midi/budget.rb +102 -0
  96. data/lib/head_music/midi/change_cursor.rb +30 -0
  97. data/lib/head_music/midi/channel_allocator.rb +47 -0
  98. data/lib/head_music/midi/chunk_reader.rb +72 -0
  99. data/lib/head_music/midi/conductor_track.rb +74 -0
  100. data/lib/head_music/midi/events.rb +112 -0
  101. data/lib/head_music/midi/flow_builder.rb +116 -0
  102. data/lib/head_music/midi/hairpin_ramps.rb +110 -0
  103. data/lib/head_music/midi/instrument_map.rb +77 -0
  104. data/lib/head_music/midi/key_changes.rb +45 -0
  105. data/lib/head_music/midi/key_overlaps.rb +48 -0
  106. data/lib/head_music/midi/level_marks.rb +61 -0
  107. data/lib/head_music/midi/lyric_attacher.rb +60 -0
  108. data/lib/head_music/midi/lyric_events.rb +44 -0
  109. data/lib/head_music/midi/meta_reader.rb +70 -0
  110. data/lib/head_music/midi/meter_changes.rb +67 -0
  111. data/lib/head_music/midi/note_pairing.rb +48 -0
  112. data/lib/head_music/midi/note_timing.rb +109 -0
  113. data/lib/head_music/midi/note_values.rb +99 -0
  114. data/lib/head_music/midi/onsets.rb +15 -0
  115. data/lib/head_music/midi/parser.rb +36 -0
  116. data/lib/head_music/midi/part_key.rb +18 -0
  117. data/lib/head_music/midi/part_reading.rb +72 -0
  118. data/lib/head_music/midi/part_source.rb +161 -0
  119. data/lib/head_music/midi/part_track.rb +115 -0
  120. data/lib/head_music/midi/performance_timeline.rb +80 -0
  121. data/lib/head_music/midi/preflight.rb +138 -0
  122. data/lib/head_music/midi/program_changes.rb +37 -0
  123. data/lib/head_music/midi/quantized_note.rb +5 -0
  124. data/lib/head_music/midi/quantizer.rb +106 -0
  125. data/lib/head_music/midi/ramp_turn.rb +26 -0
  126. data/lib/head_music/midi/reader.rb +110 -0
  127. data/lib/head_music/midi/smf.rb +41 -0
  128. data/lib/head_music/midi/sound_keys.rb +32 -0
  129. data/lib/head_music/midi/spelling.rb +74 -0
  130. data/lib/head_music/midi/tempo_changes.rb +121 -0
  131. data/lib/head_music/midi/tempo_inference.rb +143 -0
  132. data/lib/head_music/midi/tempo_steps.rb +112 -0
  133. data/lib/head_music/midi/tick_resolution.rb +44 -0
  134. data/lib/head_music/midi/timeline_reader.rb +105 -0
  135. data/lib/head_music/midi/track_reader.rb +128 -0
  136. data/lib/head_music/midi/velocity_resolver.rb +116 -0
  137. data/lib/head_music/midi/voice_separator.rb +100 -0
  138. data/lib/head_music/midi/writer.rb +64 -0
  139. data/lib/head_music/midi.rb +54 -0
  140. data/lib/head_music/notation/abc/bar_line_writer.rb +86 -0
  141. data/lib/head_music/notation/abc/bar_mark_reader.rb +75 -0
  142. data/lib/head_music/notation/abc/bar_writer.rb +144 -0
  143. data/lib/head_music/notation/abc/beam_adjacency.rb +34 -0
  144. data/lib/head_music/notation/abc/body_lexer.rb +20 -233
  145. data/lib/head_music/notation/abc/broken_rhythm.rb +41 -0
  146. data/lib/head_music/notation/abc/decoration.rb +37 -0
  147. data/lib/head_music/notation/abc/decoration_mapper.rb +102 -0
  148. data/lib/head_music/notation/abc/decoration_writer.rb +30 -0
  149. data/lib/head_music/notation/abc/duration_resolver.rb +5 -3
  150. data/lib/head_music/notation/abc/duration_writer.rb +20 -25
  151. data/lib/head_music/notation/abc/grace_reader.rb +38 -0
  152. data/lib/head_music/notation/abc/grace_scanner.rb +98 -0
  153. data/lib/head_music/notation/abc/grace_writer.rb +38 -0
  154. data/lib/head_music/notation/abc/hairpin_marks.rb +52 -0
  155. data/lib/head_music/notation/abc/header.rb +49 -19
  156. data/lib/head_music/notation/abc/invisible_rests.rb +38 -0
  157. data/lib/head_music/notation/abc/key_mapper.rb +6 -1
  158. data/lib/head_music/notation/abc/line_scanner.rb +251 -0
  159. data/lib/head_music/notation/abc/navigation_tagger.rb +86 -0
  160. data/lib/head_music/notation/abc/opening_fields.rb +63 -0
  161. data/lib/head_music/notation/abc/parser.rb +111 -110
  162. data/lib/head_music/notation/abc/pending_decorations.rb +127 -0
  163. data/lib/head_music/notation/abc/pending_graces.rb +30 -0
  164. data/lib/head_music/notation/abc/pending_note.rb +64 -0
  165. data/lib/head_music/notation/abc/pending_slurs.rb +68 -0
  166. data/lib/head_music/notation/abc/pending_tie.rb +39 -0
  167. data/lib/head_music/notation/abc/pitch_builder.rb +6 -1
  168. data/lib/head_music/notation/abc/pitch_writer.rb +17 -5
  169. data/lib/head_music/notation/abc/position_marks.rb +147 -0
  170. data/lib/head_music/notation/abc/preflight.rb +13 -0
  171. data/lib/head_music/notation/abc/render_plan.rb +35 -0
  172. data/lib/head_music/notation/abc/render_preflight.rb +71 -0
  173. data/lib/head_music/notation/abc/repeat_tagger.rb +31 -2
  174. data/lib/head_music/notation/abc/rest_writer.rb +50 -0
  175. data/lib/head_music/notation/abc/slur_writer.rb +106 -0
  176. data/lib/head_music/notation/abc/tempo_field.rb +72 -0
  177. data/lib/head_music/notation/abc/timeline_fields.rb +54 -0
  178. data/lib/head_music/notation/abc/token.rb +2 -2
  179. data/lib/head_music/notation/abc/tuplet_grouping.rb +104 -0
  180. data/lib/head_music/notation/abc/tuplet_marker.rb +30 -0
  181. data/lib/head_music/notation/abc/tuplet_writer.rb +68 -0
  182. data/lib/head_music/notation/abc/voice_overlay.rb +49 -0
  183. data/lib/head_music/notation/abc/voice_registry.rb +8 -2
  184. data/lib/head_music/notation/abc/voice_replay.rb +45 -0
  185. data/lib/head_music/notation/abc/voice_state.rb +250 -100
  186. data/lib/head_music/notation/abc/voice_tokens.rb +29 -0
  187. data/lib/head_music/notation/abc/voice_writer.rb +125 -0
  188. data/lib/head_music/notation/abc/writer.rb +53 -137
  189. data/lib/head_music/notation/abc.rb +1 -10
  190. data/lib/head_music/notation/bar_map.rb +186 -0
  191. data/lib/head_music/notation/bar_marks.rb +19 -0
  192. data/lib/head_music/notation/bar_splitter.rb +140 -0
  193. data/lib/head_music/notation/barline_placement.rb +15 -0
  194. data/lib/head_music/notation/beam_groups.rb +39 -0
  195. data/lib/head_music/notation/beam_reading.rb +20 -0
  196. data/lib/head_music/notation/chord_clashes.rb +20 -0
  197. data/lib/head_music/notation/clef_changes.rb +21 -0
  198. data/lib/head_music/notation/concert_transposition.rb +60 -0
  199. data/lib/head_music/notation/dotted_duration.rb +63 -23
  200. data/lib/head_music/notation/fermata_marks.rb +78 -0
  201. data/lib/head_music/notation/flow_timed_marks.rb +71 -0
  202. data/lib/head_music/notation/hairpin_segments.rb +22 -0
  203. data/lib/head_music/notation/kern/bar_clock.rb +195 -0
  204. data/lib/head_music/notation/kern/barline_reader.rb +37 -0
  205. data/lib/head_music/notation/kern/chord_token.rb +65 -0
  206. data/lib/head_music/notation/kern/citation_reader.rb +69 -0
  207. data/lib/head_music/notation/kern/clef_codes.rb +33 -0
  208. data/lib/head_music/notation/kern/data_rows.rb +101 -0
  209. data/lib/head_music/notation/kern/document.rb +129 -0
  210. data/lib/head_music/notation/kern/duration_reader.rb +79 -0
  211. data/lib/head_music/notation/kern/duration_writer.rb +38 -0
  212. data/lib/head_music/notation/kern/dynamic_fields.rb +93 -0
  213. data/lib/head_music/notation/kern/dynamic_placer.rb +131 -0
  214. data/lib/head_music/notation/kern/dynamic_reader.rb +88 -0
  215. data/lib/head_music/notation/kern/flow_builder.rb +140 -0
  216. data/lib/head_music/notation/kern/grace_tokens.rb +42 -0
  217. data/lib/head_music/notation/kern/instrument_codes.rb +62 -0
  218. data/lib/head_music/notation/kern/interpretation_reader.rb +43 -0
  219. data/lib/head_music/notation/kern/interpretation_rows.rb +119 -0
  220. data/lib/head_music/notation/kern/key_reader.rb +73 -0
  221. data/lib/head_music/notation/kern/layer.rb +158 -0
  222. data/lib/head_music/notation/kern/lexer.rb +89 -0
  223. data/lib/head_music/notation/kern/lyric_reader.rb +84 -0
  224. data/lib/head_music/notation/kern/mark_codes.rb +48 -0
  225. data/lib/head_music/notation/kern/mark_offset.rb +16 -0
  226. data/lib/head_music/notation/kern/melismas.rb +35 -0
  227. data/lib/head_music/notation/kern/meter_reader.rb +60 -0
  228. data/lib/head_music/notation/kern/parser.rb +34 -0
  229. data/lib/head_music/notation/kern/part_grouping.rb +70 -0
  230. data/lib/head_music/notation/kern/part_roster.rb +69 -0
  231. data/lib/head_music/notation/kern/pitch_reader.rb +32 -0
  232. data/lib/head_music/notation/kern/pitch_writer.rb +21 -0
  233. data/lib/head_music/notation/kern/preflight.rb +84 -0
  234. data/lib/head_music/notation/kern/record.rb +26 -0
  235. data/lib/head_music/notation/kern/reference_records.rb +64 -0
  236. data/lib/head_music/notation/kern/render_plan.rb +19 -0
  237. data/lib/head_music/notation/kern/row_offsets.rb +30 -0
  238. data/lib/head_music/notation/kern/span_marks.rb +80 -0
  239. data/lib/head_music/notation/kern/spine_changes.rb +78 -0
  240. data/lib/head_music/notation/kern/spine_columns.rb +86 -0
  241. data/lib/head_music/notation/kern/spine_layout.rb +151 -0
  242. data/lib/head_music/notation/kern/spine_tags.rb +30 -0
  243. data/lib/head_music/notation/kern/spine_tokens.rb +183 -0
  244. data/lib/head_music/notation/kern/spine_voices.rb +127 -0
  245. data/lib/head_music/notation/kern/tempo_reader.rb +49 -0
  246. data/lib/head_music/notation/kern/timeline_reader.rb +169 -0
  247. data/lib/head_music/notation/kern/token_reader.rb +198 -0
  248. data/lib/head_music/notation/kern/tuplet_groups.rb +93 -0
  249. data/lib/head_music/notation/kern/voice_cursor.rb +115 -0
  250. data/lib/head_music/notation/kern/writer.rb +67 -0
  251. data/lib/head_music/notation/kern/written_bars.rb +151 -0
  252. data/lib/head_music/notation/kern.rb +27 -0
  253. data/lib/head_music/notation/lily_pond/bar_commands.rb +87 -0
  254. data/lib/head_music/notation/lily_pond/bar_mark_placer.rb +82 -0
  255. data/lib/head_music/notation/lily_pond/bar_mark_reader.rb +134 -0
  256. data/lib/head_music/notation/lily_pond/bar_mark_writer.rb +65 -0
  257. data/lib/head_music/notation/lily_pond/chord_reader.rb +34 -0
  258. data/lib/head_music/notation/lily_pond/context_reader.rb +58 -12
  259. data/lib/head_music/notation/lily_pond/document.rb +32 -1
  260. data/lib/head_music/notation/lily_pond/duration_reader.rb +41 -3
  261. data/lib/head_music/notation/lily_pond/duration_writer.rb +3 -3
  262. data/lib/head_music/notation/lily_pond/dynamics_item_reader.rb +47 -0
  263. data/lib/head_music/notation/lily_pond/event_placer.rb +191 -0
  264. data/lib/head_music/notation/lily_pond/flow_builder.rb +20 -95
  265. data/lib/head_music/notation/lily_pond/grace_reader.rb +132 -0
  266. data/lib/head_music/notation/lily_pond/grace_writer.rb +35 -0
  267. data/lib/head_music/notation/lily_pond/group_reader.rb +51 -0
  268. data/lib/head_music/notation/lily_pond/hairpin_marks.rb +40 -0
  269. data/lib/head_music/notation/lily_pond/key_mapper.rb +2 -11
  270. data/lib/head_music/notation/lily_pond/lexer.rb +92 -12
  271. data/lib/head_music/notation/lily_pond/mark_reader.rb +184 -0
  272. data/lib/head_music/notation/lily_pond/mark_writer.rb +27 -0
  273. data/lib/head_music/notation/lily_pond/music_item_reader.rb +85 -57
  274. data/lib/head_music/notation/lily_pond/music_reader.rb +29 -33
  275. data/lib/head_music/notation/lily_pond/part_dynamics.rb +53 -0
  276. data/lib/head_music/notation/lily_pond/part_layout.rb +71 -0
  277. data/lib/head_music/notation/lily_pond/pending_tuplets.rb +47 -0
  278. data/lib/head_music/notation/lily_pond/pitch_writer.rb +18 -0
  279. data/lib/head_music/notation/lily_pond/preflight.rb +27 -20
  280. data/lib/head_music/notation/lily_pond/render_plan.rb +94 -24
  281. data/lib/head_music/notation/lily_pond/segment_tokens.rb +135 -0
  282. data/lib/head_music/notation/lily_pond/setting_reader.rb +86 -0
  283. data/lib/head_music/notation/lily_pond/spacer_run.rb +43 -0
  284. data/lib/head_music/notation/lily_pond/spacers.rb +58 -0
  285. data/lib/head_music/notation/lily_pond/span_marks.rb +91 -0
  286. data/lib/head_music/notation/lily_pond/span_pairing.rb +35 -0
  287. data/lib/head_music/notation/lily_pond/tempo_reader.rb +46 -0
  288. data/lib/head_music/notation/lily_pond/tempo_writer.rb +33 -0
  289. data/lib/head_music/notation/lily_pond/time_signature_writer.rb +42 -0
  290. data/lib/head_music/notation/lily_pond/timed_marks.rb +158 -0
  291. data/lib/head_music/notation/lily_pond/token.rb +8 -2
  292. data/lib/head_music/notation/lily_pond/token_cursor.rb +55 -2
  293. data/lib/head_music/notation/lily_pond/tuplet_reader.rb +56 -0
  294. data/lib/head_music/notation/lily_pond/voice_context.rb +21 -9
  295. data/lib/head_music/notation/lily_pond/voice_item_reader.rb +86 -0
  296. data/lib/head_music/notation/lily_pond/voice_stream.rb +167 -31
  297. data/lib/head_music/notation/lily_pond/voice_writer.rb +74 -24
  298. data/lib/head_music/notation/lily_pond/writer.rb +30 -15
  299. data/lib/head_music/notation/lily_pond.rb +4 -7
  300. data/lib/head_music/notation/mark_reading.rb +16 -0
  301. data/lib/head_music/notation/mei/accidental_plan.rb +94 -0
  302. data/lib/head_music/notation/mei/accidental_reading.rb +23 -0
  303. data/lib/head_music/notation/mei/artic_attributes.rb +36 -0
  304. data/lib/head_music/notation/mei/artic_reader.rb +41 -0
  305. data/lib/head_music/notation/mei/attached_marks.rb +70 -0
  306. data/lib/head_music/notation/mei/attribute_values.rb +245 -0
  307. data/lib/head_music/notation/mei/bar_marks.rb +52 -0
  308. data/lib/head_music/notation/mei/barline_reader.rb +101 -0
  309. data/lib/head_music/notation/mei/beam_containers.rb +56 -0
  310. data/lib/head_music/notation/mei/beam_reader.rb +79 -0
  311. data/lib/head_music/notation/mei/clef_reading.rb +34 -0
  312. data/lib/head_music/notation/mei/container_spans.rb +15 -0
  313. data/lib/head_music/notation/mei/control_anchors.rb +194 -0
  314. data/lib/head_music/notation/mei/copy_expansion.rb +113 -0
  315. data/lib/head_music/notation/mei/decimals.rb +29 -0
  316. data/lib/head_music/notation/mei/document.rb +138 -0
  317. data/lib/head_music/notation/mei/downbeat_tempo.rb +41 -0
  318. data/lib/head_music/notation/mei/duration_reading.rb +49 -0
  319. data/lib/head_music/notation/mei/durations.rb +31 -0
  320. data/lib/head_music/notation/mei/dynam_reader.rb +51 -0
  321. data/lib/head_music/notation/mei/editorial.rb +122 -0
  322. data/lib/head_music/notation/mei/ending_passes.rb +24 -0
  323. data/lib/head_music/notation/mei/event_records.rb +61 -0
  324. data/lib/head_music/notation/mei/event_writer.rb +102 -0
  325. data/lib/head_music/notation/mei/fermata_controls.rb +39 -0
  326. data/lib/head_music/notation/mei/fermata_reader.rb +43 -0
  327. data/lib/head_music/notation/mei/flow_builder.rb +62 -0
  328. data/lib/head_music/notation/mei/hairpin_reader.rb +38 -0
  329. data/lib/head_music/notation/mei/head_writer.rb +85 -0
  330. data/lib/head_music/notation/mei/id_index.rb +86 -0
  331. data/lib/head_music/notation/mei/identity_reader.rb +199 -0
  332. data/lib/head_music/notation/mei/ids.rb +70 -0
  333. data/lib/head_music/notation/mei/layer_containers.rb +94 -0
  334. data/lib/head_music/notation/mei/layer_reader.rb +211 -0
  335. data/lib/head_music/notation/mei/layer_signs.rb +37 -0
  336. data/lib/head_music/notation/mei/layer_tree.rb +40 -0
  337. data/lib/head_music/notation/mei/lyric_reader.rb +116 -0
  338. data/lib/head_music/notation/mei/lyric_writer.rb +72 -0
  339. data/lib/head_music/notation/mei/mark_reader.rb +52 -0
  340. data/lib/head_music/notation/mei/mark_readers.rb +18 -0
  341. data/lib/head_music/notation/mei/mark_records.rb +122 -0
  342. data/lib/head_music/notation/mei/marking_events.rb +89 -0
  343. data/lib/head_music/notation/mei/markup.rb +37 -0
  344. data/lib/head_music/notation/mei/measure_clefs.rb +38 -0
  345. data/lib/head_music/notation/mei/measure_contents.rb +87 -0
  346. data/lib/head_music/notation/mei/measure_length.rb +30 -0
  347. data/lib/head_music/notation/mei/measure_writer.rb +79 -0
  348. data/lib/head_music/notation/mei/navigation_events.rb +59 -0
  349. data/lib/head_music/notation/mei/navigation_reader.rb +71 -0
  350. data/lib/head_music/notation/mei/onset_index.rb +49 -0
  351. data/lib/head_music/notation/mei/open_spaces.rb +43 -0
  352. data/lib/head_music/notation/mei/opening_values.rb +44 -0
  353. data/lib/head_music/notation/mei/ornament_reader.rb +47 -0
  354. data/lib/head_music/notation/mei/parser.rb +45 -0
  355. data/lib/head_music/notation/mei/part_layers.rb +58 -0
  356. data/lib/head_music/notation/mei/part_reading.rb +69 -0
  357. data/lib/head_music/notation/mei/part_roster.rb +54 -0
  358. data/lib/head_music/notation/mei/part_spelling.rb +55 -0
  359. data/lib/head_music/notation/mei/pitch_reading.rb +33 -0
  360. data/lib/head_music/notation/mei/preflight.rb +63 -0
  361. data/lib/head_music/notation/mei/render_plan.rb +154 -0
  362. data/lib/head_music/notation/mei/score_def_values.rb +84 -0
  363. data/lib/head_music/notation/mei/score_def_writer.rb +138 -0
  364. data/lib/head_music/notation/mei/score_reader.rb +191 -0
  365. data/lib/head_music/notation/mei/section_writer.rb +48 -0
  366. data/lib/head_music/notation/mei/slur_reader.rb +71 -0
  367. data/lib/head_music/notation/mei/staff_groups.rb +132 -0
  368. data/lib/head_music/notation/mei/staff_numbering.rb +62 -0
  369. data/lib/head_music/notation/mei/staff_state.rb +207 -0
  370. data/lib/head_music/notation/mei/tempo_controls.rb +103 -0
  371. data/lib/head_music/notation/mei/tempo_mark_reader.rb +79 -0
  372. data/lib/head_music/notation/mei/tempo_reading.rb +40 -0
  373. data/lib/head_music/notation/mei/tie_controls.rb +145 -0
  374. data/lib/head_music/notation/mei/timeline_changes.rb +119 -0
  375. data/lib/head_music/notation/mei/timeline_reader.rb +30 -0
  376. data/lib/head_music/notation/mei/tstamp.rb +56 -0
  377. data/lib/head_music/notation/mei/tuplet_reader.rb +35 -0
  378. data/lib/head_music/notation/mei/tuplet_spans.rb +173 -0
  379. data/lib/head_music/notation/mei/voice_assignment.rb +109 -0
  380. data/lib/head_music/notation/mei/writer.rb +59 -0
  381. data/lib/head_music/notation/mei/written_items.rb +112 -0
  382. data/lib/head_music/notation/mei/xml_reading.rb +5 -0
  383. data/lib/head_music/notation/mei.rb +69 -0
  384. data/lib/head_music/notation/melisma_fill.rb +22 -0
  385. data/lib/head_music/notation/music_xml/attributes_reader.rb +103 -0
  386. data/lib/head_music/notation/music_xml/attributes_writer.rb +62 -41
  387. data/lib/head_music/notation/music_xml/barline_reader.rb +121 -0
  388. data/lib/head_music/notation/music_xml/barline_writer.rb +102 -0
  389. data/lib/head_music/notation/music_xml/beam_grouper.rb +10 -40
  390. data/lib/head_music/notation/music_xml/beam_reader.rb +46 -0
  391. data/lib/head_music/notation/music_xml/clef_reader.rb +20 -0
  392. data/lib/head_music/notation/music_xml/direction_marks.rb +51 -0
  393. data/lib/head_music/notation/music_xml/direction_plan.rb +135 -0
  394. data/lib/head_music/notation/music_xml/direction_reader.rb +142 -0
  395. data/lib/head_music/notation/music_xml/direction_writer.rb +73 -0
  396. data/lib/head_music/notation/music_xml/divisions.rb +45 -10
  397. data/lib/head_music/notation/music_xml/document.rb +77 -0
  398. data/lib/head_music/notation/music_xml/duration_writer.rb +21 -6
  399. data/lib/head_music/notation/music_xml/fermata_reader.rb +57 -0
  400. data/lib/head_music/notation/music_xml/flow_builder.rb +47 -0
  401. data/lib/head_music/notation/music_xml/grace_writer.rb +85 -0
  402. data/lib/head_music/notation/music_xml/identity_reader.rb +81 -0
  403. data/lib/head_music/notation/music_xml/instrument_sounds.rb +23 -0
  404. data/lib/head_music/notation/music_xml/instrument_sounds.yml +123 -0
  405. data/lib/head_music/notation/music_xml/key_mapper.rb +30 -0
  406. data/lib/head_music/notation/music_xml/lyric_reader.rb +116 -0
  407. data/lib/head_music/notation/music_xml/lyric_writer.rb +50 -29
  408. data/lib/head_music/notation/music_xml/mark_reader.rb +28 -0
  409. data/lib/head_music/notation/music_xml/mark_readers.rb +25 -0
  410. data/lib/head_music/notation/music_xml/mark_records.rb +124 -0
  411. data/lib/head_music/notation/music_xml/marking_elements.rb +65 -0
  412. data/lib/head_music/notation/music_xml/marking_reader.rb +76 -0
  413. data/lib/head_music/notation/music_xml/navigation_reader.rb +91 -0
  414. data/lib/head_music/notation/music_xml/navigation_writer.rb +60 -0
  415. data/lib/head_music/notation/music_xml/note_lines.rb +39 -0
  416. data/lib/head_music/notation/music_xml/note_reader.rb +100 -0
  417. data/lib/head_music/notation/music_xml/note_writer.rb +104 -70
  418. data/lib/head_music/notation/music_xml/parse_preflight.rb +11 -0
  419. data/lib/head_music/notation/music_xml/parser.rb +39 -0
  420. data/lib/head_music/notation/music_xml/part_durations.rb +83 -0
  421. data/lib/head_music/notation/music_xml/part_reader.rb +152 -0
  422. data/lib/head_music/notation/music_xml/part_roster.rb +44 -0
  423. data/lib/head_music/notation/music_xml/part_staves.rb +47 -0
  424. data/lib/head_music/notation/music_xml/part_writer.rb +119 -0
  425. data/lib/head_music/notation/music_xml/pitch_reader.rb +20 -0
  426. data/lib/head_music/notation/music_xml/pitch_writer.rb +13 -0
  427. data/lib/head_music/notation/music_xml/preflight.rb +14 -19
  428. data/lib/head_music/notation/music_xml/render_plan.rb +57 -49
  429. data/lib/head_music/notation/music_xml/slur_numbers.rb +98 -0
  430. data/lib/head_music/notation/music_xml/slur_reader.rb +69 -0
  431. data/lib/head_music/notation/music_xml/tempo_direction.rb +91 -0
  432. data/lib/head_music/notation/music_xml/tempo_reader.rb +127 -0
  433. data/lib/head_music/notation/music_xml/tempo_writer.rb +97 -0
  434. data/lib/head_music/notation/music_xml/timeline_reader.rb +37 -0
  435. data/lib/head_music/notation/music_xml/tuplet_marks.rb +76 -0
  436. data/lib/head_music/notation/music_xml/tuplet_reader.rb +86 -0
  437. data/lib/head_music/notation/music_xml/voice_tracks.rb +129 -0
  438. data/lib/head_music/notation/music_xml/wedge_numbers.rb +46 -0
  439. data/lib/head_music/notation/music_xml/writer.rb +26 -85
  440. data/lib/head_music/notation/music_xml/written_timing.rb +89 -0
  441. data/lib/head_music/notation/music_xml/xml_reading.rb +4 -0
  442. data/lib/head_music/notation/music_xml.rb +33 -1
  443. data/lib/head_music/notation/parse_preflight.rb +85 -0
  444. data/lib/head_music/notation/pitch_spelling.rb +30 -0
  445. data/lib/head_music/notation/preflight_checks.rb +117 -12
  446. data/lib/head_music/notation/render_plan.rb +47 -4
  447. data/lib/head_music/notation/span_ends.rb +11 -0
  448. data/lib/head_music/notation/tempo_mark_reading.rb +130 -0
  449. data/lib/head_music/notation/tempo_marks.rb +95 -0
  450. data/lib/head_music/notation/tie_folding.rb +90 -0
  451. data/lib/head_music/notation/timeline_reading.rb +67 -0
  452. data/lib/head_music/notation/tuplet_brackets.rb +91 -0
  453. data/lib/head_music/notation/voice_event_validation.rb +34 -0
  454. data/lib/head_music/notation/voice_placer.rb +94 -0
  455. data/lib/head_music/notation/voice_track.rb +133 -0
  456. data/lib/head_music/notation/written_links.rb +37 -0
  457. data/lib/head_music/notation/xml_reading.rb +85 -0
  458. data/lib/head_music/notation/xml_text.rb +22 -0
  459. data/lib/head_music/notation.rb +40 -0
  460. data/lib/head_music/rudiment/articulation.rb +10 -0
  461. data/lib/head_music/rudiment/articulations.yml +6 -0
  462. data/lib/head_music/rudiment/clef.rb +6 -0
  463. data/lib/head_music/rudiment/duration_comparison.rb +18 -0
  464. data/lib/head_music/rudiment/dynamic.rb +52 -0
  465. data/lib/head_music/rudiment/dynamics.yml +27 -0
  466. data/lib/head_music/rudiment/key.rb +10 -6
  467. data/lib/head_music/rudiment/keyed_catalog.rb +73 -0
  468. data/lib/head_music/rudiment/meter.rb +31 -4
  469. data/lib/head_music/rudiment/mode.rb +9 -4
  470. data/lib/head_music/rudiment/note.rb +25 -23
  471. data/lib/head_music/rudiment/ornament.rb +9 -0
  472. data/lib/head_music/rudiment/ornaments.yml +13 -0
  473. data/lib/head_music/rudiment/pitch.rb +1 -0
  474. data/lib/head_music/rudiment/quality.rb +11 -4
  475. data/lib/head_music/rudiment/rest.rb +9 -9
  476. data/lib/head_music/rudiment/rhythm.rb +2 -0
  477. data/lib/head_music/rudiment/rhythmic_duration.rb +114 -0
  478. data/lib/head_music/rudiment/rhythmic_element.rb +10 -11
  479. data/lib/head_music/rudiment/rhythmic_unit.rb +6 -4
  480. data/lib/head_music/rudiment/rhythmic_value/name_reader.rb +67 -0
  481. data/lib/head_music/rudiment/rhythmic_value.rb +73 -78
  482. data/lib/head_music/rudiment/soundable.rb +11 -0
  483. data/lib/head_music/rudiment/span_kind.rb +52 -0
  484. data/lib/head_music/rudiment/span_kinds.yml +31 -0
  485. data/lib/head_music/rudiment/tempo.rb +45 -14
  486. data/lib/head_music/rudiment/tuplet_ratio.rb +84 -0
  487. data/lib/head_music/rudiment/unpitched_note.rb +18 -18
  488. data/lib/head_music/rudiment/unpitched_sound.rb +2 -0
  489. data/lib/head_music/style/guideline/harmonic_context.rb +15 -11
  490. data/lib/head_music/style/guideline/voice_context.rb +8 -0
  491. data/lib/head_music/style/guideline/wording.rb +5 -0
  492. data/lib/head_music/style/guideline.rb +3 -3
  493. data/lib/head_music/style/guidelines/allow_fifth_species_rhythmic_values.rb +11 -6
  494. data/lib/head_music/style/guidelines/allow_whole_half_quarter_notes.rb +1 -1
  495. data/lib/head_music/style/guidelines/always_move.rb +3 -1
  496. data/lib/head_music/style/guidelines/contoured/direction_reading.rb +67 -0
  497. data/lib/head_music/style/guidelines/contoured/turn_reading.rb +66 -0
  498. data/lib/head_music/style/guidelines/contoured/wave_reading.rb +87 -0
  499. data/lib/head_music/style/guidelines/contoured.rb +56 -77
  500. data/lib/head_music/style/guidelines/dissonance_figure_detection.rb +35 -58
  501. data/lib/head_music/style/guidelines/embellished_suspension_treatment.rb +2 -2
  502. data/lib/head_music/style/guidelines/first_bar_entry.rb +5 -5
  503. data/lib/head_music/style/guidelines/first_bar_half_notes.rb +2 -2
  504. data/lib/head_music/style/guidelines/first_bar_quarter_notes.rb +2 -2
  505. data/lib/head_music/style/guidelines/first_bar_whole_note.rb +2 -2
  506. data/lib/head_music/style/guidelines/florid_dissonance_treatment.rb +1 -5
  507. data/lib/head_music/style/guidelines/limit_octave_leaps.rb +1 -4
  508. data/lib/head_music/style/guidelines/maximum_notes.rb +1 -6
  509. data/lib/head_music/style/guidelines/minimum_melodic_intervals.rb +2 -9
  510. data/lib/head_music/style/guidelines/minimum_notes.rb +1 -8
  511. data/lib/head_music/style/guidelines/minimum_threshold.rb +3 -1
  512. data/lib/head_music/style/guidelines/mix_species_textures.rb +3 -1
  513. data/lib/head_music/style/guidelines/no_parallel_perfect.rb +7 -2
  514. data/lib/head_music/style/guidelines/no_parallel_perfect_across_barline.rb +5 -36
  515. data/lib/head_music/style/guidelines/no_rests.rb +1 -1
  516. data/lib/head_music/style/guidelines/no_rests_after_note.rb +2 -2
  517. data/lib/head_music/style/guidelines/note_count_per_bar.rb +5 -7
  518. data/lib/head_music/style/guidelines/note_fills_final_bar.rb +1 -1
  519. data/lib/head_music/style/guidelines/notes_same_length.rb +16 -16
  520. data/lib/head_music/style/guidelines/prefer_long_before_short.rb +4 -2
  521. data/lib/head_music/style/guidelines/set_against_another_voice.rb +2 -2
  522. data/lib/head_music/style/guidelines/suspension_treatment.rb +3 -21
  523. data/lib/head_music/style/guidelines/weak_beat_dissonance_treatment.rb +0 -4
  524. data/lib/head_music/style/guides/fifth_species_melody.rb +3 -6
  525. data/lib/head_music/style/guides/first_species_melody.rb +2 -8
  526. data/lib/head_music/style/guides/first_three_species_melody.rb +2 -8
  527. data/lib/head_music/style/guides/fourth_species_melody.rb +1 -5
  528. data/lib/head_music/style/guides/second_species_melody.rb +1 -5
  529. data/lib/head_music/style/guides/species_melody.rb +10 -2
  530. data/lib/head_music/style/guides/third_species_melody.rb +1 -5
  531. data/lib/head_music/style/guides/third_species_triple_meter_melody.rb +1 -5
  532. data/lib/head_music/style/mark.rb +15 -15
  533. data/lib/head_music/time/a_tempo.rb +23 -0
  534. data/lib/head_music/time/change_map.rb +46 -0
  535. data/lib/head_music/time/conductor.rb +27 -2
  536. data/lib/head_music/time/event_map.rb +16 -9
  537. data/lib/head_music/time/fermata.rb +20 -0
  538. data/lib/head_music/time/key_signature_event.rb +30 -1
  539. data/lib/head_music/time/meter_map.rb +7 -27
  540. data/lib/head_music/time/meter_scale.rb +64 -0
  541. data/lib/head_music/time/musical_time_converter.rb +25 -78
  542. data/lib/head_music/time/tempo_event.rb +11 -4
  543. data/lib/head_music/time/tempo_map.rb +8 -34
  544. data/lib/head_music/time/tempo_piece.rb +69 -0
  545. data/lib/head_music/time/tempo_pieces.rb +112 -0
  546. data/lib/head_music/time/tempo_ramp.rb +34 -0
  547. data/lib/head_music/time.rb +8 -1
  548. data/lib/head_music/version.rb +1 -1
  549. data/lib/head_music.rb +63 -3
  550. data/references/content-schema.md +52 -28
  551. data/references/fourth-species-counterpoint.md +21 -21
  552. data/references/wemi.md +1 -1
  553. data/user-stories/done/abc-voices-and-meter-changes.md +274 -0
  554. data/user-stories/done/articulations-ornaments-dynamics.md +386 -0
  555. data/user-stories/done/bar-markings.md +296 -0
  556. data/user-stories/done/expand-playing-techniques.md +10 -1
  557. data/user-stories/done/grade-contour-misses-by-degree.md +634 -0
  558. data/user-stories/done/hairpins.md +229 -0
  559. data/user-stories/done/humdrum-kern-import-and-export.md +508 -0
  560. data/user-stories/done/instrument-variant.md +10 -1
  561. data/user-stories/done/key-and-spelling-inference.md +480 -0
  562. data/user-stories/done/lyric-extenders.md +203 -0
  563. data/user-stories/done/markings-review-follow-ups.md +183 -0
  564. data/user-stories/done/mei-export.md +450 -0
  565. data/user-stories/done/mei-import.md +647 -0
  566. data/user-stories/done/midi-export.md +448 -0
  567. data/user-stories/done/midi-import.md +664 -0
  568. data/user-stories/done/musicxml-import.md +613 -0
  569. data/user-stories/done/place-dynamics-where-no-note-starts.md +145 -0
  570. data/user-stories/done/require-the-species-rhythm.md +1 -1
  571. data/user-stories/done/rhythmic-duration.md +220 -0
  572. data/user-stories/done/spans-across-notes.md +283 -0
  573. data/user-stories/done/superclass-for-note.md +11 -0
  574. data/user-stories/done/timeline-expressions.md +535 -0
  575. data/user-stories/done/tuplets-and-grace-notes.md +549 -0
  576. data/user-stories/epics/organizing-content.md +1 -1
  577. data/user-stories/index.html +85 -13
  578. metadata +424 -14
  579. data/lib/head_music/content/placement.rb +0 -175
  580. data/lib/head_music/notation/music_xml/xml_text.rb +0 -21
  581. data/lib/head_music/notation/placement_validation.rb +0 -19
  582. data/user-stories/backlog/lower-the-contour-floor.md +0 -69
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f716235926455127be636b1b1c2c575a4ce7f0f4b41f7952620dfda908a8671
4
- data.tar.gz: 378c4749d7fbcc0ac7168f086cae549126d4fd8e648e5703d4311a2a9b63f5ad
3
+ metadata.gz: 157433aaffa6dc30444fe1d12511bbf3dbc1877a8384a19fb82b30b99037174d
4
+ data.tar.gz: 3168adc5bd7500b9b4e07e3b10f718e421ec5ff2419cb647e814ffe6e41d1561
5
5
  SHA512:
6
- metadata.gz: 433e4a30ce9a523d69e194b37b72f3cc5499c91d6e3605c1c901ed63ad9d2c308ae9c891876eb0cb9beef288450cc59287fbabb63e0c3115a160e8d111d032ad
7
- data.tar.gz: 925aa64aabc77e319868ab18d71964bea6ee1aae5a02477a827283b9ce01c13eb1203b1d0ff87ae361f41c41bbc02965446acc77caa1f6a1c451fe227a718806
6
+ metadata.gz: 63e33e764528684fdff03cd3473653f64c61d136eebff7a30fe408cceabb190e13594e96d55a33e991757547ce0ab4955c1b64bc24f2b6e02407009cdac57d82
7
+ data.tar.gz: 42e936c6dc431e27ebd16a6af38bc4fc1545a599bdd84bfa3c01ac204cb7b1bbedbe639f370f93a0abf1ef33d77a5bf95c9a1f3468aa5df7a812d77ce9fa303b
@@ -48,8 +48,28 @@ convention so far:
48
48
  sentence naming the bump and what a consumer must do to upgrade.
49
49
  - Set `VERSION` in `lib/head_music/version.rb`.
50
50
  - Run `bundle install` so `Gemfile.lock` records the new version.
51
+ - Bump the fixtures that record the gem's version. The MEI goldens write it
52
+ as `<application version="X.Y.Z">` and the MusicXML renders as
53
+ `<software>head_music X.Y.Z</software>`, and specs compare them to a fresh
54
+ render, so a bump left there fails the suite here and in the release
55
+ workflow after the tag is pushed. Find them with
56
+ `grep -rlF 'OLD.VERSION' spec/fixtures` and replace only the head_music
57
+ version in those two forms:
58
+
59
+ ```bash
60
+ grep -rlF 'OLD.VERSION' spec/fixtures | xargs sed -i '' \
61
+ -e 's/<application version="OLD\.VERSION">/<application version="X.Y.Z">/' \
62
+ -e 's/head_music OLD\.VERSION/head_music X.Y.Z/'
63
+ ```
64
+
65
+ Do not run `rake music_xml:musescore_fixtures` for this: it reconverts
66
+ every fixture through MuseScore and churns far more than the version.
67
+ Check that every changed fixture line is a version line, and that the
68
+ grep now finds nothing. A match left over is a new format embedding the
69
+ version; bump it the same way and add its form here.
51
70
  - Run `bundle exec rake` and `bundle exec rubocop` again.
52
- - Show the user the diff: CHANGELOG, version file, lockfile, nothing else.
71
+ - Show the user the diff: CHANGELOG, version file, lockfile, and the
72
+ fixtures' version lines, nothing else.
53
73
 
54
74
  ## 4. Commit and push
55
75
 
@@ -125,6 +125,43 @@ jobs:
125
125
  - name: Run tests
126
126
  run: bundle exec rspec spec/head_music/notation/lily_pond_round_trip_spec.rb spec/head_music/notation/lily_pond
127
127
 
128
+ # The schema specs skip unless a local schema is named, and the schemas are
129
+ # not committed, so without this job nothing proves the MEI and MusicXML
130
+ # documents are valid. Both are pinned; the MusicXML imports are pointed at
131
+ # their local copies because the specs validate with --nonet.
132
+ test-schemas:
133
+ name: test (against the MEI and MusicXML schemas)
134
+ runs-on: ubuntu-latest
135
+ timeout-minutes: 10
136
+ env:
137
+ MEI_SCHEMA: ${{ github.workspace }}/schemas/mei-CMN.rng
138
+ MUSICXML_XSD: ${{ github.workspace }}/schemas/musicxml.xsd
139
+ steps:
140
+ - uses: actions/checkout@v7
141
+
142
+ - name: Install xmllint
143
+ run: |
144
+ sudo apt-get update
145
+ sudo apt-get install -y libxml2-utils
146
+
147
+ - name: Fetch the schemas
148
+ run: |
149
+ mkdir -p schemas
150
+ curl -sSfL -o schemas/mei-CMN.rng https://raw.githubusercontent.com/music-encoding/schema/bb4fed011f52088f8c2b9bdd4ff9674e75469aa4/5.1/mei-CMN.rng
151
+ for file in musicxml.xsd xlink.xsd xml.xsd; do
152
+ curl -sSfL -o "schemas/$file" "https://raw.githubusercontent.com/w3c/musicxml/799e2defb2ece0ae7bafe08dcbcac25b2c631d53/schema/$file"
153
+ done
154
+ sed -i 's#http://www.musicxml.org/xsd/##' schemas/musicxml.xsd
155
+
156
+ - name: Set up Ruby
157
+ uses: ruby/setup-ruby@v1
158
+ with:
159
+ ruby-version: .ruby-version
160
+ bundler-cache: true
161
+
162
+ - name: Run tests
163
+ run: bundle exec rspec spec/head_music/notation/mei_schema_spec.rb spec/head_music/notation/music_xml_schema_spec.rb
164
+
128
165
  lint:
129
166
  runs-on: ubuntu-latest
130
167
  timeout-minutes: 5
data/.gitignore CHANGED
@@ -26,3 +26,4 @@
26
26
 
27
27
  # Claude settings
28
28
  **/.claude/settings.local.json
29
+ .claude/agent-memory/
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.3.0
1
+ 3.3.11
data/.rubycritic.yml ADDED
@@ -0,0 +1,7 @@
1
+ paths:
2
+ - lib
3
+ formats:
4
+ - html
5
+ - json
6
+ minimum_score: 83
7
+ no_browser: true
data/CHANGELOG.md CHANGED
@@ -7,6 +7,264 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [23.0.0] - 2026-10-30
11
+
12
+ Eighteen stories since 22.0.0, most of them about getting music in and out. head_music now reads and writes MusicXML, MEI, MIDI, and Humdrum **kern beside ABC and LilyPond, infers a key and respells pitches for formats that carry none, and a flow holds the articulations, ornaments, dynamics, hairpins, bar markings, spans, tuplets, grace notes, tempo and meter changes, and lyric extenders those formats write. Underneath, a voice's contents are now voice events, notes and rests of their own classes, and a tied length is a `RhythmicDuration` rather than a chain hung off a `RhythmicValue`.
13
+
14
+ **This is a major release**, because public classes and methods are renamed or removed and the serialization schema goes to 5. A consumer that only reads and writes notation through the format readers and writers, and stores no schema-4 documents, upgrades by upgrading.
15
+
16
+ **Migrating from 22.0.0**, in the order a consumer will hit them:
17
+
18
+ 1. **`Content::Placement` is gone.** Use `Content::VoiceEvent`, which is abstract; `VoiceEvent.build` answers a `NoteEvent` or a `RestEvent`. Methods named for placements are named for voice events, such as `Voice#voice_events` for `Voice#placements`.
19
+
20
+ 2. **Ties moved from `RhythmicValue` to `RhythmicDuration`.** `RhythmicValue.get` answers nil for a tied name; read one with `RhythmicDuration.get`. Compare lengths across units, values, and durations with `#same_duration_as?`, since `==` now compares how they are written.
21
+
22
+ 3. **Schema-4 documents must be read with `Flow.from_v4_h` or `Project.from_v4_h`** and saved again. `from_h` refuses them with an error naming the reader, and the v4 readers are kept through 23.x.
23
+
24
+ 4. **Shared MusicXML helpers moved to `HeadMusic::Notation`**, and `MusicXML::MarkingWriter` is `MusicXML::MarkingElements`. Neither is aliased.
25
+
26
+ The Changed section lists the remaining breaking changes, among them `Meter.get`, `Tempo` equality, contour grading, and the names of intervals past the octave.
27
+
28
+ ### Added
29
+
30
+ - **Rhythmic durations.** `HeadMusic::Rudiment::RhythmicDuration` is how long a note or rest lasts: an ordered, non-empty, frozen list of `RhythmicValue`s joined by ties, where an untied note is a duration of one value. `RhythmicDuration.get` reads any rhythmic value name, tied names included ("half tied to eighth tied to sixteenth"), and wraps a `RhythmicValue` or `RhythmicUnit`, answering nil for a name it cannot read. It answers `#values`, `#single_value` (the lone value, or nil when tied), `#fraction`, `#subticks`, `#ticks`, `#tuplet?`, `#uniform_tuplet_ratio`, `#with_tuplet`, and `#tied_to`, which answers a new duration with a value or another duration tied on at the end. Two durations are `==` when written the same way, and a duration is `==` to its name.
31
+ - `HeadMusic::Rudiment::DurationComparison` compares rhythmic units, values, and durations by how long they last, apart from `==`, which compares how they are written. `#same_duration_as?` takes any of the three or a name, so a dotted half has the same duration as "half tied to quarter" and an eighth as "sixteenth tied to sixteenth", and `<=>` orders the three kinds together by length. `RhythmicUnit#fraction` answers a unit's exact length.
32
+ - **Humdrum `**kern` import and export.** `HeadMusic::Notation::Kern.parse` reads a `**kern` document into a flow, and `Kern.render` and `Flow#to_kern` write one back. Each kern spine becomes a voice, parts and staves come from `*part` and `*staff` tags, and spine splits, joins, exchanges, and partial terminations map sub-spines onto voices of one part. The reader takes pitches, durations, rests, chords, ties across barlines, pickups and short final bars, repeat barlines, key signatures and their designations, meters, tempos, clefs, `*I` names (as players) and vocal and piano codes, `**text` lyrics, and a `Work` with a composer credit from the reference records. Every voice spans the whole flow, padded with rests, so a flow read back from what the writer wrote is the flow it was. All 366 readable chorales of the Bach 370 corpus parse, write back to themselves, and render to MusicXML and LilyPond; four hold bars the model cannot, and raise.
33
+ - A standalone flow's JSON keeps the players its parts carry, as a sparse `"part_players"` list with a `"player"` index on each such part, within schema 4. Project documents are unchanged: the project's own player indexes remain the record of which chair a part fills.
34
+ - The LilyPond parser reads a `\new PianoStaff` or `\new StaffGroup` as one part: its named staves become the part's staff system, with clefs from each staff's opening `\clef`, and its voices are assigned to their staves, crossing with `\change Staff` at a barline, including between the halves of a tied note. A staff filled only with whole-bar rests keeps its place without a voice. The LilyPond writer names each voice of a staff group for its role (`\new Voice = "right hand"`), so grand-staff parts round-trip.
35
+ - `HeadMusic::Rudiment::Soundable` names the role of anything a placement can sound. `Pitch` and `UnpitchedSound` include it, and `SoundResolver` passes any `Soundable` through unchanged.
36
+ - `HeadMusic::Rudiment::Articulation` (staccato, staccatissimo, accent, tenuto, marcato), `Ornament` (trill, mordent, inverted mordent, turn), and `Dynamic` (the levels *ppp* to *fff* and the accents *sf*, *sfz*, *rfz*, *fp*) are catalogs read from YAML, found with `.get` by key or alias in any case, and named in every shipped locale. `mordent` is the lower mordent and `inverted_mordent` the upper.
37
+ - A `NoteEvent` carries markings: `#articulate` and `#embellish` add articulations and ornaments by key, each at most once, and `#note_dynamic=` sets an accent such as *sfz* for that one note. A chord tone placed onto a marked note event joins it without disturbing its markings. A `RestEvent` refuses all three with `ArgumentError`.
38
+ - `HeadMusic::Content::DynamicEvent` puts a dynamic level in force at a position. `Voice#place_dynamic` places one for a voice, even under a held note or a rest, and `Part#place_dynamic` one for every voice of a part on every staff; a second at the same position raises `ArgumentError`. `Voice#dynamic_at(position)` answers the level in force from the latest of the voice's and the part's events, or *p* from an *fp* note onward.
39
+ - Flow JSON writes a voice event's `"articulations"`, `"ornaments"`, and `"note_dynamic"`, and a voice's and a part's `"dynamic_events"`, as optional keys within schema 5. A flow without markings serializes as before.
40
+ - Flow JSON writes a voice's and a part's `"spans"` as a list of `{"kind", "from", "to"}`, an optional key within schema 5, and reads a voice's back after its events, so an end its kind cannot sit on, a slur on a part, or hairpins that overlap raise `ArgumentError` naming its path. A flow without spans serializes as before.
41
+ - ABC reads and writes articulations, ornaments, and note dynamics, in shorthand (`.`, `L`, `T`, `M`, `P`), `!name!`, and `+name+` forms, and reads `!ppp!` to `!fff!` as dynamic events on the voice. It writes a voice's and its part's dynamic events before the note they fall on. Other standard decorations, such as bowings, are dropped.
42
+ - LilyPond reads and writes articulation shorthands and commands (`-.`, `-!`, `->`, `--`, `-^`), the ornaments `\trill`, `\mordent`, `\prall`, and `\turn`, the sforzandos `\sf`, `\sfz`, `\rfz`, and `\fp`, and levels from `\ppp` to `\fff` as dynamic events on the voice. It reads a `\new Dynamics` inside a `\new PianoStaff` or `\new StaffGroup`, or beside a `\new Staff`, as the part's dynamic events, giving a sforzando there to every note of the part attacking at its position, and writes a part's dynamic events that way, with spacer rests. Fermatas, arpeggios, bowings, and breath marks are dropped, as is a `\key`, `\time`, or `\clef` repeated inside a `\new Dynamics`.
43
+ - MusicXML writes articulations, ornaments, and note dynamics in `<notations>`, and each dynamic event as a `<direction>` at its position, with a `<voice>` and `<staff>` when it is a voice's.
44
+ - kern reads and writes a token's articulations (`'`, `` ` ``, `^`, `~`, `^^`), ornaments (`t`, `S`, and Humdrum's older names `m` for the upper Pralltriller and `w` for the lower mordent), and `z` (*sfz*). It reads a `**dynam` spine as dynamic events on the part of the nearest `**kern` spine to its left, giving an *sf*, *sfz*, *rfz*, or *fp* to every note of the part attacking on its row, and writes one `**dynam` spine per part that has dynamics. A dynamic that falls in the middle of a note is written on null rows spaced evenly to land on it, leaving the note whole. Text such as "cresc." is skipped.
45
+ - The ABC, LilyPond, kern, and MusicXML writers raise `RenderError` for a voice or part dynamic, or a hairpin's start or end, in a bar before or after the written music, naming it and its position, rather than leaving it out. A dynamic or hairpin start exactly where the music ends is refused; a hairpin may end there, on the final barline.
46
+ - The ABC and kern writers raise `RenderError` naming the position where a part and its voices hold different dynamic levels at one position, since an ABC voice and a part's `**dynam` spine hold one at a time, rather than writing one of them.
47
+ - `HeadMusic::Rudiment::SpanKind` is a catalog of markings that run from one point to a later one: the slur and the phrase mark (alias `phrasing_slur`), which are curves, and the crescendo (alias `cresc`) and diminuendo (aliases `decrescendo`, `decresc`, `dim`), which are hairpins. Each kind states its rules as data: its family (`#curve?`, `#hairpin?`), what its ends sit on (a slur on notes, a phrase on any voice event, a hairpin anywhere), whether it covers its last note or ends at its end position, and what may hold it (a hairpin, a voice or a part).
48
+ - `HeadMusic::Content::Span` runs from one position to a later one. `Voice#add_span(kind, from:, to:)` adds a slur, a phrase mark, or a hairpin, and `Part#add_span` a hairpin for every voice of the part, raising `ArgumentError` for an end that its kind cannot sit on, an end that does not come after the start, or the same span twice. Curves may nest or overlap; hairpins of one voice or one part may touch but not overlap, while a voice's may overlap its part's. `Voice#spans` and `Part#spans` answer their own in order, and `Voice#spans_at(position)` answers those in force there, its part's hairpins included, innermost first: a curve runs to the end of its last note, and a hairpin to its end position. A part hairpin adds no bars, and may end at `Flow#final_barline_position`.
49
+ - ABC reads and writes slurs, `(` … `)` and the dotted `.(` … `)`, as spans, pairing them by nesting as ABC does. A slur that spans a single note, closes with nothing open, or ends on a rest is dropped. The writer opens a slur before a note's decorations and closes it after the last part of its last note. ABC has no phrase mark, so a phrase is written as a slur drawn in to the notes at its ends, and left out where it would cross a slur or cover the same notes. A slur that begins where another ends starts on the note after, since ABC cannot say both on one note, and two slurs that cross raise `RenderError`.
50
+ - LilyPond reads and writes slurs `(` … `)` and phrasing slurs `\( … \)` as spans, with a `^` or `_` direction, and named with `\=` so that two may overlap, as `\=1(` … `\=1)`. A close on a tie's later link belongs to the tied note, and marks pair in the order written, so `e')( f'` ends one slur and begins the next. As in LilyPond, an open while one of its kind and name is open is ignored; an unmatched close, an unclosed open, or a slur ending on a rest is dropped. The writer opens a span after its first note's marks and closes it on the last link of its last note, closing before opening where one slur ends and the next begins, and names a slur with `\=` only when another of its kind is open, so nested and crossing slurs compile.
51
+ - MusicXML writes a voice's slurs and phrase marks as `<slur>` in `<notations>`, starting on the first note of a span's first voice event and stopping on the last note of its last, on a rest where a phrase begins or ends there. Numbers are given in document order, bar by bar and voice by voice as the part is written, so spans open at once in the file keep apart, whether they cross, sit in different hands of a grand staff, or touch on a tied note; a seventeenth open at once raises `RenderError`.
52
+ - kern reads and writes slurs `(` … `)` and phrase marks `{` … `}` as spans, nested or elided with `&` into an overlap. A close on a tie's later link belongs to the tied note, a note's closes come before its opens so one slur can end where the next begins, and a slur that opens on a grace note stays there. An unmatched mark, or a slur the voice refuses, such as one on a rest, is dropped. The writer opens a span on its first note's first tied link and closes it on its last note's last, once per chord, and elides a slur that crosses another as `&(` … `&)`.
53
+ - **Bar markings.** A `Bar` carries a closing `barline` (`:regular`, `:double`, `:final`, `:dashed`, or `:dotted`), a `rehearsal_mark` (stored as a string), `segno?` and `coda?` signs, a `fine?`, a `to_coda?`, and a `jump`: a `HeadMusic::Content::Jump` of kind `:da_capo` or `:dal_segno`, `to` `:fine`, `:coda`, or nil. `Jump.get` reads "D.S. al Coda" and its kin. A final barline at the end of the flow is implied. Flow JSON writes the new fields sparsely within schema 5.
54
+ - `Flow#performance_order` answers the bars as played, each a `PlayedBar` with its `bar`, its repeat `pass`, and its `playing` (how many times that bar has sounded). It unfolds repeats and endings, and follows a D.C. or D.S. without taking repeats again, playing each section's last ending, to the Fine or through the To Coda. A plain D.C. or D.S. stops at whichever comes first. More than one jump, a D.S. with no segno, an al Fine with no Fine, or an al Coda with no To Coda or coda sign raises `ArgumentError`.
55
+ - ABC reads and writes `||`, `|]`, and `.|` barlines, body `P:` labels and inline `[P:x]` as rehearsal marks, and the navigation decorations `!segno!`, `!coda!`, `!fine!`, `!dacoda!`, `!D.C.!`, `!D.S.!`, and the abcm2ps `!D.C.alfine!`, `!D.C.alcoda!`, `!D.S.alfine!`, and `!D.S.alcoda!`. In a tune with an al Coda jump, two `!coda!`, and no `!dacoda!`, the first coda sign is read as the To Coda. A part label, segno, or coda sign after the last note is dropped. The writer now writes repeats, `::`, and 1st and 2nd endings, closing an ending before a plain barline with `[|`.
56
+ - LilyPond reads and writes `\bar` styles, `\mark`, `\sectionLabel`, `\section`, `\segnoMark`, `\codaMark`, `\fine`, and `\jump`, and writes a final `\bar "|."`. A Fine before the last bar, or a To Coda on a jump bar, is written as `\textEndMark` in the first voice, and a coda sign beside a segno as a `\textMark` of its glyph, so the output compiles with lilypond 2.26 without warnings. As in ABC, two `\codaMark`s with an al Coda jump and no To Coda read the first as the To Coda.
57
+ - MusicXML writes `<barline>` elements with `<bar-style>`, repeats, and endings, a final `light-heavy` barline, and `<rehearsal>`, `<segno>`, `<coda>`, and words with the `<sound>` attributes for Fine, To Coda, D.C., and D.S., in every part.
58
+ - kern reads and writes `||` and `|!` barlines and `*>A` section labels as rehearsal marks, and writes `==` only at the end. Expansion lists are still ignored.
59
+ - The ABC, LilyPond, MusicXML, and kern writers raise `RenderError` for repeat structure or a marking on a bar after the music ends, rather than dropping it. `Flow#last_sounding_bar_number` answers the bar the music ends in, which the readers and writers share, and `Flow#bar(number)` answers one bar, allocating only that one.
60
+ - `HeadMusic::Notation::BarSplitter.position_at(flow, bar_number, offset)` turns an offset in a bar into a position, the inverse of `offset_in_bar`.
61
+ - ABC reads `x` invisible rests and `y` spacers, placing a level written on either at its position; an `x` that ends the voice in the bar where its music ends adds no event, and any other is read as a rest.
62
+ - ABC reads a voice overlay `&` of invisible rests and spacers, placing its levels at their positions in the bar, and raises `UnsupportedFeatureError` for an overlay holding notes or rests and for the multi-bar `(&` … `&)`, and `ParseError` for one that runs past its bar's music.
63
+ - LilyPond import reads a spacer `s` in a voice: one that ends the voice in the bar where its music ends adds no event and keeps its dynamic level at its position, and any other reads as a rest.
64
+ - LilyPond import reads `\after D mark music`, placing a dynamic level part way into a note, rest, or sequence at its exact position.
65
+ - kern import leaves out invisible rests (`ryy`) after a spine's last visible event in the bar where its music ends, so a `**dynam` level beside them keeps its position; any other invisible rest is read as a rest.
66
+ - Every writer places a voice or part dynamic at its exact position where no note starts. ABC writes one inside a note or rest on a voice overlay of invisible rests (`C4 D4 & x2 !f!x6|]`) and one after the last note of a short final bar on a trailing invisible rest (`C2 D2 E2 !f!x2|]`). LilyPond writes one inside a note with `\after` and one after the music on spacers. kern writes them on null rows and invisible rests (`ryy`), and one before a pickup's first note beside the pickup's leading rest. MusicXML writes each with an `<offset>`. ABC and LilyPond raise `RenderError` naming the position for a dynamic no binary note value reaches.
67
+ - **Hairpins in every format.** Each writer places a hairpin's start and end at its exact position, inside a note, on a tied note's later link, or on the final barline, writing at one position its ends, then its levels and accents, then its starts. Each reader drops a hairpin it cannot pair rather than raising, so every file that imported still imports.
68
+ - ABC reads `!<(!`, `!<)!`, `!>(!`, `!>)!`, and `!crescendo(!`/`!diminuendo(!` with their closes as a voice's hairpins, pairing them by kind, and writes a voice's and its part's hairpins that way, ending one on the final barline as `!<)!y`. It refuses a voice's and its part's hairpins that overlap.
69
+ - LilyPond reads `\<`, `\>`, `\!`, `\cresc`, `\dim`, `\decresc`, and their `\end…` forms in a voice and in a `\new Dynamics`, ending an open hairpin at a level, an accent, or the next hairpin, reads `<>` as a marker that takes no time, and skips styling commands such as `\crescTextCresc`. It writes a voice's hairpins in the voice and a part's in its `\new Dynamics`, cut where a level or accent in that context falls inside one, as LilyPond would end it there, and ends one on the final barline with `<>\!`. It also writes a voice's dynamics in a bar where the voice has only a whole-bar rest.
70
+ - kern reads `<`, `>`, `[`, `]`, and the doubled closes `[[`/`]]` in a `**dynam` spine as the part's hairpins, pairing them as Verovio does, and writes a part's and its voices' hairpins there, cut at the levels and accents inside them, ending one on the final barline with a doubled close. It refuses hairpins of a part and its voices that overlap.
71
+ - MusicXML writes hairpins as `<wedge>` directions, numbered in document order across a part's voices, with `<divisions>` fine enough for every start and end.
72
+ - **Tuplet ratios.** `HeadMusic::Rudiment::TupletRatio.get("3:2" | [3, 2] | "triplet")` is an unreduced actual-to-normal ratio, so 6:4 is not 3:2, with a `factor` of normal over actual; 1:1 and non-positive numbers raise `ArgumentError`. `RhythmicValue.get(identifier, tuplet:)` and `RhythmicValue.new(unit, dots:, tuplet:)` put a ratio on a value, and `RhythmicDuration.get(identifier, tuplet:)` and `#with_tuplet` put one on every value of a duration, each value of a tied duration carrying its own. Values are named with the ratio first ("triplet eighth", "5:4 sixteenth", "triplet dotted eighth"), and durations each value in turn ("triplet eighth tied to quarter"). `RhythmicValue.get` reads a value's name back and `RhythmicDuration.get` a duration's, where "triplet eighth" had read as a plain eighth. A name with a tuplet word anywhere else, such as "eighth triplet" or "quintuplet eighth", is nil rather than a plain value. `#tuplet_ratio` and `#tuplet?` answer a value's ratio, `RhythmicDuration#tuplet?` and `#uniform_tuplet_ratio` a duration's, and `#written_fraction`, `#fraction`, `#exact_subticks`, and `#subticks` give exact lengths: a 7:4 sixteenth is 230400/7 subticks, 32,914 rounded, and a tied duration rounds once over all its values. `Rhythm::SUBTICKS_PER_TICK` and `SUBTICKS_PER_WHOLE` join `PPQN`, and `HeadMusic::Time::SUBTICKS_PER_TICK` is aliased to it.
73
+ - Ratios that do not land on whole subticks, such as septuplets, round each onset to the nearest subtick, measured from the start of its run of consecutive same-ratio values, so the run's span stays exact and rounding never accumulates: seven 7:4 sixteenths from the downbeat end exactly on beat 2. `VoiceEvent#onset_remainder` answers the exact onset less the position, in Rational subticks, and `Voice#voice_event_preceding(position)` the event starting last before a position.
74
+ - **Tuplet groups.** `Voice#group_tuplet(from:, to:, unit: nil)` brackets the notes and rests from one position up to an exclusive end as one `HeadMusic::Content::TupletGroup`, which tells two triplets from one sextuplet and gives the written `unit` its bracket counts in. A group brackets tied links, so it may begin or end partway through a chain; its links must be consecutive, carry one ratio, and fill its span exactly, and a group of a ratio that rounds must begin and end on voice events. Such a group may begin partway through a run of its ratio, and grouping moves no onset, so back-to-back brackets of three, seven, and four septuplet sixteenths keep the run's exact onsets and read and write as authored. Otherwise `ArgumentError` names the group ("the 3:2 tuplet from 1:1:000 to 1:2:000 …"), as it does for overlapping groups, since nested tuplets are not supported. The default unit is the written total over the ratio's actual count where that is one plain or dotted value, so a quarter and an eighth under 3:2 count in eighths, and otherwise the largest value dividing every link. `Voice#tuplets` and `#tuplet_at(position)` answer the groups, `Voice#to_h` writes them as `"tuplets"`, and `Voice#place` refuses a new event inside a group, while a chord tone or a note placed on a rest there merges as before.
75
+ - **Grace notes.** `NoteEvent#add_grace_note(sound_or_sounds, rhythmic_value, slashed: false, tied: false, articulations: [])` adds a `HeadMusic::Content::GraceNote` before the note: one or more sounds, pitched or unpitched, a plain or dotted written value, a slashed (acciaccatura) or unslashed (appoggiatura) flag, a tie to whatever follows, and articulations. `NoteEvent#grace_notes` answers them in order, and a note keeps them when another sound is merged into it. Grace notes take no time and are never voice events, so positions, bars, continuity, melodic and harmonic analysis, and every guide pass them by. A `RestEvent` refuses one with `ArgumentError`. `Voice#add_span(kind, from:, to:, from_grace: true)` starts a slur or phrase on the first grace note of the note at `from`, and may end on that same note; a hairpin, a note without grace notes, or a part's span refuses it.
76
+ - Flow JSON writes tuplets and grace notes as optional keys within schema 5, and documents without them read as before. A ratio is written inside a value's `"rhythmic_value"` name (`"triplet eighth"`, `"7:4 sixteenth"`), a voice's groups as `"tuplets"`, a list of `{"from", "to", "unit"}` read back after its events, a note's grace notes as `"grace_notes"`, each `{"sounds", "rhythmic_value", "slashed"?, "tied"?, "articulations"?}`, and a span from grace notes with `"from_grace": true`. A bad group, grace note, or flag raises `ArgumentError` naming its path, such as `parts[0].voices[0].tuplets[2]` or `parts[0].voices[0].voice_events[3].grace_notes[1]`, as do grace notes on a rest. Since schema 5 first ships in 23.0.0 and 22.x reads only schema 4, no released gem misreads them.
77
+ - `DottedDuration.sounding_fraction(value)` answers a value's length under its tuplet ratio, beside `dotted_unit_fraction`, its written length, and `rhythmic_duration_for(fraction, tuplet:)` builds a duration under a ratio from its sounding length. `BarSplitter.onset_in_bar(voice_event)` answers a voice event's exact onset in its bar, and `BarSplitter.segments_of` cuts at exact onsets and lengths, so a tuplet note crossing a barline splits into tied notes of its ratio, a rounded septuplet at the exact barline, and a tied duration of mixed ratios value by value. Each `Segment` carries its exact `offset` in its bar and the fraction `elapsed` before it. A piece that cannot be written under its ratio, such as a 2:3 duplet across a 6/8 barline, raises the writer's `RenderError` naming the group or the ratio.
78
+ - `HeadMusic::Notation::TupletBrackets.for(voice, segments)` answers the tuplet brackets a writer draws over a voice's bar segments, each a `Bracket` with its `ratio`, `unit`, written `links`, and `group`: one per tuplet group, across a barline where the group crosses one, and one per run of ungrouped values of a ratio, closing once its written total reaches the ratio's actual count of its shortest unit and counted in a group's default unit. The ABC, LilyPond, kern, and MusicXML writers raise `RenderError` for a dynamic or hairpin end strictly inside a note of a ratio that does not land on whole subticks, such as a septuplet, where no note starts, naming the mark and the note, since no exact time can be written for it.
79
+ - **Tuplets and grace notes in every format.** Each reader reads them, and each writer writes them, with written durations taken from a value and its ratio, never from its rounded ticks. Every file that imported still imports.
80
+ - ABC reads `(3`, `(p:q:r`, and the meter-dependent default for `(n` as tuplet groups, and `{…}` and `{/…}` as grace notes before the next note, with a slur opened before the braces starting on the first of them. It writes groups, and runs of tuplet values with no group, as `(p:q:r`, or the short `(3` and `(2` where they mean the same thing, and grace notes as `{…}` and `{/…}`. A tuplet inside another raises `UnsupportedFeatureError`, and a tuplet across a barline raises `RenderError` naming the group.
81
+ - LilyPond reads `\tuplet n/d { … }`, its span form `\tuplet n/d dur { … }`, `\tupletSpan`, and `\times d/n { … }` as tuplet groups, and `\grace`, `\slashedGrace`, `\appoggiatura`, and `\acciaccatura` as grace notes, with the slur the last two imply. `\afterGrace`, and grace notes before a rest, skip their grace notes. Nested tuplets, and `R` or `<< >>` inside a tuplet, raise `UnsupportedFeatureError`. It writes one `\tuplet n/d { … }` per group and per run of ungrouped values, across barlines where a group crosses one, and `\acciaccatura` or `\appoggiatura` for grace notes slurred to their main note, `\slashedGrace` or `\grace` otherwise. Spacers that fall between subticks, such as `s1*1/28` beside a septuplet, round to the nearest subtick instead of raising.
82
+ - kern reads a duration whose undotted value has a non-power-of-two denominator as a tuplet value, with actual the odd factor of the denominator and normal the largest such count below it (`12` a triplet eighth, `24` a triplet sixteenth, `20` a 5:4 sixteenth, `28` a 7:4 sixteenth, `3%2` a triplet whole); duplets read as dotted values, and nested tuplets as their one flattened ratio. It infers a group from the first run of at least two consecutive values of one ratio whose exact length is a plain note value, across barlines, so a sextuplet reads back as two triplets. It infers link by link, so a group may end or begin inside a tied chain that mixes ratios, as a triplet ending in a triplet eighth tied to a quarter. It reads `q` as slashed grace notes and `Q` or `qq` as unslashed ones, keeping their articulations, a tie into the note, and a slur or phrase that opens on the first of them, and drops grace notes before a rest, a tie's later link, or the end of a spine. It writes tuplet values as reciprocals of what they sound (`12`, `12.`, `28`), a value whose length is a plain or dotted note as that note (duplets as `8.`), and `%` otherwise (`10%3`), and grace notes as `q` and `Q` tokens on rows of their own.
83
+ - MusicXML writes `<time-modification>` on every tuplet note, `<tuplet type="start">` and `<tuplet type="stop">` on the ends of each group or run, across barlines too, and grace notes as `<grace/>` notes, `<grace slash="yes"/>` for acciaccaturas, with their ties, articulations, and slurs. `<duration>` is the exact sounding length, and `<divisions>` are computed from written values and ratios, so septuplets are exact too.
84
+ - `DottedDuration.tuplet_ratio(fraction)` infers the ratio a non-binary length sounds under, and `DottedDuration.rest_duration(fraction)` answers the rest that fills a length, under that ratio where no plain value does; kern now uses them. `TupletGroup::Link#start_position` and `#end_position` answer where a link of a tied chain starts and ends.
85
+ - The MusicXML writer writes a `<part-symbol>` for a multi-staff part joined by a bracket or by nothing, leaving a brace to its default.
86
+ - The MusicXML writer writes a part's instrument as `<score-instrument>` with its W3C standard `<instrument-sound>`, from a table (`MusicXML::InstrumentSounds`) of about 120 sound ids the reader shares. `MusicXML::KeyMapper.tonal_context(fifths, mode)` reads a `<key>` back as the key or mode the writer wrote, and `MusicXML::MarkingElements` maps articulation, ornament, and accent elements both ways.
87
+ - **MusicXML import.** `MusicXML::Parser` reads a score-partwise or score-timewise document with REXML, in UTF-8 with or without a byte order mark, UTF-16 with one, or a binary string, and raises `MusicXML::ParseError` naming the line of malformed XML; `MusicXML::UnsupportedFeatureError` names a compressed `.mxl` as a follow-up. `MusicXML::ParsePreflight` refuses a document that declares entities, and text past REXML's expansion limits raises `ParseError` rather than a bare `RuntimeError`. The DTD is never fetched. `MusicXML::Document` regroups a timewise score's measures into parts.
88
+ - `HeadMusic::Notation::MusicXML.parse(string)` returns a `Flow`. It reads notes and rests from `<pitch>`, `<type>`, `<dot>`, and `<duration>` at the `<divisions>` in force, ignoring a `<duration>` within one division of its written value as exporter rounding and raising `ParseError` naming the measure beyond that, or for a `<duration>` with no `<divisions>` in force; a note with no `<type>` takes its value from its `<duration>`. A `<divisions>`, `<staves>`, `<beats>`, or `<beat-type>` that is empty or not positive, a number written with an exponent, and a `<note>` with no `<pitch>`, `<unpitched>`, or `<rest>` raise `ParseError`, and a pitch outside octaves -1 to 9 or altered past a double sharp or flat, as written or once transposed, raises `UnsupportedFeatureError`, each naming the measure. The `<movement-title>` names the flow, with the `<work-title>` beside it as its `Work`, and a `<work-title>` alone, or failing that a title `<credit>`, names it; composer `<creator>`s become the composer, credited on the `Work` where there is one; each `<part-name>` names a `Player` with no project; and a mapped `<instrument-sound>` gives the part its instrument.
89
+ - MusicXML import aligns every part's measures into bars: an implicit first measure is a pickup in bar 0, a short first measure is right-aligned in its bar, a short measure the next one completes is one bar with it, whether or not the next is marked implicit, unless it changes the meter, and a short final bar reads as written; an overfull measure, any other short measure, a meter of added beats such as 3+2/8, and a beat type that names no note value, such as 3/3, raise `UnsupportedFeatureError` naming the measure. A whole-measure rest, `measure="yes"` or as long as the meter, fills its bar. `<key>` (`<fifths>` and `<mode>`), `<time>`, and `<sound tempo>` open the flow and change it at later barlines, the first part's key winning; a key, time, or tempo in the middle of a bar is skipped, while a `<divisions>` change applies from its point on.
90
+ - MusicXML import reads each `<voice>` value of a part as a voice, in order of first appearance, a note with none being voice 1; `<backup>` and `<forward>` move the cursor, one of no duration moving nothing, a `<forward>` gap becomes a rest, and every voice is padded with rests split at barlines from the first bar to the end of the music, parts with fewer measures included. `<chord/>` adds a tone to the note before it, cue notes read as rests of their length, a note or rest that does not print (`print-object="no"`) takes its time as a `<forward>` does, with its chord tones and grace notes, and `<unpitched>` notes as unpitched sounds of the part's instrument, or of none. A `<multiple-rest>` is skipped. A `<backup>` before the start of its measure, a `<chord/>` with no note before it in its voice, and a voice that overlaps itself raise `ParseError`, and chord tones of different lengths and a `<measure-repeat>` raise `UnsupportedFeatureError`, each naming the measure.
91
+ - MusicXML import folds tied notes into one voice event, across barlines, keeping each link's written value: `<tie>`, falling back to `<tied>`, where `<tie>` wins when they disagree and `let-ring` and `continue` are skipped. A tie that nothing stops, that a rest follows, or whose stop does not start where the note before it ends, within one division, is dropped and its note keeps its written value; a tie on only some pitches of a chord, or between chords that share only some pitches, is skipped; and a tie between pitches that share nothing raises `ParseError` naming the measure it starts in.
92
+ - MusicXML import reads `<time-modification>` as tuplet values and `<tuplet>` starts and stops as tuplet groups, link by link, so a bracket may end on the first piece of a tied note, with the `<normal-type>` as the group's unit. A run with no `<tuplet>`, an unmatched start or stop, or a bracket the voice refuses stays ungrouped with its ratios kept, and a start is abandoned once its run of one ratio ends; a start inside a bracket's run, its ratio the run's or a multiple of it, raises `UnsupportedFeatureError` for nested tuplets.
93
+ - MusicXML import reads `<grace>` notes as grace notes of their voice's next main note, slashed for `slash="yes"`, a grace chord as one grace note with several pitches, an eighth where there is no `<type>`, with their articulations, and tied where a tie starts on a pitch what follows shares; a tie into the main note takes the place of its stop. Grace notes before a rest, a tie's later link, or the end of the part are dropped.
94
+ - MusicXML import builds a part's staff system from `<staves>`, joined as its `<part-symbol>` says (`bracket` and `square` as a bracket, `none` as none) and otherwise braced as `StaffSystem.grand_staff` is, with each staff's clef from `<clef>`, and assigns each voice to the `<staff>` of its first note in each bar, so a change at a barline is a staff crossing; a change in the middle of a bar is skipped. A later clef on a downbeat is a clef change, `<clef-octave-change>` reads octave clefs, and percussion and TAB clefs read as no clef. A `<staff>` below 1 or beyond the part's `<staves>` raises `ParseError` naming the measure.
95
+ - MusicXML import honors `<transpose>` (`<diatonic>`, `<chromatic>`, `<octave-change>`), so a transposing part's notes, grace notes, and key land in concert pitch, a later `<transpose>` replacing the one before it, as after an instrument change; a numbered `<transpose>` applies to its staff alone. `<double>` is skipped.
96
+ - MusicXML import runs a registry of mark readers (`MusicXML::MARK_READERS`, each a `MusicXML::MarkReader`) once every voice is final, handing them a `NoteRecord` for each `<note>`, a `DirectionRecord` for each `<direction>` and measure-level `<sound>`, and a `BarlineRecord` for each `<barline>`, in document order, then having each apply what it read. The core reads in linear time: doubling a document's bars at most doubles the positions it builds and the meters it looks up, and it places every voice in order, so none is revised.
97
+ - MusicXML import reads articulations, ornaments, and note dynamics in `<notations>`, and a `<direction>` accent onto the note attacked at its position.
98
+ - MusicXML import reads lyrics: verses by `number`, unnumbered as verse 1 and other labels ranked by first appearance, `<syllabic>` hyphenation, and elisions.
99
+ - MusicXML import reads `<slur>`s as slurs, paired by number, and as phrase marks where either end falls on a rest, including slurs from grace notes; an unmatched slur is dropped.
100
+ - MusicXML import reads `<beam>`s as beam breaks where they differ from the default grouping, so the writer's beams round-trip.
101
+ - MusicXML import reads barline styles, repeats, and 1st and 2nd endings, including ending number lists such as "1, 2". A repeat with no `times` plays as often as its section's highest ending.
102
+ - MusicXML import reads rehearsal marks, segno and coda signs, Fine, To Coda, D.C., and D.S., letting their `<sound>` attributes decide over the words, and reads MuseScore's segno and coda glyph words (U+E047, U+E048) as the signs.
103
+ - MusicXML import reads `<direction>` dynamics as dynamic events at their `<offset>`, on the voice `<voice>` names or the part, and pairs `<wedge>`s by number into hairpins in document order; a stop with no open wedge waits for a later start of its number that sounds before it, as MuseScore writes them, and unmatched wedges are dropped.
104
+ - `Instrument#gm_program` answers the nearest General MIDI program, 1-based as GM tables print it, and `Instrument#gm_percussion_key` the nearest GM percussion key (35–81) for unpitched percussion. A variant such as `clarinet_in_a` inherits its parent's. Didgeridoo, theremin, and tabla have no program, and the drum kit and castanets no percussion key; the tam-tam stands in as the Chinese Cymbal (52) and the tenor drum as the Low Tom (45).
105
+ - **MIDI export.** `HeadMusic::MIDI.render(flow, transposed: false)` and `Flow#to_midi` write a format 1 Standard MIDI File as a binary string: a conductor track with the sequence name and the meter, tempo, and key in force at each played bar wherever they change, then one named track per part. Bars play in `Flow#performance_order`, a pickup starts the file at its first note, a tie over a barline sounds on only into the next bar of the score, and a key struck again on its channel while it sounds is released and re-struck. Pitched parts take channels in part order around channel 10, sharing one where they play the same instrument throughout. The ticks per quarter are the least common multiple of 480 and every time written, or 960 with rounding past what a division holds. `MIDI::RenderError`, a subclass of `Notation::RenderError` so one rescue covers every writer, is raised before any bytes are written for an empty flow, a broken navigation such as a D.S. with no segno, a flow name, track name, or lyric that is not valid UTF-8, a meter whose click a MIDI time signature cannot count (6/1, 9/1, 12/1, and denominators of 64 or more), a performance longer than MIDI's delta times hold, a key outside 0..127, a meter or tempo MIDI cannot hold, and more than fifteen distinct pitched channels.
106
+ - Velocities follow the dynamic level in force, from `HeadMusic::MIDI::VELOCITIES` (*ppp* 16 to *fff* 127, *mf* 80 by default), raised by accents and marcatos (`ARTICULATION_BOOSTS`) and by *sf*, *sfz*, and *rfz* (`NOTE_DYNAMIC_BOOSTS`), and clamped to 1..127. An *fp* note strikes at *f*. Under a voice or part hairpin, velocities ramp by onset to the level placed at its end, or one level beyond its start, which stays in force after it; the innermost hairpin governs, a voice's over its part's.
107
+ - Grace notes sound on the beat, taking their time from the main note, and a grace note tied to its main note holds through it. Staccatissimo, staccato, and portato notes sound for a quarter, half, and three quarters of their last tied link, staccatissimo winning over the others.
108
+ - Each pitched part writes its instrument's General MIDI program at its start and at every instrument change, restored by repeats and D.C., or 1 (Acoustic Grand Piano) when the instrument has none. Unpitched sounds play on channel 10 at their instrument's GM percussion key, else `DEFAULT_PERCUSSION_KEY` (76, Hi Wood Block), even within a pitched part's track.
109
+ - With `transposed: true`, each pitch is read as written and sounds at its written pitch plus the sounding transposition of its part's instrument in force at its bar; the key signature stays concert.
110
+ - Each sung syllable writes a UTF-8 lyric meta event at its note's onset, with "-" when the word continues. Pass *n* of a repeat sings verse *n* where the flow has one, else verse 1, and the first voice of a part with a syllable wins when two sing at once.
111
+ - A pickup shorter than a count writes the time signature that holds it exactly, such as 1/8 then 3/4 for an eighth pickup in 3/4, or the flow's meter restated at bar 1 where no such signature exists. A part with no instrument writes no program change; one that loses its instrument after having one writes program 1, so a repeat back past the change cannot replay a stale program.
112
+ - **MEI export.** `HeadMusic::Notation::MEI.render(flow, **options)`, `Flow#to_mei`, and `Layout#to_mei` write MEI 5.1 documents, generated as text with no new dependency, and valid against `mei-CMN.rng`. `MEI::RenderError` is raised before anything is written for an empty flow, unpitched sounds, text XML cannot hold, or a part whose staff system changes.
113
+ - The `<meiHead>` carries the title (the layout's, the Work's, or the flow's name) and one `<persName role>` per credited person; each flow is an `<mdiv label>`. Every element from `<mdiv>` down has an `xml:id` built from its part, voice, bar, and onset, so the same flow always gives the same ids.
114
+ - Multi-staff parts are nested `<staffGrp symbol>`s labeled with the player; each staff carries its clef and General MIDI `<instrDef>`, each voice with a role a `<layerDef>`, and a voice keeps its `<layer n>` when it crosses staves.
115
+ - Notes carry `@pname`, `@oct`, `@dur`, and `@dots`, with `@accid` where the key and earlier notes in the bar on that staff do not imply the pitch and `@accid.ges` otherwise. Rests, whole-bar `<mRest>`s (also for a voice with nothing in a bar), chords, and `@tie` follow; a pickup is `<measure n="0" type="upbeat">` and an underfull final bar carries `metcon="false"`. Beams are `<beam>` groups from the grouping MusicXML uses, tuplets `<tuplet>`s nesting with them, with `<beamSpan>` and `<tupletSpan>` where they cannot nest, and grace notes carry `@grace`.
116
+ - Key, meter, clef, and instrument changes are `<scoreDef>` and `<staffDef>` before their measure, and tempo changes are `<tempo>` with `@midi.bpm`, the opening tempo being `scoreDef/@midi.bpm` alone. With `transposed: true`, transposing staves carry their written `@keysig` and `@trans.diat`/`@trans.semi`, reset to 0 when a part returns to an instrument in C.
117
+ - Articulations are `@artic`; trills, mordents, turns, and note dynamics are control events at their note; dynamic levels and hairpins sit at their `@tstamp`, with `@layer` for a voice's; slurs and phrase marks point at their notes by `@startid` and `@endid`.
118
+ - Barline styles, repeats, and endings are `measure/@left`, `@right`, and `<ending n label>`; rehearsal marks are `<reh>`; segno, coda, Fine, To Coda, D.C., and D.S. are `<repeatMark @func>`; a repeat played more than twice gets a `<dir>` "×N"; and lyrics are `<verse n><syl>` with `@wordpos` and `@con`.
119
+ - **MIDI import.** `HeadMusic::MIDI.parse(bytes, key: nil, grid: :auto)` reads a Standard MIDI File of format 0 or 1 as a flow: a part per track and channel, voices and chords, meter, tempo, key, instrument changes, percussion on channel 10, and lyrics. All 366 readable Bach chorales come back note for note through MIDI export and import, spelled 99.947% as written with `key: :infer`.
120
+ - `HeadMusic::MIDI::Reader`, now part of the gem, decodes the file's tracks of timed events and pairs notes per channel and key across tracks. It tolerates what real files do (a longer `MThd`, extra tracks, a missing end-of-track, running status after meta events, Windows-1252 text). `MIDI::ParseError` names the byte offset of anything malformed, and `MIDI::UnsupportedFeatureError` covers format 2, SMPTE time division, and denominators above 64. Files needing more than 200,000 notes, 10,000 bars, 128 parts, or 1,000,000 voice-bars are refused before any flow is built, and a part needing more than 64 voices raises `UnsupportedFeatureError`.
121
+ - Bars come from the time signatures, a short first bar reading as a pickup; a meter change within a bar raises. Tempo is taken at each downbeat and restated in the meter's beat. Key signatures apply at the downbeat; with none, or with `key: :infer`, the key is found by `KeyFinder`, and every note is spelled by `PitchSpeller` with its voice. `key:` also takes a key, a key name, or key regions, which spell the notes while the file's signatures stay. Unknown options or values raise `ArgumentError`.
122
+ - The grid is the coarsest of sixteenths, thirty-seconds, sixty-fourths, and their triplets that places every onset within a tick, or failing that the coarsest within 99% of the best; `grid:` takes one of these or any Rational down to 1/256, the finest value the model can write. A release just short of the next onset extends to it. Notes are tied chains cut at barlines, and at beats where a value is not binary, with triplets as 3:2 tuplets; rests are cut at beats and aligned to their own length.
123
+ - Notes that start and end together form chords, and overlapping notes separate into voices by nearest mean key number, deterministically.
124
+ - GM programs map to catalog instruments, through a pinned table where several share a program and through the track name where it names one; percussion keys map to unpitched sounds, with aliases for the common drum-kit keys. A track name becomes the player unless it is exactly the instrument's name, and track 0's name names the flow when that track holds no notes.
125
+ - Lyric meta events sing on the notes they fall on, with hyphens, and with successive verses at one onset.
126
+ - What MIDI cannot carry comes back as it can: modes as major keys (a MIDI key signature holds only major or minor), legacy-encoded text as Windows-1252, and drum keys sharing one sound as one note.
127
+ - `PitchSpeller::Respelling.signature_changes(regions)` answers the regions whose key signature differs from the one before.
128
+ - **Key and spelling inference.** `HeadMusic::Analysis::KeyFinder` infers the key of music given only key numbers and timing, and `HeadMusic::Analysis::PitchSpeller` spells each key number as a pitch in that key, so music that arrives unspelled, such as MIDI, becomes readable notation. On the Bach chorales, spelling matches the written notes 99.95% of the time, whether the key is given or inferred.
129
+ - Input: both take `NoteInput`, which reads `[number, onset, duration, voice]` tuples (with `"bar:count"` onsets when a `meter:` is given), `UnspelledNote`s, or a flow, with `bars:` and `first_bar:` for meter changes and pickups.
130
+ - `KeyFinder#key` answers the major or minor key whose profile correlates best with the duration-weighted pitch-class profile, and `#candidates` ranks all 24. `KeyProfile` offers Aarden–Essen (the default), Krumhansl–Kessler, and Temperley (2007), each with its source. `#regions` answers key regions beginning at downbeats, found over the bars with a `change_penalty:` for each change; a later region's tonic takes the enharmonic nearer the one before.
131
+ - `PitchSpeller` spells by a key, a mode, or key regions, inferring them when none is given. It spells notes in the key's collection, and a minor key's raised 6th and 7th, from the key. Near ties go by melodic direction, then by keeping a sonority compact on the line of fifths, then by the key's center (Temperley 2001, Meredith 2006). `#spelled_notes` reports the rule that decided each note.
132
+ - `PitchSpeller.respell(flow, key:)` returns a respelled copy of a flow: by the key signatures in force, or with `key: :infer`, under inferred key signatures. The flow passed in is unchanged.
133
+ - `HeadMusic::Analysis::LineOfFifths` places spellings on the line of fifths.
134
+ - **MEI import.** `HeadMusic::Notation::MEI.parse(string)` reads the one movement (leaf `<mdiv>`) of an MEI document into a `Flow`, and `MEI.parse_book` reads one flow per movement with music, through nested `<mdiv>`s, `<group>`s, and `<meiCorpus>`. It reads MEI 5 and MEI 4 alike, whatever `@meiversion` says, and never reads an incipit in the header. `MEI::ParseError` names the element and measure; `MEI::UnsupportedFeatureError` covers mensural, neume, and tablature notation, measure repeats, microtones, nested tuplets, and staves added mid-piece. No other error escapes, with every attribute bounded and typed, `@copyof` expanded under a cycle guard and budget, entities refused, and nesting capped at 256.
135
+ - Identity: the flow is named by `mdiv/@label` or the head title, a `Work` (with catalog number, year, and composers' dates) comes from `<workList>`, players from staff and staff-group labels, and instruments from `<instrDef @label>` or, failing that, the General MIDI program.
136
+ - Parts, staves, and voices: a labeled or braced `<staffGrp>` is one multi-staff part, voices are keyed by layer number within a part (or by staff and layer where numbers repeat across staves), a layer whose notes share an `@staff` crosses staves at the barline, and a voice's leading and trailing whole-bar rests are not placed, so tacet voices round-trip.
137
+ - Notes and rhythms: pitches, gestural and written accidentals (otherwise the key and earlier accidentals on that staff decide), rests, chords, and lengths from `@dur`, its chord, `@dur.default`, or the time a layer or meter leaves. Ties come from `@tie` and `<tie>`, tuplets from `<tuplet>`, `<tupletSpan>`, and `@tuplet`, beams from `<beam>`, `<beamSpan>`, and `@beam`, grace notes from `@grace` and `<graceGrp>`, and tremolos read as their notes.
138
+ - Timeline: keys and modes, meters, tempos, clef changes, and `@trans.*` transpositions (read into concert pitch), from `<scoreDef>` and `<staffDef>` at the opening and at later measures. Control events placed by `@tstamp` snap to the nearest onset within 1/1000 beat.
139
+ - Markings: articulations, trills, mordents, turns, dynamic levels and note dynamics, hairpins, slurs and phrase marks, barlines, repeats, endings, rehearsal marks, and navigation, through a registry of mark readers (`MEI::MARK_READERS`) fed by event, control, and measure records.
140
+ - Lyrics: `<verse>/<syl>`, bare `<syl>`, and `@syl`, by verse, with hyphens and elisions. Editorial markup follows a fixed rule: `<lem>`, then `<corr>`, `<reg>`, `<expan>`, and `<add>`.
141
+ - Every MEI Export golden, fixture, and generated seed round-trips, MuseScore 4's MEI exports and two Bach editions from the music-encoding sample encodings read, and the sample encodings sweep reads 84 of 167 files in each MEI version against a manifest of refusals. A new `mei:musescore_fixtures` rake task regenerates the MuseScore exports.
142
+ - MEI export writes `<instrDef label>` with the instrument's name key, `@key.mode` on key signatures, and a `<workList>` with each flow's `Work`, its catalog number, year, and composers, so MEI import reads them back.
143
+ - `HeadMusic::Notation::XmlReading` and `HeadMusic::Notation::ParsePreflight` are shared by the MusicXML and MEI readers.
144
+ - `HeadMusic::Notation::BeamGroups` holds the beam grouping MusicXML and MEI share, and `HeadMusic::Notation::WrittenLinks` the written pieces of a voice's segments, which tuplet brackets and beams both read.
145
+ - `Position.for_flow` builds a position in a flow from a code or keeps one of that flow's, raising for another flow's. `Dynamic.fetch` answers a dynamic of the kind asked for, `Clef.placing` the clef that puts a pitch on a line, `GraceNote#tied_pitches` the pitches a grace note ties into, and `Notation::BarMap#downbeat_bar` the bar a measure starts on its downbeat.
146
+ - `HeadMusic::Notation::LineParseError` carries the line number and snippet that the ABC, kern, and LilyPond `ParseError`s report, and each now subclasses it. `HeadMusic::Notation::TimelineReading` holds the timeline reading the MusicXML and MEI readers share, and `HeadMusic::Time::ChangeMap` the change bookkeeping of `MeterMap` and `TempoMap`.
147
+ - `HeadMusic::Content::ChainLink` is one link of a voice event's tied chain, found with `ChainLink.of(voice_event)` and `ChainLink.between(voice, from, to)`; `TupletGroup.links_of` and `.links_between` delegate to it. `NoteEvent#add_grace_notes` adds `GraceNote`s already built.
148
+ - `Position.get(flow, position)` keeps a position or builds one from a code, `Position#ending_bar_number` answers the bar music ending there ends in, `VoiceEvent#bar_number` the bar an event starts in, `DottedDuration.tied` ties a list of rhythmic values in order, and a guideline's `count_values` builds the count and number-word values its templates take.
149
+ - **Meter symbols.** `Meter.common_time` carries `Meter#symbol` `:common`, and `Meter.cut_time` `:cut`; a meter named by its numerals has none. A meter with a symbol still equals its numeric meter and calculates as it, and `Meter.common_time.to_s` is still `"4/4"`. `Meter.for_symbol(:common)` or `(:cut)` answers the meter for a symbol and raises `ArgumentError` for anything else, as `Meter.new` does for a symbol that disagrees with its numerals. A flow keeps the symbol through `Flow.new(meter:)` and `#change_meter`, and Flow JSON writes it as `"meter_symbol"` beside the opening `"meter"` and as `"symbol"` in each meter change, leaving either out where there is none; a symbol that disagrees with its numerals raises `ArgumentError` naming its path.
150
+ - **Tempo words.** `Tempo.new(beat_value, beats_per_minute, text: nil, metronome_mark: true)` carries the words a tempo was written with, and `metronome_mark: false` records that its number was not written, though `beats_per_minute` always answers one so that time can be kept. `Tempo.for_text(text)` answers a tempo for free tempo text, with the defaults of the longest named tempo the text contains as whole words ("Allegro ma non troppo" is an allegro), or else ♩=120, and `metronome_mark: false`; blank text raises `ArgumentError`. Flow JSON writes a tempo's `"text"`, and `"metronome_mark": false`, only where they differ from the defaults, and two tempos that differ only in text are different tempo changes. `Time::TempoEvent.new` takes a `Tempo` in place of a beat value and beats per minute, and it and `Time::TempoMap` keep a `Tempo` they are given rather than rebuilding it from its numbers.
151
+ - **Tempo ramps and fermatas in the `Conductor`.** `Time::TempoRamp` runs from one position to a later one toward a target tempo, with optional text, linear in beats per minute over musical position, and in whole notes per minute between different beat values; with no target it ramps to ×3/4 of its start, or ×4/3 for accel., accelerando, and stringendo. `Time::Fermata` holds a span with a `stretch`, `Fermata::DEFAULT_STRETCH` being 2, and `Time::ATempo` marks a return to the tempo before a ramp. `Conductor.new` takes `tempo_ramps:` and `fermatas:`, and converts positions to clock time through them in closed form and clock time back to positions: a 4/4 bar ramping from ♩=120 to ♩=80 lasts 2.432790649 s, a fermata multiplies its span's clock time by its stretch, and a clock time inside it maps back into its span. `Conductor#tempo_at(position)` answers the tempo sounding there, interpolated inside a ramp and held through a fermata. A ramp ending where it begins, a target of no beats per minute, or a stretch below 1 raises `ArgumentError`.
152
+ - **Tempo ramps, "a tempo", and fermatas on a flow.** `Flow#add_tempo_ramp(from:, to:, to_tempo: nil, text: nil)` adds a ramp between two positions, which may fall in the middle of a bar; `#add_a_tempo(position, text: "a tempo")` restores the tempo in force at the start of the latest ramp before it; and `#add_fermata(position, span, stretch: 2)` holds a rhythmic value's span from a position, or `#add_fermata(position, to:)` runs to a later one. Each takes positions or codes for them and allocates the bars it reaches. `#remove_tempo_ramp`, `#remove_a_tempo`, and `#remove_fermata` remove one by its start, and `#tempo_ramps`, `#a_tempos`, and `#fermatas` list them in order. Overlapping ramps, overlapping fermatas, a tempo change or an "a tempo" strictly inside a ramp, and a meter change that would leave one of them on a count its bar no longer has raise `ArgumentError`; ramps and fermatas may touch, a fermata may fall inside a ramp, and a tempo change may sit at a ramp's start or end.
153
+ - `Flow#tempo_at(bar)` answers the tempo as written: in a bar that begins inside a ramp, the tempo at its start; at or after its end, its target, which with none written is the tempo change there, or else ×3/4 of the start (×4/3 for accel., accelerando, and stringendo) with `metronome_mark: false`; and after an "a tempo", the tempo it restores. A fermata leaves it unchanged. `Flow#sounding_tempo_at(position)` answers the tempo sounding there, interpolated inside a ramp.
154
+ - `Flow#conductor` answers a `Conductor` built from the flow's meters, tempos, ramps, "a tempo" points, and fermatas, with clock zero at the downbeat of the earliest bar, pickup included. Ramp targets and "a tempo" points become tempo events, and a tempo change wins where it shares a position with one.
155
+ - Flow JSON writes the timeline's `"tempo_ramps"` (`{"from", "to", "tempo", "text"}`, leaving out a target or text that was not written), `"a_tempos"` (`{"position", "text"}`), and `"fermatas"` (`{"from", "to", "stretch"}`) as lists, empty when there are none. They are optional within schema 5: a document without them reads with none, and they are read after the meter changes, raising `ArgumentError` naming their path.
156
+ - MIDI writes tempo steps through ramps and fermatas, and reads ramps and fermatas back. `MIDI::TempoSteps` steps the tempo at each downbeat, at each count under a ramp at the average rate over the step, so the file's clock keeps time with `Flow#conductor` at every step, at both ends of every ramp and fermata, and at each "a tempo"; under a fermata the tempo is divided by its stretch and restored at its end, even at the end of the piece. Each pass through a repeat or jump writes its bars' steps again, so a jump into a ramp resumes at the tempo sounding there, and preflight refuses a step MIDI cannot hold, naming its position. A flow with no ramps or fermatas writes the same bytes as before. On import, a slower tempo restored to the one before within one bar is a fermata, its stretch read to three decimals; a run of at least two changes within bars, all faster or all slower, that settles into a tempo held until the next downbeat is a ramp to that tempo, marked "rit." or "accel."; and a change within a bar back to the tempo the latest ramp began from is an "a tempo". Other changes within a bar are still skipped.
157
+ - ABC reads and writes tempos and the timeline's fermatas, ramps, and "a tempo" points. It reads `Q:` in the header, as an inline `[Q:…]`, or as a body line: `1/4=120`, `3/8=60`, quoted tempo text before or after the number or alone, and the legacy `Q:120` and `Q:C=120` in unit note lengths; beats that no single note value spans keep only the text. It reads `H`, `!fermata!`, `+fermata+`, and `!invertedfermata!` on notes, chords, and rests as timeline fermatas, from a tie's later link where one is written there and on the event before a bar line where one stands there, and reads "rit.", "rall.", "accel.", "a tempo", and their kin from `"^…"`, `"_…"`, `"<…"`, `">…"`, and `"@x,y…"` annotations as ramps and "a tempo" points, dropping other annotation text; chord symbols still raise. It writes `M:C` and `M:C|`, the opening tempo as `Q:` unless it is ♩=120, each tempo change and ramp target as `[Q:…]` where a note starts or on the final barline, each ramp's text and each "a tempo" as a `"^…"` annotation, and `!fermata!` on the note or rest where each fermata begins. `Notation::FermataMarks`, `Notation::TempoMarkReading`, and `Notation::TempoMarks` hold the reading and writing rules every format shares, and the writers' preflight checks cover fermatas, ramps, and "a tempo" points. The ABC writer raises `RenderError` for a fractional tempo, text holding a double quote, a `[Q:…]` where no note starts, and a ramp that would not read back as written: one whose text is no ramp word, one with a fermata beginning inside it, and one with no target ending where no tempo, "a tempo", ramp, fermata, or the end of the music would end it.
158
+ - ABC reads meter changes after the opening, from a body `M:` line or an inline `[M:…]` field, `C` and `C|` included, so `[M:C]` after `M:4/4` changes only the symbol. A field in any one voice changes the flow's meter at its bar, and the voices are read together so every voice's music in that bar is placed in the new meter. The first field read for a bar wins, one that restates the meter in force makes no change, and one partway through a bar takes effect at the next downbeat. A meter or key field that a voice states before its first note sets the flow's opening meter or key, as a LilyPond `\time` or `\key` at the start does, rather than changing it at the first bar; the first stated wins, with the voices taken in the order they are declared. A tuplet's default count follows the meter of its bar.
159
+ - ABC reads key changes after the opening, from a body `K:` line or an inline `[K:…]` field, by the same rules as meter changes: a field in any one voice changes the flow's key at its bar, the first read for a bar wins, a restatement of the same signature and name makes no change, and one partway through a bar changes the flow at the next downbeat. A voice spells its notes in a key it states from that point on, even partway through a bar or where the flow kept another voice's key, until the flow's key changes at a later bar; the other voices spell in the flow's key. A field holding only terms such as `clef=bass` changes nothing.
160
+ - ABC writes each meter change and key change as an inline `[M:…]` or `[K:…]` field opening its bar, after any volta and part label, `[M:C]` and `[M:C|]` included, and spells each bar's notes in the key in force there, so a note tied into a change reads back at its pitch. It raises `RenderError` only for a change at a bar where no voice sounds, where it had refused every change after the opening. `RenderPlan#measure_key_at` answers the key in force in a part at a bar.
161
+ - ABC writes every voice of a flow. Each is declared with a `V:` field before `K:`, named for its role where the role holds no whitespace, `]`, `%`, or `=` and no other voice has it, and otherwise by its place in the flow, and the voices' lines are written in turn, four bars at a time, as a score is laid out in systems. Every voice writes every bar: one it is silent in, before it enters or after it ends, is written as whole-bar rests, and the bar where it ends early is filled with them, so each reads back as rests. Bar lines, repeats, voltas, and meter and key changes are written in every voice; tempo marks, ramps, "a tempo" points, part labels, and navigation in the first voice only; a part's dynamics and hairpins in its first voice; and a timeline fermata on every voice's note sounding where it begins. A flow of one voice is written as before, with no `V:` field, except that a voice entering after the first bar is written with whole-bar rests before it rather than from its first note, which had read back a bar or more early. `RenderPlan#lead?`, `#music_end`, `#music_end_fraction`, and `#fermata_carriers` are shared by every format's plan.
162
+ - Transposed ABC output writes each voice in its own part's written key, and a part that picks up an instrument in another key as a `[K:…]` in its voices there. Wherever any part's key changes, or the parts open in different written keys, every voice writes its own part's key, so that each reads back spelled in its own key.
163
+ - The LilyPond reader reads `\tempo` with text, a metronome mark, or both, `\fermata` and its variants on notes and rests as timeline fermatas, and "rit.", "rall.", "accel.", and "a tempo" from `^"…"` and `^\markup` annotations, dropping other annotation text instead of raising.
164
+ - The LilyPond writer writes `\tempo` with text, a metronome mark, or both, `\fermata` on every voice event sounding where a fermata begins, and a tempo ramp's words at its start and its target `\tempo` at its end.
165
+ - The MEI reader reads `@fermata` and `<fermata>`, by `@startid` across measures or by `@tstamp`, into timeline fermatas, and the writer writes a `<fermata>` on every voice event sounding where a fermata begins.
166
+ - The MEI reader and writer keep tempo text in `<tempo>`, including text in `<rend>` and text with no `@mm`, which reads with `metronome_mark: false`.
167
+ - The MEI reader and writer read and write tempo ramps as `<tempo func="continuous">` with `@tstamp2` or `@endid`, toward the numbered `<tempo>` at the ramp's end, and "a tempo" points as `<tempo>` text.
168
+ - The MEI reader and writer keep the common- and cut-time symbols through `@meter.sym` and `<meterSig @sym>`.
169
+ - The MusicXML writer writes tempo text as `<words>` beside a `<metronome>` and a `<sound tempo>`, ramps as words with `<dashes>` to their target, "a tempo", `<fermata>` on every voice event sounding where a fermata begins, and the time `symbol` for common and cut time.
170
+ - The MusicXML reader reads `<fermata>` on notes, rests, and barlines into timeline fermatas, `<metronome>` and `<sound tempo>` anywhere in a bar, `<words>` as tempo text beside them or when they begin with a named tempo, ramps from ramp words and their `<dashes>`, "a tempo", and `<time symbol>`.
171
+ - The kern reader reads `;` fermatas on notes, rests, and chords into the timeline's fermatas, and the writer writes `;` on every voice event sounding where a fermata begins.
172
+ - The kern reader and writer keep common and cut time as `*met(c)` and `*met(c|)`, including a change of sign alone.
173
+ - The kern reader reads `!!!OMD` as the opening tempo's text and `*MM[Andante]` as tempo text, and the writer writes both; a flow with tempo ramps renders to kern at each bar's authored tempo.
174
+ - A melisma's held notes carry extenders. `NoteEvent#extend_syllable(verse: 1)` marks a note as holding the syllable of the voice event just before it, which must sing or hold that verse and end where the note starts; a verse holds a syllable or an extender, never both. `#extends_syllable?` and `#extended_verses` answer them, and `VoiceEvent#melisma_continues_after?` whether the next event holds a verse. An extender is not sung, so nothing that reads syllable text sees one, and a rest refuses one with `ArgumentError`.
175
+ - Flow JSON writes a held note's extenders among its `"syllables"`, by verse, as `{"extend": true}` with a `"verse"` other than 1, an optional entry within schema 5. Reading adds a voice's extenders after its events, so one listed before the syllable it holds reads back, and one with nothing before it to hold, or with text, raises `ArgumentError` naming its path.
176
+ - MusicXML writes a melisma's line as `<extend type="start"/>` in its syllable's `<lyric>`, then a `<lyric>` holding only `<extend type="continue"/>` on each held note and `type="stop"` on the last, on a chord's lead note and a tie's first link. A hyphenated syllable's held notes write nothing. The reader holds the notes those marks name, and an extend-only `<lyric>` with no type; after a bare or `start` `<extend/>` with no marks following, or a hyphenated syllable, it holds each following note with nothing in that verse, up to the next sung note or rest.
177
+ - MEI writes `@con="u"` on a word-final syllable whose melisma continues, and keeps `"d"` on a hyphenated one, since `@con` holds one value; held notes write no `<verse>`. The reader, as MEI names no end, holds each note after a `@con="u"` or a hyphenated syllable that has nothing in that verse, up to the next sung note or rest.
178
+ - kern writes a trailing underscore on a word-final syllable whose melisma continues (`Lord_`), as Verovio reads it, with nulls on its held notes. The reader strips that underscore rather than keeping it in the text, and holds each following note with nothing in that verse, up to the next sung note or rest, as it does after a hyphenated syllable. A lone `_` or `|` marks a held note itself, and is skipped with nothing before it to hold.
179
+
180
+ ### Changed
181
+
182
+ - **Breaking.** A `RhythmicValue` is one written value: a unit, up to three dots, and an optional tuplet ratio. Ties belong to the new `RhythmicDuration`, so `RhythmicValue` loses `#tied_value`, `#tied_chain`, `#append_tied`, `#uniform_tuplet_ratio`, `#link_fraction` (its `#fraction` is now its own length), `#single_value_name`, the `tied_value:` keyword, and `.from_tied_words`. `RhythmicValue.get` answers nil for a tied name, and for any name it cannot read instead of a value with no unit, and `RhythmicValue.new` raises `ArgumentError` for an unknown unit. A `RhythmicValue` no longer equals a `RhythmicUnit` or a `RhythmicDuration` of the same name; use `#same_duration_as?` to compare lengths across them. Serialized flows keep their `"rhythmic_value"` key and names, so this change asks nothing of a saved flow beyond the schema-5 upgrade.
183
+ - Voice events, `Content::Note`, and the rudiment `Note`, `Rest`, and `UnpitchedNote` hold a `RhythmicDuration`, read as `#rhythmic_duration` where they answered `#rhythmic_value`, and `#with_rhythmic_value` is `#with_rhythmic_duration`. `Voice#place` takes any duration or name, as before. `RhythmicElement` no longer delegates `#unit`, `#dots`, or `#tied_value`, since a duration may hold several values; ask its `#values` or `#single_value`. `CantusFirmus::Example#to_flow` takes `rhythmic_duration:` where it took `rhythmic_value:`. A voice event and a `Content::Note` raise `ArgumentError` for a rhythmic duration they cannot read, and `Rudiment::Note.get`, `Rest.get`, and `UnpitchedNote.get` answer nil for one, where they used to fail on the hollow value. "tied to" is read in any case.
184
+ - Grace notes, tuplet group units, and tempo beats take a `RhythmicValue`. A `Tempo` raises `ArgumentError` for a beat it cannot read or one that is tied, and a meter whose beat no note value spans, such as 6/1, answers a nil `#beat_value`.
185
+ - `DottedDuration.rhythmic_duration_for` answers the duration spanning a fraction, tied where it must be, and `.rhythmic_value_for` now answers only a single value, nil where a tie is needed. `DottedDuration.rest_value` is `.rest_duration`, `.tied` answers a `RhythmicDuration`, `BarSplitter::Segment#rhythmic_value` is `#rhythmic_duration`, and `MIDI::NoteValues#value` is `#rhythmic_duration`.
186
+ - The style guidelines that look for a plain quarter or half note judge a tied note by its first written value, so grading is unchanged, and `AllowFifthSpeciesRhythmicValues` checks every value of a tied note for its unit and dots.
187
+ - To migrate, read a placed note's length from `rhythmic_duration`, and walk ties over its values:
188
+
189
+ ```ruby
190
+ # Before
191
+ voice_event.rhythmic_value.tied_chain.each { |link| link.link_fraction }
192
+ voice_event.rhythmic_value.unit_name
193
+
194
+ # After
195
+ voice_event.rhythmic_duration.values.each { |value| value.fraction }
196
+ voice_event.rhythmic_duration.values.first.unit_name
197
+ ```
198
+ - The MusicXML reader trusts the written values of a measure whose rounded `<duration>`s overflow the bar while its `<type>`s, dots, and `<time-modification>`s fill it exactly in every voice, instead of refusing it as overfull. `<backup>` and `<forward>` land on the nearest written onset. Files written at divisions that cannot express their notes, such as music21's Beethoven op. 59 no. 2, now read; the corpus sweep reads 1,001 of 1,067 files.
199
+ - **Breaking.** `MusicXML::BarMap`, `MusicXML::VoiceTrack`, `MusicXML::TieFolding`, `MusicXML::VoicePlacer`, and `MusicXML::ConcertTransposition` move to `HeadMusic::Notation`, shared with MEI import, and `MusicXML::PitchReader`'s spelling helpers move to `Notation::PitchSpelling`. `MusicXML::XmlReading` and `MusicXML::ParsePreflight` remain. With the move, `VoicePlacer.new` requires `tuplet_reader:` (and takes `trim_edges:`), `ConcertTransposition.new` takes `errors:`, and the readings `BarMap` and `TieFolding` take must answer `errors` and `where`.
200
+ - `MusicXML.parse` raises `ParseError` on a document nesting elements deeper than 256.
201
+ - ABC reads a `P:` header, the playing order, and ignores it rather than raising. A repeat read from ABC plays as many times as the highest pass its endings name, rather than twice.
202
+ - ABC reads a `V:` voice id as the words before its `name=value` terms, in the header and the body alike, so `V:S clef=bass` continues voice `S` rather than starting a voice of that name, and `V:cantus firmus` keeps both words. A `V:` field holding only `name=value` terms names no voice, so the music stays in the voice it was in. A `K:` value skips its `clef=` and other `name=value` terms, and a header `K:` naming no key, empty or as `K:clef=bass`, is read as C major, as abcm2ps engraves it.
203
+ - ABC reads a tune's voices together in time order, rather than each line as it comes, so a tie, slur, or tuplet carries across a `V:` line into the voice's next line instead of ending there.
204
+ - **Breaking.** `ABC::BarWriter#bar` takes the key signature of its bar, and `ABC::PitchWriter#start_new_bar` takes an optional one, since a bar may open in a new key. `ABC::RenderPreflight.check!` no longer takes `transposed:`.
205
+ - **Breaking.** The ABC writer writes a flow of several voices, a meter or key change after the opening, and a transposed part that changes instrument, where it raised `RenderError`, and the ABC reader reads body `M:` and `K:` fields and inline `[M:…]` and `[K:…]`, where it raised `UnsupportedFeatureError`. The writer checks the dynamics of each voice with its part's where it writes them, rather than every part's and voice's together, so two voices may now hold different levels or hairpins at once.
206
+ - **Breaking.** `HeadMusic::Content::Placement` is `HeadMusic::Content::VoiceEvent`, a name for the time-filling notes, chords, and rests of a voice, apart from the point events such as dynamics still to come. The constant is gone, not aliased. `HeadMusic::Notation::PlacementValidation` is `VoiceEventValidation` to match.
207
+ - **Breaking.** `VoiceEvent` is abstract, with two subclasses: `NoteEvent`, which holds one or more `Soundable`s and raises `ArgumentError` without one, and `RestEvent`, which is silent and cannot sing. `VoiceEvent.build` answers whichever the sounds call for, and `Voice#place` uses it. A note placed where a rest already stands now replaces the rest rather than turning it into a note, and keeps its beam flag; a rest placed on a note still leaves the note alone. Flow JSON that puts syllables on a rest raises `ArgumentError` naming its path.
208
+ - **Breaking.** Methods and arguments named for placements are named for voice events. `Voice#placements` is `Voice#voice_events`, `Voice#last_placement` is `#last_voice_event`, `Voice#rests` is `#rest_events` beside a new `#note_events` (which, unlike `#notes`, includes unpitched note events), `Position#within_placement?` is `#within_voice_event?`, and a style guideline's `rests` and `placements` follow. `Voice#place` keeps its name.
209
+ - **Breaking.** The serialization schema goes to 5: each voice's `"placements"` is now `"voice_events"`, and nothing else changes. `Flow.from_h` and `Project.from_h` refuse a v4 document and name the reader for it: `Flow.from_v4_h` and `Project.from_v4_h` read one by renaming the key, and are kept through 23.x so persisted documents can be read and saved again.
210
+ - **Breaking.** `Meter.get` returns a `Meter` it is given unchanged, rather than the registered meter with the same numerals, so a meter's symbol survives it.
211
+ - **Breaking.** `Tempo` compares by value, over its beat value, beats per minute, text, and metronome mark, with `eql?` and `hash` to match, where it had compared by identity. `Tempo.get` finds a named tempo in any case and keeps the spelling it was given as its text, writing `_` as a space, so `Tempo.get("allegro").text` is `"allegro"`, and answers it with `metronome_mark: false`; it memoizes on the exact identifier. A metronome mark such as `"q = 120"`, or a word it does not know, still has no text.
212
+ - **Breaking.** kern import times a `**dynam` level on a row where no `**kern` spine attacks by the Humdrum convention: such rows split the time between the timed rows around them evenly. A level written beside a held note, which had been placed at the next attack, now falls in the middle of the note.
213
+ - **Breaking.** Flow JSON refuses a voice event's articulations or ornaments that name one marking twice through an alias or another spelling, such as `["mordent", "lower_mordent"]` or `["accent", "Accent"]`, with the error a repeated key raises. It had read them as one.
214
+ - The kern, MEI, and LilyPond writers refuse an unpitched grace note with one message: `cannot render the unpitched grace note "snare drum" before 1:1:000: percussion rendering is not yet supported`.
215
+ - `MEI::RenderPlan::StaffEntry` is `MEI::StaffNumbering::Entry`, `MEI::RenderPlan::Item` is `MEI::WrittenItems::Item`, `MEI::LayerContainers::BEAM_LEVELS_BY_DUR` is `MEI::BeamContainers::LEVELS_BY_DUR`, and `MEI::MarkingEvents::ORNAMENTS` is `MEI::AttachedMarks::ORNAMENTS`.
216
+ - `rexml` is now a runtime dependency, in preparation for reading MusicXML and MEI.
217
+ - `Project#add_flow` adopts a player that a part already carries into the project's `players`, rather than leaving it out, and raises `ArgumentError` when that player belongs to another project.
218
+ - The MusicXML and LilyPond writers render a note that crosses a barline as tied notes, one per bar, instead of raising `RenderError`. A chord ties each of its pitches, and a rest splits into consecutive rests. Fourth-species flows now render.
219
+ - The LilyPond parser reads a tie across a bar check, such as `c'1~ | c'2`, as one placement instead of raising `ParseError`. A `\key` or `\time` written between the halves of such a tie takes effect at that barline; one in the middle of a bar still raises.
220
+ - The LilyPond writer renders a short final bar, such as one that balances a pickup, when every voice ends there together: it leaves the bar check off that bar, and a tacet voice rests only as long as the music, followed by spacers where a dynamic comes after the music in that bar. Voices that end at different points short of the final bar still raise `RenderError`.
221
+ - **Breaking.** The MusicXML writer's work title defaults to the title of the flow's `Work`, with the flow's name as the `<movement-title>`, so a flow that cites a work, such as one read from kern, writes it back. Pass `work_title:` to choose another; `work_title: nil` still falls back to the `Work`'s title.
222
+ - **Breaking.** `MusicXML::MarkingWriter` is `MusicXML::MarkingElements`, since the reader uses it too. The old constant is gone, not aliased.
223
+ - `MinimumNotes` with no minimum configured raises `KeyError`, as `MinimumMelodicIntervals` does, rather than `NameError`.
224
+ - **Breaking.** A contour guide grades a miss by its degree, at φ⁻² per degree, rather than every miss at φ⁻². An arch is read as a rise to its climax and a fall from it, each graded as a direction whose turn back may reach only half its net change, and a valley as the mirror. A small dip no longer turns an arch into a wave, so Fux's D F E D G F A G F E D and most published cantus firmi now pass `arch_contour_melody` and fail `wave_contour_melody`. A valley submitted to `arch_contour_melody` grades about 0.416 rather than 0.618. A wave needs three or more legs, the rises and falls of at least a minor third, and must not read as a single arch or valley. Ascending and descending are directions, read from the endpoints and the headway between them, with a degree for each condition failed: starting within a whole step of one extreme, ending within a whole step of the other, ending at least a minor third beyond where the melody began, and never turning back three quarters as far as that net change. So a melody that wanders between its endpoints matches if it makes headway, and one whose endpoint sits a step from its extreme now matches too. Static is unchanged. A miss names what is wrong, such as "Write the arch contour: reverse the rise and fall.", in place of "Write a melody with the arch contour."
225
+ - **Breaking.** A diatonic interval is named by its number through the seventeenth, as every locale translates it: `"major seventeenth"` rather than `"two octaves and a major third"`.
226
+ - **Breaking.** A plain LilyPond `\time 4/4` or `\time 2/2` reads as common or cut time, as LilyPond prints it, and `\numericTimeSignature` and `\defaultTimeSignature` switch the reading; the writer writes `\numericTimeSignature` before a 4/4 or 2/2 with no symbol, and `\defaultTimeSignature` when a symbol returns.
227
+ - **Breaking.** The LilyPond writer raises `RenderError` for a fractional tempo, which it used to drop with every other tempo.
228
+
229
+ ### Removed
230
+
231
+ - **Breaking.** `Credits.from_h`, which nothing in the gem called. `Credits.new(level, array)` reads the same array of credit hashes, `nil` included.
232
+
233
+ ### Fixed
234
+
235
+ - `Quality.get` reads `"doubly augmented"` and `"doubly diminished"` as written, so a doubly altered `DiatonicInterval` answers `doubly_augmented?`, `doubly_diminished?`, and `shorthand` (`"AA4"`) instead of raising `NoMethodError`. A quality's `to_s` is its name as written, and it equals either spelling.
236
+ - A diatonic interval altered past the next octave is named for it. An augmented fourteenth was named `" fourteenth"`, and an augmented compound seventh, or any doubly altered interval past the seventeenth, raised `NoMethodError`. An augmented compound octave counts one more octave and an augmented unison, so C3 to C♯6 is "three octaves and an augmented unison", not "three octaves and an augmented octave".
237
+ - The ABC parser names the line of a tie between notes of different pitches; it gave none.
238
+ - An MEI `@tuplet` run whose entries are not all scaled alike is left ungrouped, as when they are scaled by different ratios. A run of triplet and unscaled entries was grouped as a triplet when a triplet entry came first.
239
+ - The MEI, LilyPond, and ABC writers print an accidental on each chord member that shares a letter and octave with another under a different alteration, so F4 with F♯4 in C major shows its natural; LilyPond writes them as `<fis'! f'!>` so a later F in the bar prints correctly too.
240
+ - The LilyPond reader reads notes carrying a forced (`!`) or cautionary (`?`) accidental sign, such as `fis'!4`, rather than refusing them.
241
+ - Adding a tempo, meter, or key change to a flow, and growing its bars, no longer revisit every earlier change or bar, so a long flow with a change in each bar builds in linear time: 10,000 bars of tempo changes took 7.9 s and now take 0.12 s.
242
+ - `Key#relative` spells flat-side keys with flats: E♭ minor's relative is G♭ major, not F♯ major, and G♭ major's is E♭ minor, not D♯ minor.
243
+ - `Mode#relative_major` spells flat-side modes with flats, so E♭ Dorian's relative major is D♭ major, not C♯ major.
244
+ - MusicXML import folds tied chains in linear time; a long chain of tied notes had taken quadratic time.
245
+ - The MusicXML writer raises `RenderError` for a character XML forbids in a syllable, player or instrument name, rehearsal mark, Work title, or a `work_title:` or `arranger:` option, and for U+FFFE, U+FFFF, or text that is not valid UTF-8 or is non-ASCII text in another encoding, instead of writing ill-formed XML.
246
+ - The MusicXML writer renders a meter whose beat is longer than any note value, such as 6/1 or 12/1, leaving its notes unbeamed, instead of raising `NoMethodError`.
247
+ - `HeadMusic::Notation::ABC.parse` keeps the grace notes before a tied note, which it dropped when it folded the tie into one event.
248
+ - A transposed layout writes grace notes at written pitch, as it does their main notes, in MusicXML, LilyPond, and ABC. They had kept their concert pitch.
249
+ - **A `Content::Position` carried whole counts across a meter change in the origin bar's count unit.** A dotted half tied to a dotted half from `1:1` in 6/8, with 2/2 from bar 2, ended at `5:1:000`, its six leftover eighths spent as six halves, rather than at `2:2:960`. The carry now stops at the first bar with a different count unit and restates what is left there as ticks.
250
+ - The kern reader no longer raises "A null token … falls where no note is sounding" on a row holding only grace notes.
251
+ - MusicXML no longer raises beaming a compound-meter piece whose tuplets leave its beat without whole divisions.
252
+ - ABC tunes with slurs, and LilyPond files with slurs, phrasing slurs, or named slurs, import instead of raising `UnsupportedFeatureError` or, for `\(` and `\=1(`, `ParseError`.
253
+ - ABC tunes with a staccato dot, `T` or `M` shorthand, or `!…!` decorations, and LilyPond files with articulations or dynamics, import instead of raising. kern tokens with a `z` sforzando no longer raise `UnsupportedFeatureError`.
254
+ - The MusicXML writer writes a voice's dynamic that falls after its last note in a bar, as a `<direction>` after that note, instead of leaving it out.
255
+ - The ABC writer no longer writes a zero-length note (`C8-|C0`) for a note that crosses a barline and ends on a later one.
256
+ - `Position#+` adds a rhythmic value's subticks, so a length finer than a tick carries instead of being dropped: a dotted 256th from the downbeat ends at `1:1:022:120`, not `1:1:022`. `Position#advance(subticks:)` moves a position by subticks, and `BarSplitter.offset_in_bar` counts a position's subticks.
257
+ - `RhythmicValue` defines `eql?` and `hash` to match `==`, so `uniq` and `tally` count equal values together. `NotesSameLength` now finds the most common value in a line, rather than taking the first note's, and marks fewer notes in lines of mixed values.
258
+ - The MusicXML writer resets `<transpose>` to nothing where a transposed part changes back to an instrument in C, rather than leaving the old transposition in force; writes a clef change (`Staff#change_clef`) as a `<clef>` in the measure it falls in, numbered for its staff; and writes `<clef-octave-change>` for octave clefs such as the tenor's treble clef.
259
+ - `Flow#performance_order` plays the bar a final tied note reaches, rather than stopping at the bar it starts in.
260
+ - `Conductor#musical_to_clock` counts each bar in its own meter: 6:1 after a change from 4/4 to 3/4 at bar 5 is 9.5 s at ♩=120, not 10.0 s. `Conductor#clock_to_musical` no longer stops a thousand bars past the start.
261
+ - `Tempo.get("Andante")` answers an andante, ♩=78, instead of ♩=120, and `Tempo.get("Moderato")` a moderato instead of ♩=0: a named tempo is found in any case.
262
+ - MEI keeps an opening tempo's beat other than a quarter, in a `<tempo>` beside the `<scoreDef>`'s `@midi.bpm`.
263
+
264
+ ### Security
265
+
266
+ - `rexml` must be at least 3.4.2, since 3.3.3 through 3.4.1 are open to a denial of service from XML with several XML declarations (CVE-2025-58767).
267
+
10
268
  ## [22.0.0] - 2026-09-24
11
269
 
12
270
  Four counterpoint stories, [Require the Species Rhythm](https://github.com/roberthead/head_music/tree/main/user-stories/done/require-the-species-rhythm.md), [Embellish Fifth Species Suspensions](https://github.com/roberthead/head_music/tree/main/user-stories/done/embellish-fifth-species-suspensions.md), [Grade Every Fux Fifth-Species Figure](https://github.com/roberthead/head_music/tree/main/user-stories/done/grade-every-fux-fifth-species-figure.md), and [Fifth Species Guide Improvements](https://github.com/roberthead/head_music/tree/main/user-stories/done/fifth-species-guide-improvements.md). Every species guide now insists on the rhythm its species teaches, fifth species accepts the decorated suspensions Fux allows and requires the mixture that defines it, and every two-voice fifth-species figure in Gradus is graded and pinned. Fux's lines are graded as evidence, not as an oracle: where one of them outruns a rule that every other source keeps, the mark stands and is recorded as his liberty. Two grades move, both in the direction the pedagogy says they should, and the count guidelines now grade the solo and cantus-firmus lines they had passed on nothing; everything else is additive, including ties across bar lines in ABC.
data/CLAUDE.md CHANGED
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4
4
 
5
5
  ## Project Overview
6
6
 
7
- HeadMusic is a Ruby gem for Western music theory. It provides a comprehensive toolkit for working with pitches, scales, intervals, chords, and musical analysis. The gem supports internationalization with translations in 7 languages.
7
+ HeadMusic is a Ruby gem for Western music theory. It provides a comprehensive toolkit for working with pitches, scales, intervals, chords, and musical analysis. The gem supports internationalization with translations in 6 languages, plus British English.
8
8
 
9
9
  ## Development Commands
10
10
 
@@ -17,6 +17,7 @@ bundle exec rake validate # tests with coverage, linting, security audit, code
17
17
  bundle exec rake # tests WITH coverage (the default task, same as `rake spec`)
18
18
  bundle exec rspec # tests WITHOUT coverage, so a single file can run on its own
19
19
  bundle exec rake coverage # opens the last coverage report
20
+ bundle exec rake oracles # only the specs checked against outside tools and files, each missing one saying how to set it up
20
21
  ```
21
22
 
22
23
  ### Git Etiquette
@@ -53,6 +54,7 @@ The codebase follows a domain-driven design with clear module boundaries:
53
54
  - Abstract concepts: pitch, interval, scale, chord, key
54
55
  - Duration concepts (without visual representation)
55
56
  - Factory methods: `.get()` for most rudiments
57
+ - Future: catalogs of articulations, ornaments, and dynamics
56
58
 
57
59
  2. **HeadMusic::Notation** - Visual music notation and representation
58
60
  - Staff positions, lines, spaces, ledger lines
@@ -60,7 +62,7 @@ The codebase follows a domain-driven design with clear module boundaries:
60
62
  - Clef placement and rendering
61
63
  - Notehead shapes, stems, flags, beams
62
64
  - Accidental placement rules
63
- - Future: ties, slurs, articulations, dynamics
65
+ - Future: ties, slurs
64
66
 
65
67
  3. **HeadMusic::Instruments** - Instrument definitions
66
68
  - Instrument families and classification
@@ -70,7 +72,7 @@ The codebase follows a domain-driven design with clear module boundaries:
70
72
 
71
73
  4. **HeadMusic::Content** - Musical composition representation
72
74
  - Compositions, voices, bars, positions
73
- - Notes in context (pitch + duration + placement)
75
+ - Voice events in context: note events and rest events (sounds + rhythmic value + position)
74
76
  - Uses HeadMusic::Time for temporal positioning
75
77
 
76
78
  5. **HeadMusic::Time** - Temporal infrastructure
@@ -116,7 +118,7 @@ Module loading order matters and is defined in `lib/head_music.rb`.
116
118
 
117
119
  The gem supports multiple languages through the HeadMusic::Named mixin:
118
120
  - Translations in `lib/head_music/locales/`
119
- - Languages: en, de, es, fr, it, ja, nl
121
+ - Languages: en, de, es, fr, it, ru, and en_GB for British spellings
120
122
  - Use `I18n.locale = :de` to change language
121
123
 
122
124
  ### Testing Patterns
data/Gemfile CHANGED
@@ -14,7 +14,7 @@ gem "activesupport", "~> #{activesupport_version}.0" unless activesupport_versio
14
14
  gem "standard", require: false
15
15
 
16
16
  group :test do
17
- # rubocop is not listed directly: standard pins it (~> 1.88.0) and must
17
+ # rubocop is not listed directly: standard pins it (~> 1.91.0) and must
18
18
  # lead. Listing it here let Dependabot bump it past standard, which the
19
19
  # resolver "fixed" by downgrading standard 1.56 -> 1.35.
20
20
  gem "rubocop-rspec", require: false