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
data/Gemfile.lock CHANGED
@@ -1,15 +1,16 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- head_music (22.0.0)
4
+ head_music (23.0.0)
5
5
  activesupport (>= 7.0, < 10)
6
6
  humanize (>= 2, < 4)
7
7
  i18n (~> 1.8)
8
+ rexml (~> 3.4, >= 3.4.2)
8
9
 
9
10
  GEM
10
11
  remote: https://rubygems.org/
11
12
  specs:
12
- activesupport (8.1.3.1)
13
+ activesupport (8.1.4)
13
14
  base64
14
15
  bigdecimal
15
16
  concurrent-ruby (~> 1.0, >= 1.3.1)
@@ -30,7 +31,7 @@ GEM
30
31
  ice_nine (~> 0.11.0)
31
32
  thread_safe (~> 0.3, >= 0.3.1)
32
33
  base64 (0.3.0)
33
- bigdecimal (4.1.2)
34
+ bigdecimal (4.1.3)
34
35
  bundler-audit (0.9.3)
35
36
  bundler (>= 1.2.0)
36
37
  thor (~> 1.0)
@@ -87,7 +88,7 @@ GEM
87
88
  i18n (1.15.2)
88
89
  concurrent-ruby (~> 1.0)
89
90
  ice_nine (0.11.2)
90
- json (2.21.2)
91
+ json (3.1.0)
91
92
  kramdown (2.5.2)
92
93
  rexml (>= 3.4.4)
93
94
  language_server-protocol (3.17.0.6)
@@ -101,7 +102,7 @@ GEM
101
102
  drb (~> 2.0)
102
103
  prism (~> 1.5)
103
104
  ostruct (0.6.3)
104
- parallel (2.1.0)
105
+ parallel (2.3.0)
105
106
  parser (3.3.12.0)
106
107
  ast (~> 2.4.1)
107
108
  racc
@@ -117,7 +118,7 @@ GEM
117
118
  parser (~> 3.3.0)
118
119
  rainbow (>= 2.0, < 4.0)
119
120
  rexml (~> 3.1)
120
- regexp_parser (2.12.0)
121
+ regexp_parser (2.13.1)
121
122
  rexml (3.4.4)
122
123
  rspec (3.13.2)
123
124
  rspec-core (~> 3.13.0)
@@ -135,8 +136,8 @@ GEM
135
136
  diff-lcs (>= 1.2.0, < 2.0)
136
137
  rspec-support (~> 3.13.0)
137
138
  rspec-support (3.13.7)
138
- rubocop (1.88.2)
139
- json (~> 2.3)
139
+ rubocop (1.91.0)
140
+ json (>= 2.3)
140
141
  language_server-protocol (~> 3.17.0.2)
141
142
  lint_roller (~> 1.1.0)
142
143
  parallel (>= 1.10)
@@ -179,12 +180,12 @@ GEM
179
180
  tty-which (~> 0.5.0)
180
181
  virtus (~> 2.0)
181
182
  securerandom (0.4.1)
182
- sexp_processor (4.17.5)
183
+ sexp_processor (4.17.6)
183
184
  simplecov (1.0.3)
184
- standard (1.56.0)
185
+ standard (1.57.0)
185
186
  language_server-protocol (~> 3.17.0.2)
186
187
  lint_roller (~> 1.0)
187
- rubocop (~> 1.88.0)
188
+ rubocop (~> 1.91.0)
188
189
  standard-custom (~> 1.0.0)
189
190
  standard-performance (~> 1.8)
190
191
  standard-custom (1.0.2)
@@ -198,10 +199,10 @@ GEM
198
199
  tty-which (0.5.0)
199
200
  tzinfo (2.0.6)
200
201
  concurrent-ruby (~> 1.0)
201
- unicode-display_width (3.2.0)
202
- unicode-emoji (~> 4.1)
203
- unicode-emoji (4.2.0)
204
- uri (1.1.1)
202
+ unicode-display_width (3.3.0)
203
+ unicode-emoji (~> 4.3)
204
+ unicode-emoji (4.3.0)
205
+ uri (1.1.2)
205
206
  virtus (2.0.0)
206
207
  axiom-types (~> 0.1)
207
208
  coercible (~> 1.0)
@@ -220,7 +221,6 @@ DEPENDENCIES
220
221
  head_music!
221
222
  kramdown
222
223
  rake (~> 13.0)
223
- rexml (~> 3.4)
224
224
  rspec (~> 3.0)
225
225
  rspec-its (~> 2.0)
226
226
  rubocop-rake
data/README.md CHANGED
@@ -14,7 +14,7 @@ The **head_music** Ruby gem provides a toolkit for working with Western music th
14
14
  - **Style Analysis**: Rules for species counterpoint and voice leading
15
15
  - **Internationalization**: Support for multiple languages (English, French, German, Italian, Russian, Spanish)
16
16
  - **Instrument Modeling**: Extensive database of musical instruments with ranges and properties
17
- - **Notation Formats**: Read ABC and LilyPond into compositions; write compositions as ABC, LilyPond, and MusicXML
17
+ - **Notation Formats**: Read ABC, LilyPond, and Humdrum `**kern` into compositions; write compositions as ABC, LilyPond, MusicXML, and `**kern`
18
18
 
19
19
  ## Installation
20
20
 
@@ -64,7 +64,17 @@ puts composition.voices.first.pitches.map(&:to_s) # => ["G3", "A3", "B3", "C4",
64
64
  puts composition.to_lilypond # => a complete LilyPond document
65
65
  ```
66
66
 
67
- The LilyPond reader covers absolute and `\relative` pitches, durations and dots, rests and whole-bar rests, chords, intra-bar ties, `\key`, `\time`, `\clef`, bar checks, `\header` title and composer, and `\new Staff` / `\new Voice` contexts. Constructs outside that subset (tuplets, lyrics, variables, articulations, and so on) raise an `UnsupportedFeatureError` rather than being skipped.
67
+ The LilyPond reader covers absolute and `\relative` pitches, durations and dots, rests and whole-bar rests, chords, ties (including across bar checks), `\key`, `\time`, `\clef`, bar checks, `\header` title and composer, `\new Staff` / `\new Voice` contexts, and grand staves and staff groups with `\change Staff`. Constructs outside that subset (tuplets, lyrics, variables, articulations, and so on) raise an `UnsupportedFeatureError` rather than being skipped.
68
+
69
+ ```ruby
70
+ # Read a Humdrum **kern file, such as a Bach chorale from KernScores
71
+ chorale = HeadMusic::Notation::Kern.parse(File.read("chor001.krn"))
72
+ chorale.parts.map { |part| part.player.name } # => ["Soprano", "Alto", "Tenor", "Bass"]
73
+ chorale.work.catalog_number # => "BWV 269"
74
+ puts chorale.to_kern # => the same spines back
75
+ ```
76
+
77
+ The kern reader covers pitches, durations and dots, rests, chords, ties across barlines, pickups and short final bars, repeat barlines, `*k`, key and mode designations, `*M`, `*MM`, `*clef`, `*I` names and vocal and piano codes, `*part` / `*staff` grouping, spine splits, joins, and exchanges, `**text` lyrics, and the reference records of a work. Tuplets, `*+`, and transposed spines raise an `UnsupportedFeatureError`; grace notes, ornaments, articulations, beams, and fermatas are dropped.
68
78
 
69
79
  ## Style Analysis
70
80
 
data/Rakefile CHANGED
@@ -1,6 +1,8 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rspec/core/rake_task"
3
- require "standard/rake"
3
+ require "rubocop/rake_task"
4
+
5
+ RuboCop::RakeTask.new
4
6
 
5
7
  # The block runs when the task does, just before rspec is launched.
6
8
  RSpec::Core::RakeTask.new(:spec) { ENV["COVERAGE"] = "true" }
@@ -27,15 +29,25 @@ rescue LoadError
27
29
  # bundler-audit not available
28
30
  end
29
31
 
30
- desc "Run RubyCritic code quality analysis"
32
+ desc "Run only the specs checked against outside tools and files, each missing one skipping with how to set it up"
33
+ RSpec::Core::RakeTask.new(:oracles) { ENV["ORACLES"] = "1" }
34
+
35
+ desc "Run RubyCritic code quality analysis, failing below .rubycritic.yml's minimum score or on any file graded D or F"
31
36
  task :rubycritic do
32
- sh "rubycritic lib"
37
+ require "json"
38
+ sh "rubycritic"
39
+ report = JSON.parse(File.read("tmp/rubycritic/report.json"))
40
+ failing = report["analysed_modules"].select { |file| %w[D F].include?(file["rating"]) }
41
+ next if failing.empty?
42
+
43
+ failing.each { |file| warn format("%s %5.2f %s", file["rating"], file["cost"], file["path"]) }
44
+ abort "RubyCritic graded #{failing.length} file(s) below C"
33
45
  end
34
46
 
35
47
  task default: :spec
36
48
 
37
49
  desc "Run all validation checks (tests, linting, security audit, code quality)"
38
- task validate: [:spec, :standard, "bundle:audit:check", :rubycritic]
50
+ task validate: [:spec, :rubocop, "bundle:audit:check", :rubycritic]
39
51
 
40
52
  desc "Open an irb session preloaded with this library"
41
53
  task :console do
@@ -47,6 +59,73 @@ task :coverage do
47
59
  sh "open coverage/index.html" if File.exist?("coverage/index.html")
48
60
  end
49
61
 
62
+ namespace :music_xml do
63
+ desc "Regenerate the MuseScore 4 and timewise fixtures (spec/fixtures/notation/music_xml/); MSCORE overrides the mscore path"
64
+ task :musescore_fixtures do
65
+ $LOAD_PATH.unshift File.expand_path("lib", __dir__)
66
+ require "head_music"
67
+ require "rspec/core"
68
+ require "shellwords"
69
+ require_relative "spec/support/musescore"
70
+
71
+ with_final_newline = ->(path) { File.write(path, File.read(path).chomp + "\n") }
72
+ MuseScoreFixtures.names.each do |name|
73
+ rendered = MuseScoreFixtures.path(name, "head_music")
74
+ File.write(rendered, MuseScoreFixtures.musicxml(name))
75
+ MuseScore.convert_file(rendered, MuseScoreFixtures.path(name, "musescore4"))
76
+ with_final_newline.call(MuseScoreFixtures.path(name, "musescore4"))
77
+ puts "Wrote #{name}"
78
+ end
79
+
80
+ stylesheet = "/Applications/Dorico 6.app/Contents/Resources/musicxml/parttime.xsl"
81
+ next puts("Skipped the timewise fixtures: #{stylesheet} not found") unless File.exist?(stylesheet)
82
+
83
+ MuseScoreFixtures::TIMEWISE.each do |name|
84
+ timewise = MuseScoreFixtures.path(name, "timewise")
85
+ sh "xsltproc --nonet --novalid #{stylesheet.shellescape} #{MuseScoreFixtures.path(name, "head_music").shellescape} > #{timewise.shellescape}"
86
+ with_final_newline.call(timewise)
87
+ puts "Wrote #{name} timewise"
88
+ end
89
+ end
90
+ end
91
+
92
+ namespace :mei do
93
+ desc "Regenerate the MEI golden documents (spec/fixtures/notation/mei/)"
94
+ task :golden_fixtures do
95
+ $LOAD_PATH.unshift File.expand_path("lib", __dir__), File.expand_path("spec", __dir__)
96
+ require "rspec/core"
97
+ require "spec_helper"
98
+
99
+ MEIFixtures.names.each do |name|
100
+ File.write(MEIFixtures.path(name), MEIFixtures.mei(name))
101
+ puts "Wrote #{name}"
102
+ end
103
+ end
104
+
105
+ desc "Regenerate the MuseScore 4 MEI exports (spec/fixtures/notation/mei/musescore/); MSCORE overrides the mscore path"
106
+ task :musescore_fixtures do
107
+ $LOAD_PATH.unshift File.expand_path("lib", __dir__)
108
+ require "head_music"
109
+ require "rspec/core"
110
+ require_relative "spec/support/musescore"
111
+
112
+ target_directory = File.expand_path("spec/fixtures/notation/mei/musescore", __dir__)
113
+ FileUtils.mkdir_p(target_directory)
114
+ sources = Dir[File.join(MuseScoreFixtures::DIRECTORY, "*.head_music.musicxml")].sort
115
+ failures = sources.filter_map do |source|
116
+ name = File.basename(source, ".head_music.musicxml")
117
+ target = File.join(target_directory, "#{name}.musescore4.mei")
118
+ MuseScore.convert_file(source, target, timeout: 120)
119
+ File.write(target, File.read(target).chomp + "\n")
120
+ puts "Wrote #{name}"
121
+ nil
122
+ rescue MuseScore::ConversionError => error
123
+ "#{name}: #{error.message}"
124
+ end
125
+ abort "MuseScore failed on:\n#{failures.join("\n")}" if failures.any?
126
+ end
127
+ end
128
+
50
129
  namespace :style do
51
130
  desc "Regenerate the pinned English guide strings (spec/fixtures/style/english_strings.yml)"
52
131
  task :snapshot_english do
data/bin/validate ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ # Runs the full validation: specs with coverage, Standard, the audit, and RubyCritic.
3
+ set -euo pipefail
4
+ cd "$(dirname "$0")/.."
5
+ exec bundle exec rake validate "$@"
data/head_music.gemspec CHANGED
@@ -35,9 +35,9 @@ Gem::Specification.new do |spec|
35
35
  spec.add_runtime_dependency "activesupport", ">= 7.0", "< 10"
36
36
  spec.add_runtime_dependency "humanize", ">= 2", "< 4"
37
37
  spec.add_runtime_dependency "i18n", "~> 1.8"
38
+ spec.add_runtime_dependency "rexml", "~> 3.4", ">= 3.4.2"
38
39
 
39
40
  spec.add_development_dependency "rake", "~> 13.0"
40
- spec.add_development_dependency "rexml", "~> 3.4"
41
41
  spec.add_development_dependency "rspec", "~> 3.0"
42
42
  spec.add_development_dependency "rspec-its", "~> 2.0"
43
43
  spec.add_development_dependency "bundler-audit", "~> 0.9"
@@ -20,10 +20,8 @@ class HeadMusic::Analysis::DiatonicInterval::Naming
20
20
  end
21
21
 
22
22
  def quality_name
23
- starting_quality = QUALITY_SEMITONES[simple_number_name.to_sym].keys.first
24
- delta = simple_semitones - (QUALITY_SEMITONES[simple_number_name.to_sym][starting_quality] % 12)
25
- delta -= 12 while delta >= 6
26
- HeadMusic::Rudiment::Quality.from(starting_quality, delta)
23
+ starting_quality, starting_semitones = QUALITY_SEMITONES[simple_number_name.to_sym].first
24
+ HeadMusic::Rudiment::Quality.from(starting_quality, semitones - starting_semitones - 12 * compound_octaves)
27
25
  end
28
26
 
29
27
  def simple_number_name
@@ -34,14 +32,15 @@ class HeadMusic::Analysis::DiatonicInterval::Naming
34
32
  NUMBER_NAMES[number - 1] || (number.to_s + NAME_SUFFIXES[number % 10])
35
33
  end
36
34
 
35
+ # Past the named numbers, an interval is so many octaves and a simple
36
+ # interval. An octave altered upward adds an octave and alters the unison.
37
37
  def name
38
- if named_number?
39
- [quality_name, number_name].join(" ")
40
- elsif simple_name == "perfect octave"
41
- "#{octaves.humanize} octaves"
42
- else
43
- "#{octaves.humanize} octaves and #{quality.article} #{simple_name}"
44
- end
38
+ return [quality_name, number_name].join(" ") if named_number?
39
+
40
+ octaves, remainder = octaves_and_remainder
41
+ return "#{octaves.humanize} octaves" unless remainder
42
+
43
+ "#{octaves.humanize} octaves and #{remainder.start_with?(/[aeiou]/) ? "an" : "a"} #{remainder}"
45
44
  end
46
45
 
47
46
  def shorthand
@@ -50,20 +49,23 @@ class HeadMusic::Analysis::DiatonicInterval::Naming
50
49
 
51
50
  private
52
51
 
53
- def simple_semitones
54
- @simple_semitones ||= semitones % 12
52
+ def named_number?
53
+ number <= NUMBER_NAMES.length
55
54
  end
56
55
 
57
- def named_number?
58
- number < NUMBER_NAMES.length
56
+ # The octaves added to the simple interval.
57
+ def compound_octaves
58
+ (number - simple_number) / 7
59
59
  end
60
60
 
61
61
  def quality
62
62
  @quality ||= HeadMusic::Rudiment::Quality.get(quality_name)
63
63
  end
64
64
 
65
- def octaves
66
- @octaves ||= semitones / 12
65
+ def octaves_and_remainder
66
+ return [compound_octaves, simple_name] unless simple_number == 8 && !quality_name.include?("diminished")
67
+
68
+ [compound_octaves + 1, ("#{quality_name} unison" unless quality_name == "perfect")]
67
69
  end
68
70
 
69
71
  def octave_equivalent?
@@ -0,0 +1,55 @@
1
+ class HeadMusic::Analysis::KeyFinder
2
+ # Duration-weighted pitch-class histograms of a NoteInput's notes: of the
3
+ # whole input, and of the time sounding in each bar.
4
+ class Histograms
5
+ def initialize(input)
6
+ @input = input
7
+ end
8
+
9
+ def whole
10
+ @input.notes.each_with_object(empty) do |note, histogram|
11
+ histogram[note.number % 12] += note.duration
12
+ end
13
+ end
14
+
15
+ # Each bar's histogram summed with +window+ bars on either side.
16
+ def windowed(window)
17
+ bars = by_bar
18
+ prefix = bars.each_with_object([empty]) { |histogram, sums| sums << sums.last.zip(histogram).map(&:sum) }
19
+ bars.each_index.map do |index|
20
+ low = [index - window, 0].max
21
+ high = [index + window, bars.length - 1].min
22
+ prefix[high + 1].zip(prefix[low]).map { |upper, lower| upper - lower }
23
+ end
24
+ end
25
+
26
+ # Each note's duration is credited to the bars it sounds in.
27
+ def by_bar
28
+ starts = @input.bar_starts.values
29
+ index_of_bar = @input.bar_starts.keys.each_with_index.to_h
30
+ histograms = Array.new(starts.length) { empty }
31
+ @input.notes.each do |note|
32
+ credit(histograms, starts, note, index_of_bar.fetch(@input.bar_at(note.onset)))
33
+ end
34
+ histograms
35
+ end
36
+
37
+ private
38
+
39
+ def credit(histograms, starts, note, bar_index)
40
+ from = note.onset
41
+ finish = note.onset + note.duration
42
+ while from < finish
43
+ bar_end = starts[bar_index + 1]
44
+ to = (bar_end.nil? || finish < bar_end) ? finish : bar_end
45
+ histograms[bar_index][note.number % 12] += to - from
46
+ from = to
47
+ bar_index += 1
48
+ end
49
+ end
50
+
51
+ def empty
52
+ Array.new(12, 0r)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,39 @@
1
+ class HeadMusic::Analysis::KeyFinder
2
+ # The key index for each bar that maximizes the summed scores, less the
3
+ # change penalty for every change of key: Viterbi over bars and keys. A tie
4
+ # keeps the key in force, then goes by preference. A bar that
5
+ # scores nothing changes no key, so a change waits for the music.
6
+ class KeyPath
7
+ def initialize(scores, change_penalty:, preference:)
8
+ @scores = scores
9
+ @change_penalty = change_penalty
10
+ @preference = preference
11
+ end
12
+
13
+ def to_a
14
+ totals = @scores.first.dup
15
+ back_pointers = @scores.drop(1).map do |bar_scores|
16
+ next totals.each_index.to_a if bar_scores.all?(&:zero?)
17
+
18
+ pointers = pointers_from(totals)
19
+ totals = pointers.each_with_index.map { |from, index| totals[from] - ((from == index) ? 0 : @change_penalty) + bar_scores[index] }
20
+ pointers
21
+ end
22
+ path = [best_index(totals)]
23
+ back_pointers.reverse_each { |pointers| path.unshift(pointers[path.first]) }
24
+ path
25
+ end
26
+
27
+ private
28
+
29
+ def pointers_from(totals)
30
+ leader = best_index(totals)
31
+ moved = totals[leader] - @change_penalty
32
+ totals.each_index.map { |index| (totals[index] >= moved) ? index : leader }
33
+ end
34
+
35
+ def best_index(totals)
36
+ totals.each_index.min_by { |index| [-totals[index], *@preference[index]] }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,105 @@
1
+ # A module for musical analysis
2
+ module HeadMusic::Analysis; end
3
+
4
+ # Infers the key of unspelled notes by correlating their duration-weighted
5
+ # pitch-class histogram with a key profile (Krumhansl, Cognitive Foundations
6
+ # of Musical Pitch, 1990; Temperley, The Cognition of Basic Musical
7
+ # Structures, 2001). The input is anything NoteInput.for reads.
8
+ #
9
+ # Over a whole piece it answers key regions, each starting at a downbeat. Each
10
+ # bar is scored by the histogram of the time sounding in it, summed with
11
+ # +window+ bars on either side. The regions maximize the summed correlation of
12
+ # each bar with its key, less +change_penalty+ for every change of key, so a
13
+ # region is worth starting only where it gains more than the penalty. The
14
+ # first region's tonic is spelled as KEYS spells it, and each later one by the
15
+ # enharmonic nearer the previous region's on the line of fifths, so a piece in
16
+ # B major moves to F♯ major rather than to G♭ major.
17
+ class HeadMusic::Analysis::KeyFinder
18
+ # The 24 keys a key finder chooses among, fixed by name rather than built
19
+ # through Key#relative. Of each enharmonic pair the tonic with fewer
20
+ # accidentals wins; at six apiece, G♭ major and E♭ minor share a signature.
21
+ KEYS = [
22
+ *%w[C D♭ D E♭ E F G♭ G A♭ A B♭ B].map { |tonic| "#{tonic} major" },
23
+ *%w[C C♯ D E♭ E F F♯ G G♯ A B♭ B].map { |tonic| "#{tonic} minor" }
24
+ ].map { |name| HeadMusic::Rudiment::Key.get(name) }.freeze
25
+
26
+ # On equal correlation, the key with fewer accidentals, then major, then the
27
+ # lower tonic pitch class.
28
+ PREFERENCE = KEYS.map do |key|
29
+ [key.key_signature.num_alterations, key.major? ? 0 : 1, key.tonic_spelling.pitch_class.to_i]
30
+ end.freeze
31
+
32
+ Candidate = Data.define(:key, :correlation)
33
+
34
+ attr_reader :input, :profile, :change_penalty, :window
35
+
36
+ def initialize(input, profile: :aarden_essen, change_penalty: 1.0, window: 0, **note_input_options)
37
+ unless change_penalty.is_a?(Numeric) && change_penalty.real? && change_penalty >= 0
38
+ raise ArgumentError, "change_penalty: must be a non-negative Numeric"
39
+ end
40
+ raise ArgumentError, "window: must be a non-negative Integer" unless window.is_a?(Integer) && !window.negative?
41
+
42
+ @input = HeadMusic::Analysis::NoteInput.for(input, **note_input_options)
43
+ @profile = HeadMusic::Analysis::KeyProfile.get(profile)
44
+ @change_penalty = change_penalty
45
+ @window = window
46
+ end
47
+
48
+ # The best key for the whole input, or nil when nothing sounds.
49
+ def key
50
+ candidates.first&.key
51
+ end
52
+
53
+ # All 24 keys, best first, or none when nothing sounds.
54
+ def candidates
55
+ @candidates ||= begin
56
+ histogram = Histograms.new(input).whole
57
+ if histogram.sum.zero?
58
+ []
59
+ else
60
+ ranked(profile.correlations(histogram)).map { |index, correlation| Candidate.new(KEYS[index], correlation) }.freeze
61
+ end
62
+ end
63
+ end
64
+
65
+ # The key regions of the input, in bar order, or none when nothing sounds.
66
+ # An empty bar scores nothing, so the key in force carries through it.
67
+ def regions
68
+ @regions ||= if candidates.empty?
69
+ []
70
+ else
71
+ bar_starts = input.bar_starts.to_a
72
+ previous = nil
73
+ key_path.each_with_index.chunk_while { |(a, _), (b, _)| a == b }.map do |run|
74
+ key_index, bar_index = run.first
75
+ bar_number, onset = bar_starts[bar_index]
76
+ previous = previous ? self.class.enharmonic_nearer(KEYS[key_index], previous) : KEYS[key_index]
77
+ HeadMusic::Analysis::KeyRegion.new(bar_number: bar_number, onset: onset, key: previous)
78
+ end.freeze
79
+ end
80
+ end
81
+
82
+ # The spelling of a key whose tonic is nearer another key's on the line of
83
+ # fifths, within seven accidentals, keeping the key as spelled on a tie.
84
+ def self.enharmonic_nearer(key, other)
85
+ line_of_fifths = HeadMusic::Analysis::LineOfFifths
86
+ tonic = line_of_fifths.index_of(key.tonic_spelling)
87
+ target = line_of_fifths.index_of(other.tonic_spelling)
88
+ offset = key.major? ? 0 : -3
89
+ nearest = [tonic, tonic - 12, tonic + 12].select { |index| (index + offset).abs <= 7 }.min_by { |index| (index - target).abs }
90
+ return key if nearest == tonic
91
+
92
+ HeadMusic::Rudiment::Key.get("#{line_of_fifths.spelling_at(nearest)} #{key.quality}")
93
+ end
94
+
95
+ private
96
+
97
+ def key_path
98
+ scores = Histograms.new(input).windowed(window).map { |histogram| profile.correlations(histogram) }
99
+ KeyPath.new(scores, change_penalty: change_penalty, preference: PREFERENCE).to_a
100
+ end
101
+
102
+ def ranked(correlations)
103
+ correlations.each_with_index.sort_by { |correlation, index| [-correlation, *PREFERENCE[index]] }.map(&:reverse)
104
+ end
105
+ end
@@ -0,0 +1,82 @@
1
+ # A module for musical analysis
2
+ module HeadMusic::Analysis; end
3
+
4
+ # The weight of each pitch class in a key, from the tonic upward in semitones,
5
+ # for major and for minor. Correlating a pitch-class histogram with each of
6
+ # the 24 rotations scores how well the music fits each key.
7
+ #
8
+ # The values are stored as Rationals, so that keys whose correlations are
9
+ # equal in exact arithmetic tie exactly.
10
+ HeadMusic::Analysis::KeyProfile = Data.define(:name, :major, :minor, :source)
11
+
12
+ class HeadMusic::Analysis::KeyProfile
13
+ # Each table was checked against Humdrum keycor (humextra cli/keycor.cpp)
14
+ # and music21 (analysis/discrete.py), which agree digit for digit.
15
+ PROFILES = {
16
+ aarden_essen: {
17
+ major: [17.7661r, 0.145624r, 14.9265r, 0.160186r, 19.8049r, 11.3587r, 0.291248r, 22.062r, 0.145624r, 8.15494r, 0.232998r, 4.95122r],
18
+ minor: [18.2648r, 0.737619r, 14.0499r, 16.8599r, 0.702494r, 14.4362r, 0.702494r, 18.6161r, 4.56621r, 1.93186r, 7.37619r, 1.75623r],
19
+ source: "Aarden, Dynamic Melodic Expectancy, PhD dissertation, Ohio State University (2003), from the Essen Folksong Collection"
20
+ },
21
+ krumhansl_kessler: {
22
+ major: [6.35r, 2.23r, 3.48r, 2.33r, 4.38r, 4.09r, 2.52r, 5.19r, 2.39r, 3.66r, 2.29r, 2.88r],
23
+ minor: [6.33r, 2.68r, 3.52r, 5.38r, 2.60r, 3.53r, 2.54r, 4.75r, 3.98r, 2.69r, 3.34r, 3.17r],
24
+ source: "Krumhansl, Cognitive Foundations of Musical Pitch (1990), from Krumhansl and Kessler, Psychological Review 89 (1982)"
25
+ },
26
+ temperley: {
27
+ major: [0.748r, 0.060r, 0.488r, 0.082r, 0.670r, 0.460r, 0.096r, 0.715r, 0.104r, 0.366r, 0.057r, 0.400r],
28
+ minor: [0.712r, 0.084r, 0.474r, 0.618r, 0.049r, 0.460r, 0.105r, 0.747r, 0.404r, 0.067r, 0.133r, 0.330r],
29
+ source: "Temperley, Music and Probability (2007), p. 85, from the Kostka-Payne corpus"
30
+ }
31
+ }.freeze
32
+
33
+ def self.get(name)
34
+ return name if name.is_a?(self)
35
+
36
+ attributes = PROFILES[name&.to_sym]
37
+ raise ArgumentError, "unknown key profile #{name.inspect}; use one of #{names.join(", ")}" unless attributes
38
+
39
+ new(name: name.to_sym, **attributes)
40
+ end
41
+
42
+ def self.names
43
+ PROFILES.keys
44
+ end
45
+
46
+ def initialize(name:, major:, minor:, source:)
47
+ super(name: name, major: major.map(&:to_r).freeze, minor: minor.map(&:to_r).freeze, source: source)
48
+ end
49
+
50
+ # The Pearson correlation of the histogram with each key: the twelve major
51
+ # keys with tonics C upward, then the twelve minor keys. A histogram or
52
+ # profile with no variance correlates 0 with everything.
53
+ def correlations(histogram)
54
+ weights = histogram.map(&:to_r)
55
+ deviations = deviations_of(weights)
56
+ variance = sum_of_squares(deviations)
57
+ [terms_of(major), terms_of(minor)].flat_map do |profile_deviations, profile_variance|
58
+ (0..11).map do |tonic|
59
+ next 0.0 if variance.zero? || profile_variance.zero?
60
+
61
+ covariance = (0..11).sum { |pitch_class| deviations[pitch_class] * profile_deviations[(pitch_class - tonic) % 12] }
62
+ covariance.to_f / Math.sqrt((variance * profile_variance).to_f)
63
+ end
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def terms_of(values)
70
+ deviations = deviations_of(values)
71
+ [deviations, sum_of_squares(deviations)]
72
+ end
73
+
74
+ def deviations_of(values)
75
+ mean = values.sum(0r) / values.length
76
+ values.map { |value| value - mean }
77
+ end
78
+
79
+ def sum_of_squares(values)
80
+ values.sum(0r) { |value| value * value }
81
+ end
82
+ end
@@ -0,0 +1,5 @@
1
+ # A module for musical analysis
2
+ module HeadMusic::Analysis; end
3
+
4
+ # A key in force from the downbeat of a bar, at that onset in whole notes.
5
+ HeadMusic::Analysis::KeyRegion = Data.define(:bar_number, :onset, :key)
@@ -0,0 +1,52 @@
1
+ # A module for musical analysis
2
+ module HeadMusic::Analysis; end
3
+
4
+ # Spellings placed on the line of fifths, after Temperley, The Cognition of
5
+ # Basic Musical Structures (2001), and Meredith, "The ps13 pitch spelling
6
+ # algorithm," Journal of New Music Research 35 (2006). C is 0, each fifth up
7
+ # adds one, and each sharp adds seven: F♭ -8, F -1, C 0, G 1, F♯ 6, B♯ 12.
8
+ module HeadMusic::Analysis::LineOfFifths
9
+ LETTER_INDICES = {"F" => -1, "C" => 0, "G" => 1, "D" => 2, "A" => 3, "E" => 4, "B" => 5}.freeze
10
+ LETTERS = LETTER_INDICES.keys.freeze
11
+ RANGE = (-15..19)
12
+
13
+ # Built once, because Alteration.by searches every alteration on each call.
14
+ SPELLINGS = RANGE.to_h do |index|
15
+ letter = LETTERS[(index + 1) % 7]
16
+ semitones = (index + 1).div(7)
17
+ alteration = HeadMusic::Rudiment::Alteration.by(:semitones, semitones) unless semitones.zero?
18
+ [index, HeadMusic::Rudiment::Spelling.fetch_or_create(HeadMusic::Rudiment::LetterName.get(letter), alteration)]
19
+ end.freeze
20
+
21
+ module_function
22
+
23
+ def index_of(spelling)
24
+ spelling = HeadMusic::Rudiment::Spelling.get(spelling)
25
+ LETTER_INDICES.fetch(spelling.letter_name.to_s) + 7 * (spelling.alteration&.semitones || 0)
26
+ end
27
+
28
+ def spelling_at(index)
29
+ SPELLINGS[index]
30
+ end
31
+
32
+ def pitch_class_of(index)
33
+ (7 * index) % 12
34
+ end
35
+
36
+ # The pitch of a key number spelled at an index, or nil where the catalog has
37
+ # none: past a double accidental, or outside registers -1 through 9.
38
+ def pitch_at(number, index)
39
+ spelling = SPELLINGS[index]
40
+ return unless spelling
41
+
42
+ offset = number - spelling.semitones_above_c
43
+ return unless (offset % 12).zero?
44
+
45
+ pitch = HeadMusic::Rudiment::Pitch.fetch_or_create(spelling, offset / 12 - 1)
46
+ pitch if pitch&.midi_note_number == number && pitch.spelling == spelling
47
+ end
48
+
49
+ def pitch_for(number, index)
50
+ pitch_at(number, index) || HeadMusic::Rudiment::Pitch.from_number(number)
51
+ end
52
+ end