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.
- checksums.yaml +4 -4
- data/.claude/skills/release/SKILL.md +21 -1
- data/.github/workflows/ci.yml +37 -0
- data/.gitignore +1 -0
- data/.ruby-version +1 -1
- data/.rubycritic.yml +7 -0
- data/CHANGELOG.md +258 -0
- data/CLAUDE.md +6 -4
- data/Gemfile +1 -1
- data/Gemfile.lock +16 -16
- data/README.md +12 -2
- data/Rakefile +83 -4
- data/bin/validate +5 -0
- data/head_music.gemspec +1 -1
- data/lib/head_music/analysis/diatonic_interval/naming.rb +19 -17
- data/lib/head_music/analysis/key_finder/histograms.rb +55 -0
- data/lib/head_music/analysis/key_finder/key_path.rb +39 -0
- data/lib/head_music/analysis/key_finder.rb +105 -0
- data/lib/head_music/analysis/key_profile.rb +82 -0
- data/lib/head_music/analysis/key_region.rb +5 -0
- data/lib/head_music/analysis/line_of_fifths.rb +52 -0
- data/lib/head_music/analysis/note_input/flow_reader.rb +86 -0
- data/lib/head_music/analysis/note_input.rb +121 -0
- data/lib/head_music/analysis/pitch_speller/compactness.rb +53 -0
- data/lib/head_music/analysis/pitch_speller/key_center.rb +73 -0
- data/lib/head_music/analysis/pitch_speller/key_centers.rb +65 -0
- data/lib/head_music/analysis/pitch_speller/respelling.rb +75 -0
- data/lib/head_music/analysis/pitch_speller/sounding.rb +21 -0
- data/lib/head_music/analysis/pitch_speller/successors.rb +58 -0
- data/lib/head_music/analysis/pitch_speller.rb +107 -0
- data/lib/head_music/analysis/unspelled_note.rb +16 -0
- data/lib/head_music/content/bar.rb +69 -10
- data/lib/head_music/content/cantus_firmus/example.rb +2 -2
- data/lib/head_music/content/chain_link.rb +67 -0
- data/lib/head_music/content/comment.rb +1 -16
- data/lib/head_music/content/credits.rb +0 -4
- data/lib/head_music/content/dynamic_event.rb +35 -0
- data/lib/head_music/content/dynamic_events.rb +38 -0
- data/lib/head_music/content/flow/bars.rb +56 -0
- data/lib/head_music/content/flow/deserializer.rb +123 -25
- data/lib/head_music/content/flow/hash_deserializer.rb +29 -7
- data/lib/head_music/content/flow/hash_serializer.rb +47 -0
- data/lib/head_music/content/flow/navigation.rb +80 -0
- data/lib/head_music/content/flow/performance_order.rb +128 -0
- data/lib/head_music/content/flow/schema_validation.rb +53 -0
- data/lib/head_music/content/flow/schema_values.rb +67 -143
- data/lib/head_music/content/flow/timeline.rb +90 -58
- data/lib/head_music/content/flow/timeline_serializer.rb +58 -0
- data/lib/head_music/content/flow/timeline_spans.rb +68 -0
- data/lib/head_music/content/flow/timeline_tempos.rb +169 -0
- data/lib/head_music/content/flow/timeline_values.rb +139 -0
- data/lib/head_music/content/flow/v4_upgrade.rb +46 -0
- data/lib/head_music/content/flow/voice_event_values.rb +165 -0
- data/lib/head_music/content/flow.rb +114 -62
- data/lib/head_music/content/grace_note.rb +72 -0
- data/lib/head_music/content/jump.rb +53 -0
- data/lib/head_music/content/layout/realization.rb +20 -13
- data/lib/head_music/content/layout/rendering.rb +82 -0
- data/lib/head_music/content/layout.rb +3 -58
- data/lib/head_music/content/note.rb +9 -8
- data/lib/head_music/content/note_event.rb +154 -0
- data/lib/head_music/content/part.rb +39 -0
- data/lib/head_music/content/player.rb +1 -2
- data/lib/head_music/content/position.rb +47 -15
- data/lib/head_music/content/project.rb +57 -21
- data/lib/head_music/content/publication.rb +3 -27
- data/lib/head_music/content/rest_event.rb +45 -0
- data/lib/head_music/content/sound_resolver.rb +9 -3
- data/lib/head_music/content/span.rb +81 -0
- data/lib/head_music/content/spans.rb +50 -0
- data/lib/head_music/content/syllable.rb +3 -3
- data/lib/head_music/content/tuplet_group/bracketing.rb +77 -0
- data/lib/head_music/content/tuplet_group.rb +120 -0
- data/lib/head_music/content/tuplet_groups.rb +36 -0
- data/lib/head_music/content/voice/continuity.rb +15 -15
- data/lib/head_music/content/voice/dynamic_resolver.rb +49 -0
- data/lib/head_music/content/voice/melodic_line.rb +3 -3
- data/lib/head_music/content/voice/span_anchors.rb +37 -0
- data/lib/head_music/content/voice/staff_assignments.rb +56 -0
- data/lib/head_music/content/voice.rb +119 -92
- data/lib/head_music/content/voice_event/run.rb +50 -0
- data/lib/head_music/content/voice_event.rb +230 -0
- data/lib/head_music/content/voice_events.rb +60 -0
- data/lib/head_music/content/work.rb +3 -21
- data/lib/head_music/instruments/instrument.rb +12 -0
- data/lib/head_music/instruments/instruments.yml +115 -0
- data/lib/head_music/locales/de.yml +29 -0
- data/lib/head_music/locales/en.yml +51 -0
- data/lib/head_music/locales/es.yml +29 -0
- data/lib/head_music/locales/fr.yml +29 -0
- data/lib/head_music/locales/it.yml +29 -0
- data/lib/head_music/locales/ru.yml +29 -0
- data/lib/head_music/midi/bar_layout.rb +150 -0
- data/lib/head_music/midi/bar_pieces.rb +41 -0
- data/lib/head_music/midi/budget.rb +102 -0
- data/lib/head_music/midi/change_cursor.rb +30 -0
- data/lib/head_music/midi/channel_allocator.rb +47 -0
- data/lib/head_music/midi/chunk_reader.rb +72 -0
- data/lib/head_music/midi/conductor_track.rb +74 -0
- data/lib/head_music/midi/events.rb +112 -0
- data/lib/head_music/midi/flow_builder.rb +116 -0
- data/lib/head_music/midi/hairpin_ramps.rb +110 -0
- data/lib/head_music/midi/instrument_map.rb +77 -0
- data/lib/head_music/midi/key_changes.rb +45 -0
- data/lib/head_music/midi/key_overlaps.rb +48 -0
- data/lib/head_music/midi/level_marks.rb +61 -0
- data/lib/head_music/midi/lyric_attacher.rb +60 -0
- data/lib/head_music/midi/lyric_events.rb +44 -0
- data/lib/head_music/midi/meta_reader.rb +70 -0
- data/lib/head_music/midi/meter_changes.rb +67 -0
- data/lib/head_music/midi/note_pairing.rb +48 -0
- data/lib/head_music/midi/note_timing.rb +109 -0
- data/lib/head_music/midi/note_values.rb +99 -0
- data/lib/head_music/midi/onsets.rb +15 -0
- data/lib/head_music/midi/parser.rb +36 -0
- data/lib/head_music/midi/part_key.rb +18 -0
- data/lib/head_music/midi/part_reading.rb +72 -0
- data/lib/head_music/midi/part_source.rb +161 -0
- data/lib/head_music/midi/part_track.rb +115 -0
- data/lib/head_music/midi/performance_timeline.rb +80 -0
- data/lib/head_music/midi/preflight.rb +138 -0
- data/lib/head_music/midi/program_changes.rb +37 -0
- data/lib/head_music/midi/quantized_note.rb +5 -0
- data/lib/head_music/midi/quantizer.rb +106 -0
- data/lib/head_music/midi/ramp_turn.rb +26 -0
- data/lib/head_music/midi/reader.rb +110 -0
- data/lib/head_music/midi/smf.rb +41 -0
- data/lib/head_music/midi/sound_keys.rb +32 -0
- data/lib/head_music/midi/spelling.rb +74 -0
- data/lib/head_music/midi/tempo_changes.rb +121 -0
- data/lib/head_music/midi/tempo_inference.rb +143 -0
- data/lib/head_music/midi/tempo_steps.rb +112 -0
- data/lib/head_music/midi/tick_resolution.rb +44 -0
- data/lib/head_music/midi/timeline_reader.rb +105 -0
- data/lib/head_music/midi/track_reader.rb +128 -0
- data/lib/head_music/midi/velocity_resolver.rb +116 -0
- data/lib/head_music/midi/voice_separator.rb +100 -0
- data/lib/head_music/midi/writer.rb +64 -0
- data/lib/head_music/midi.rb +54 -0
- data/lib/head_music/notation/abc/bar_line_writer.rb +86 -0
- data/lib/head_music/notation/abc/bar_mark_reader.rb +75 -0
- data/lib/head_music/notation/abc/bar_writer.rb +144 -0
- data/lib/head_music/notation/abc/beam_adjacency.rb +34 -0
- data/lib/head_music/notation/abc/body_lexer.rb +20 -233
- data/lib/head_music/notation/abc/broken_rhythm.rb +41 -0
- data/lib/head_music/notation/abc/decoration.rb +37 -0
- data/lib/head_music/notation/abc/decoration_mapper.rb +102 -0
- data/lib/head_music/notation/abc/decoration_writer.rb +30 -0
- data/lib/head_music/notation/abc/duration_resolver.rb +5 -3
- data/lib/head_music/notation/abc/duration_writer.rb +20 -25
- data/lib/head_music/notation/abc/grace_reader.rb +38 -0
- data/lib/head_music/notation/abc/grace_scanner.rb +98 -0
- data/lib/head_music/notation/abc/grace_writer.rb +38 -0
- data/lib/head_music/notation/abc/hairpin_marks.rb +52 -0
- data/lib/head_music/notation/abc/header.rb +49 -19
- data/lib/head_music/notation/abc/invisible_rests.rb +38 -0
- data/lib/head_music/notation/abc/key_mapper.rb +6 -1
- data/lib/head_music/notation/abc/line_scanner.rb +251 -0
- data/lib/head_music/notation/abc/navigation_tagger.rb +86 -0
- data/lib/head_music/notation/abc/opening_fields.rb +63 -0
- data/lib/head_music/notation/abc/parser.rb +111 -110
- data/lib/head_music/notation/abc/pending_decorations.rb +127 -0
- data/lib/head_music/notation/abc/pending_graces.rb +30 -0
- data/lib/head_music/notation/abc/pending_note.rb +64 -0
- data/lib/head_music/notation/abc/pending_slurs.rb +68 -0
- data/lib/head_music/notation/abc/pending_tie.rb +39 -0
- data/lib/head_music/notation/abc/pitch_builder.rb +6 -1
- data/lib/head_music/notation/abc/pitch_writer.rb +17 -5
- data/lib/head_music/notation/abc/position_marks.rb +147 -0
- data/lib/head_music/notation/abc/preflight.rb +13 -0
- data/lib/head_music/notation/abc/render_plan.rb +35 -0
- data/lib/head_music/notation/abc/render_preflight.rb +71 -0
- data/lib/head_music/notation/abc/repeat_tagger.rb +31 -2
- data/lib/head_music/notation/abc/rest_writer.rb +50 -0
- data/lib/head_music/notation/abc/slur_writer.rb +106 -0
- data/lib/head_music/notation/abc/tempo_field.rb +72 -0
- data/lib/head_music/notation/abc/timeline_fields.rb +54 -0
- data/lib/head_music/notation/abc/token.rb +2 -2
- data/lib/head_music/notation/abc/tuplet_grouping.rb +104 -0
- data/lib/head_music/notation/abc/tuplet_marker.rb +30 -0
- data/lib/head_music/notation/abc/tuplet_writer.rb +68 -0
- data/lib/head_music/notation/abc/voice_overlay.rb +49 -0
- data/lib/head_music/notation/abc/voice_registry.rb +8 -2
- data/lib/head_music/notation/abc/voice_replay.rb +45 -0
- data/lib/head_music/notation/abc/voice_state.rb +250 -100
- data/lib/head_music/notation/abc/voice_tokens.rb +29 -0
- data/lib/head_music/notation/abc/voice_writer.rb +125 -0
- data/lib/head_music/notation/abc/writer.rb +53 -137
- data/lib/head_music/notation/abc.rb +1 -10
- data/lib/head_music/notation/bar_map.rb +186 -0
- data/lib/head_music/notation/bar_marks.rb +19 -0
- data/lib/head_music/notation/bar_splitter.rb +140 -0
- data/lib/head_music/notation/barline_placement.rb +15 -0
- data/lib/head_music/notation/beam_groups.rb +39 -0
- data/lib/head_music/notation/beam_reading.rb +20 -0
- data/lib/head_music/notation/chord_clashes.rb +20 -0
- data/lib/head_music/notation/clef_changes.rb +21 -0
- data/lib/head_music/notation/concert_transposition.rb +60 -0
- data/lib/head_music/notation/dotted_duration.rb +63 -23
- data/lib/head_music/notation/fermata_marks.rb +78 -0
- data/lib/head_music/notation/flow_timed_marks.rb +71 -0
- data/lib/head_music/notation/hairpin_segments.rb +22 -0
- data/lib/head_music/notation/kern/bar_clock.rb +195 -0
- data/lib/head_music/notation/kern/barline_reader.rb +37 -0
- data/lib/head_music/notation/kern/chord_token.rb +65 -0
- data/lib/head_music/notation/kern/citation_reader.rb +69 -0
- data/lib/head_music/notation/kern/clef_codes.rb +33 -0
- data/lib/head_music/notation/kern/data_rows.rb +101 -0
- data/lib/head_music/notation/kern/document.rb +129 -0
- data/lib/head_music/notation/kern/duration_reader.rb +79 -0
- data/lib/head_music/notation/kern/duration_writer.rb +38 -0
- data/lib/head_music/notation/kern/dynamic_fields.rb +93 -0
- data/lib/head_music/notation/kern/dynamic_placer.rb +131 -0
- data/lib/head_music/notation/kern/dynamic_reader.rb +88 -0
- data/lib/head_music/notation/kern/flow_builder.rb +140 -0
- data/lib/head_music/notation/kern/grace_tokens.rb +42 -0
- data/lib/head_music/notation/kern/instrument_codes.rb +62 -0
- data/lib/head_music/notation/kern/interpretation_reader.rb +43 -0
- data/lib/head_music/notation/kern/interpretation_rows.rb +119 -0
- data/lib/head_music/notation/kern/key_reader.rb +73 -0
- data/lib/head_music/notation/kern/layer.rb +158 -0
- data/lib/head_music/notation/kern/lexer.rb +89 -0
- data/lib/head_music/notation/kern/lyric_reader.rb +84 -0
- data/lib/head_music/notation/kern/mark_codes.rb +48 -0
- data/lib/head_music/notation/kern/mark_offset.rb +16 -0
- data/lib/head_music/notation/kern/melismas.rb +35 -0
- data/lib/head_music/notation/kern/meter_reader.rb +60 -0
- data/lib/head_music/notation/kern/parser.rb +34 -0
- data/lib/head_music/notation/kern/part_grouping.rb +70 -0
- data/lib/head_music/notation/kern/part_roster.rb +69 -0
- data/lib/head_music/notation/kern/pitch_reader.rb +32 -0
- data/lib/head_music/notation/kern/pitch_writer.rb +21 -0
- data/lib/head_music/notation/kern/preflight.rb +84 -0
- data/lib/head_music/notation/kern/record.rb +26 -0
- data/lib/head_music/notation/kern/reference_records.rb +64 -0
- data/lib/head_music/notation/kern/render_plan.rb +19 -0
- data/lib/head_music/notation/kern/row_offsets.rb +30 -0
- data/lib/head_music/notation/kern/span_marks.rb +80 -0
- data/lib/head_music/notation/kern/spine_changes.rb +78 -0
- data/lib/head_music/notation/kern/spine_columns.rb +86 -0
- data/lib/head_music/notation/kern/spine_layout.rb +151 -0
- data/lib/head_music/notation/kern/spine_tags.rb +30 -0
- data/lib/head_music/notation/kern/spine_tokens.rb +183 -0
- data/lib/head_music/notation/kern/spine_voices.rb +127 -0
- data/lib/head_music/notation/kern/tempo_reader.rb +49 -0
- data/lib/head_music/notation/kern/timeline_reader.rb +169 -0
- data/lib/head_music/notation/kern/token_reader.rb +198 -0
- data/lib/head_music/notation/kern/tuplet_groups.rb +93 -0
- data/lib/head_music/notation/kern/voice_cursor.rb +115 -0
- data/lib/head_music/notation/kern/writer.rb +67 -0
- data/lib/head_music/notation/kern/written_bars.rb +151 -0
- data/lib/head_music/notation/kern.rb +27 -0
- data/lib/head_music/notation/lily_pond/bar_commands.rb +87 -0
- data/lib/head_music/notation/lily_pond/bar_mark_placer.rb +82 -0
- data/lib/head_music/notation/lily_pond/bar_mark_reader.rb +134 -0
- data/lib/head_music/notation/lily_pond/bar_mark_writer.rb +65 -0
- data/lib/head_music/notation/lily_pond/chord_reader.rb +34 -0
- data/lib/head_music/notation/lily_pond/context_reader.rb +58 -12
- data/lib/head_music/notation/lily_pond/document.rb +32 -1
- data/lib/head_music/notation/lily_pond/duration_reader.rb +41 -3
- data/lib/head_music/notation/lily_pond/duration_writer.rb +3 -3
- data/lib/head_music/notation/lily_pond/dynamics_item_reader.rb +47 -0
- data/lib/head_music/notation/lily_pond/event_placer.rb +191 -0
- data/lib/head_music/notation/lily_pond/flow_builder.rb +20 -95
- data/lib/head_music/notation/lily_pond/grace_reader.rb +132 -0
- data/lib/head_music/notation/lily_pond/grace_writer.rb +35 -0
- data/lib/head_music/notation/lily_pond/group_reader.rb +51 -0
- data/lib/head_music/notation/lily_pond/hairpin_marks.rb +40 -0
- data/lib/head_music/notation/lily_pond/key_mapper.rb +2 -11
- data/lib/head_music/notation/lily_pond/lexer.rb +92 -12
- data/lib/head_music/notation/lily_pond/mark_reader.rb +184 -0
- data/lib/head_music/notation/lily_pond/mark_writer.rb +27 -0
- data/lib/head_music/notation/lily_pond/music_item_reader.rb +85 -57
- data/lib/head_music/notation/lily_pond/music_reader.rb +29 -33
- data/lib/head_music/notation/lily_pond/part_dynamics.rb +53 -0
- data/lib/head_music/notation/lily_pond/part_layout.rb +71 -0
- data/lib/head_music/notation/lily_pond/pending_tuplets.rb +47 -0
- data/lib/head_music/notation/lily_pond/pitch_writer.rb +18 -0
- data/lib/head_music/notation/lily_pond/preflight.rb +27 -20
- data/lib/head_music/notation/lily_pond/render_plan.rb +94 -24
- data/lib/head_music/notation/lily_pond/segment_tokens.rb +135 -0
- data/lib/head_music/notation/lily_pond/setting_reader.rb +86 -0
- data/lib/head_music/notation/lily_pond/spacer_run.rb +43 -0
- data/lib/head_music/notation/lily_pond/spacers.rb +58 -0
- data/lib/head_music/notation/lily_pond/span_marks.rb +91 -0
- data/lib/head_music/notation/lily_pond/span_pairing.rb +35 -0
- data/lib/head_music/notation/lily_pond/tempo_reader.rb +46 -0
- data/lib/head_music/notation/lily_pond/tempo_writer.rb +33 -0
- data/lib/head_music/notation/lily_pond/time_signature_writer.rb +42 -0
- data/lib/head_music/notation/lily_pond/timed_marks.rb +158 -0
- data/lib/head_music/notation/lily_pond/token.rb +8 -2
- data/lib/head_music/notation/lily_pond/token_cursor.rb +55 -2
- data/lib/head_music/notation/lily_pond/tuplet_reader.rb +56 -0
- data/lib/head_music/notation/lily_pond/voice_context.rb +21 -9
- data/lib/head_music/notation/lily_pond/voice_item_reader.rb +86 -0
- data/lib/head_music/notation/lily_pond/voice_stream.rb +167 -31
- data/lib/head_music/notation/lily_pond/voice_writer.rb +74 -24
- data/lib/head_music/notation/lily_pond/writer.rb +30 -15
- data/lib/head_music/notation/lily_pond.rb +4 -7
- data/lib/head_music/notation/mark_reading.rb +16 -0
- data/lib/head_music/notation/mei/accidental_plan.rb +94 -0
- data/lib/head_music/notation/mei/accidental_reading.rb +23 -0
- data/lib/head_music/notation/mei/artic_attributes.rb +36 -0
- data/lib/head_music/notation/mei/artic_reader.rb +41 -0
- data/lib/head_music/notation/mei/attached_marks.rb +70 -0
- data/lib/head_music/notation/mei/attribute_values.rb +245 -0
- data/lib/head_music/notation/mei/bar_marks.rb +52 -0
- data/lib/head_music/notation/mei/barline_reader.rb +101 -0
- data/lib/head_music/notation/mei/beam_containers.rb +56 -0
- data/lib/head_music/notation/mei/beam_reader.rb +79 -0
- data/lib/head_music/notation/mei/clef_reading.rb +34 -0
- data/lib/head_music/notation/mei/container_spans.rb +15 -0
- data/lib/head_music/notation/mei/control_anchors.rb +194 -0
- data/lib/head_music/notation/mei/copy_expansion.rb +113 -0
- data/lib/head_music/notation/mei/decimals.rb +29 -0
- data/lib/head_music/notation/mei/document.rb +138 -0
- data/lib/head_music/notation/mei/downbeat_tempo.rb +41 -0
- data/lib/head_music/notation/mei/duration_reading.rb +49 -0
- data/lib/head_music/notation/mei/durations.rb +31 -0
- data/lib/head_music/notation/mei/dynam_reader.rb +51 -0
- data/lib/head_music/notation/mei/editorial.rb +122 -0
- data/lib/head_music/notation/mei/ending_passes.rb +24 -0
- data/lib/head_music/notation/mei/event_records.rb +61 -0
- data/lib/head_music/notation/mei/event_writer.rb +102 -0
- data/lib/head_music/notation/mei/fermata_controls.rb +39 -0
- data/lib/head_music/notation/mei/fermata_reader.rb +43 -0
- data/lib/head_music/notation/mei/flow_builder.rb +62 -0
- data/lib/head_music/notation/mei/hairpin_reader.rb +38 -0
- data/lib/head_music/notation/mei/head_writer.rb +85 -0
- data/lib/head_music/notation/mei/id_index.rb +86 -0
- data/lib/head_music/notation/mei/identity_reader.rb +199 -0
- data/lib/head_music/notation/mei/ids.rb +70 -0
- data/lib/head_music/notation/mei/layer_containers.rb +94 -0
- data/lib/head_music/notation/mei/layer_reader.rb +211 -0
- data/lib/head_music/notation/mei/layer_signs.rb +37 -0
- data/lib/head_music/notation/mei/layer_tree.rb +40 -0
- data/lib/head_music/notation/mei/lyric_reader.rb +116 -0
- data/lib/head_music/notation/mei/lyric_writer.rb +72 -0
- data/lib/head_music/notation/mei/mark_reader.rb +52 -0
- data/lib/head_music/notation/mei/mark_readers.rb +18 -0
- data/lib/head_music/notation/mei/mark_records.rb +122 -0
- data/lib/head_music/notation/mei/marking_events.rb +89 -0
- data/lib/head_music/notation/mei/markup.rb +37 -0
- data/lib/head_music/notation/mei/measure_clefs.rb +38 -0
- data/lib/head_music/notation/mei/measure_contents.rb +87 -0
- data/lib/head_music/notation/mei/measure_length.rb +30 -0
- data/lib/head_music/notation/mei/measure_writer.rb +79 -0
- data/lib/head_music/notation/mei/navigation_events.rb +59 -0
- data/lib/head_music/notation/mei/navigation_reader.rb +71 -0
- data/lib/head_music/notation/mei/onset_index.rb +49 -0
- data/lib/head_music/notation/mei/open_spaces.rb +43 -0
- data/lib/head_music/notation/mei/opening_values.rb +44 -0
- data/lib/head_music/notation/mei/ornament_reader.rb +47 -0
- data/lib/head_music/notation/mei/parser.rb +45 -0
- data/lib/head_music/notation/mei/part_layers.rb +58 -0
- data/lib/head_music/notation/mei/part_reading.rb +69 -0
- data/lib/head_music/notation/mei/part_roster.rb +54 -0
- data/lib/head_music/notation/mei/part_spelling.rb +55 -0
- data/lib/head_music/notation/mei/pitch_reading.rb +33 -0
- data/lib/head_music/notation/mei/preflight.rb +63 -0
- data/lib/head_music/notation/mei/render_plan.rb +154 -0
- data/lib/head_music/notation/mei/score_def_values.rb +84 -0
- data/lib/head_music/notation/mei/score_def_writer.rb +138 -0
- data/lib/head_music/notation/mei/score_reader.rb +191 -0
- data/lib/head_music/notation/mei/section_writer.rb +48 -0
- data/lib/head_music/notation/mei/slur_reader.rb +71 -0
- data/lib/head_music/notation/mei/staff_groups.rb +132 -0
- data/lib/head_music/notation/mei/staff_numbering.rb +62 -0
- data/lib/head_music/notation/mei/staff_state.rb +207 -0
- data/lib/head_music/notation/mei/tempo_controls.rb +103 -0
- data/lib/head_music/notation/mei/tempo_mark_reader.rb +79 -0
- data/lib/head_music/notation/mei/tempo_reading.rb +40 -0
- data/lib/head_music/notation/mei/tie_controls.rb +145 -0
- data/lib/head_music/notation/mei/timeline_changes.rb +119 -0
- data/lib/head_music/notation/mei/timeline_reader.rb +30 -0
- data/lib/head_music/notation/mei/tstamp.rb +56 -0
- data/lib/head_music/notation/mei/tuplet_reader.rb +35 -0
- data/lib/head_music/notation/mei/tuplet_spans.rb +173 -0
- data/lib/head_music/notation/mei/voice_assignment.rb +109 -0
- data/lib/head_music/notation/mei/writer.rb +59 -0
- data/lib/head_music/notation/mei/written_items.rb +112 -0
- data/lib/head_music/notation/mei/xml_reading.rb +5 -0
- data/lib/head_music/notation/mei.rb +69 -0
- data/lib/head_music/notation/melisma_fill.rb +22 -0
- data/lib/head_music/notation/music_xml/attributes_reader.rb +103 -0
- data/lib/head_music/notation/music_xml/attributes_writer.rb +62 -41
- data/lib/head_music/notation/music_xml/barline_reader.rb +121 -0
- data/lib/head_music/notation/music_xml/barline_writer.rb +102 -0
- data/lib/head_music/notation/music_xml/beam_grouper.rb +10 -40
- data/lib/head_music/notation/music_xml/beam_reader.rb +46 -0
- data/lib/head_music/notation/music_xml/clef_reader.rb +20 -0
- data/lib/head_music/notation/music_xml/direction_marks.rb +51 -0
- data/lib/head_music/notation/music_xml/direction_plan.rb +135 -0
- data/lib/head_music/notation/music_xml/direction_reader.rb +142 -0
- data/lib/head_music/notation/music_xml/direction_writer.rb +73 -0
- data/lib/head_music/notation/music_xml/divisions.rb +45 -10
- data/lib/head_music/notation/music_xml/document.rb +77 -0
- data/lib/head_music/notation/music_xml/duration_writer.rb +21 -6
- data/lib/head_music/notation/music_xml/fermata_reader.rb +57 -0
- data/lib/head_music/notation/music_xml/flow_builder.rb +47 -0
- data/lib/head_music/notation/music_xml/grace_writer.rb +85 -0
- data/lib/head_music/notation/music_xml/identity_reader.rb +81 -0
- data/lib/head_music/notation/music_xml/instrument_sounds.rb +23 -0
- data/lib/head_music/notation/music_xml/instrument_sounds.yml +123 -0
- data/lib/head_music/notation/music_xml/key_mapper.rb +30 -0
- data/lib/head_music/notation/music_xml/lyric_reader.rb +116 -0
- data/lib/head_music/notation/music_xml/lyric_writer.rb +50 -29
- data/lib/head_music/notation/music_xml/mark_reader.rb +28 -0
- data/lib/head_music/notation/music_xml/mark_readers.rb +25 -0
- data/lib/head_music/notation/music_xml/mark_records.rb +124 -0
- data/lib/head_music/notation/music_xml/marking_elements.rb +65 -0
- data/lib/head_music/notation/music_xml/marking_reader.rb +76 -0
- data/lib/head_music/notation/music_xml/navigation_reader.rb +91 -0
- data/lib/head_music/notation/music_xml/navigation_writer.rb +60 -0
- data/lib/head_music/notation/music_xml/note_lines.rb +39 -0
- data/lib/head_music/notation/music_xml/note_reader.rb +100 -0
- data/lib/head_music/notation/music_xml/note_writer.rb +104 -70
- data/lib/head_music/notation/music_xml/parse_preflight.rb +11 -0
- data/lib/head_music/notation/music_xml/parser.rb +39 -0
- data/lib/head_music/notation/music_xml/part_durations.rb +83 -0
- data/lib/head_music/notation/music_xml/part_reader.rb +152 -0
- data/lib/head_music/notation/music_xml/part_roster.rb +44 -0
- data/lib/head_music/notation/music_xml/part_staves.rb +47 -0
- data/lib/head_music/notation/music_xml/part_writer.rb +119 -0
- data/lib/head_music/notation/music_xml/pitch_reader.rb +20 -0
- data/lib/head_music/notation/music_xml/pitch_writer.rb +13 -0
- data/lib/head_music/notation/music_xml/preflight.rb +14 -19
- data/lib/head_music/notation/music_xml/render_plan.rb +57 -49
- data/lib/head_music/notation/music_xml/slur_numbers.rb +98 -0
- data/lib/head_music/notation/music_xml/slur_reader.rb +69 -0
- data/lib/head_music/notation/music_xml/tempo_direction.rb +91 -0
- data/lib/head_music/notation/music_xml/tempo_reader.rb +127 -0
- data/lib/head_music/notation/music_xml/tempo_writer.rb +97 -0
- data/lib/head_music/notation/music_xml/timeline_reader.rb +37 -0
- data/lib/head_music/notation/music_xml/tuplet_marks.rb +76 -0
- data/lib/head_music/notation/music_xml/tuplet_reader.rb +86 -0
- data/lib/head_music/notation/music_xml/voice_tracks.rb +129 -0
- data/lib/head_music/notation/music_xml/wedge_numbers.rb +46 -0
- data/lib/head_music/notation/music_xml/writer.rb +26 -85
- data/lib/head_music/notation/music_xml/written_timing.rb +89 -0
- data/lib/head_music/notation/music_xml/xml_reading.rb +4 -0
- data/lib/head_music/notation/music_xml.rb +33 -1
- data/lib/head_music/notation/parse_preflight.rb +85 -0
- data/lib/head_music/notation/pitch_spelling.rb +30 -0
- data/lib/head_music/notation/preflight_checks.rb +117 -12
- data/lib/head_music/notation/render_plan.rb +47 -4
- data/lib/head_music/notation/span_ends.rb +11 -0
- data/lib/head_music/notation/tempo_mark_reading.rb +130 -0
- data/lib/head_music/notation/tempo_marks.rb +95 -0
- data/lib/head_music/notation/tie_folding.rb +90 -0
- data/lib/head_music/notation/timeline_reading.rb +67 -0
- data/lib/head_music/notation/tuplet_brackets.rb +91 -0
- data/lib/head_music/notation/voice_event_validation.rb +34 -0
- data/lib/head_music/notation/voice_placer.rb +94 -0
- data/lib/head_music/notation/voice_track.rb +133 -0
- data/lib/head_music/notation/written_links.rb +37 -0
- data/lib/head_music/notation/xml_reading.rb +85 -0
- data/lib/head_music/notation/xml_text.rb +22 -0
- data/lib/head_music/notation.rb +40 -0
- data/lib/head_music/rudiment/articulation.rb +10 -0
- data/lib/head_music/rudiment/articulations.yml +6 -0
- data/lib/head_music/rudiment/clef.rb +6 -0
- data/lib/head_music/rudiment/duration_comparison.rb +18 -0
- data/lib/head_music/rudiment/dynamic.rb +52 -0
- data/lib/head_music/rudiment/dynamics.yml +27 -0
- data/lib/head_music/rudiment/key.rb +10 -6
- data/lib/head_music/rudiment/keyed_catalog.rb +73 -0
- data/lib/head_music/rudiment/meter.rb +31 -4
- data/lib/head_music/rudiment/mode.rb +9 -4
- data/lib/head_music/rudiment/note.rb +25 -23
- data/lib/head_music/rudiment/ornament.rb +9 -0
- data/lib/head_music/rudiment/ornaments.yml +13 -0
- data/lib/head_music/rudiment/pitch.rb +1 -0
- data/lib/head_music/rudiment/quality.rb +11 -4
- data/lib/head_music/rudiment/rest.rb +9 -9
- data/lib/head_music/rudiment/rhythm.rb +2 -0
- data/lib/head_music/rudiment/rhythmic_duration.rb +114 -0
- data/lib/head_music/rudiment/rhythmic_element.rb +10 -11
- data/lib/head_music/rudiment/rhythmic_unit.rb +6 -4
- data/lib/head_music/rudiment/rhythmic_value/name_reader.rb +67 -0
- data/lib/head_music/rudiment/rhythmic_value.rb +73 -78
- data/lib/head_music/rudiment/soundable.rb +11 -0
- data/lib/head_music/rudiment/span_kind.rb +52 -0
- data/lib/head_music/rudiment/span_kinds.yml +31 -0
- data/lib/head_music/rudiment/tempo.rb +45 -14
- data/lib/head_music/rudiment/tuplet_ratio.rb +84 -0
- data/lib/head_music/rudiment/unpitched_note.rb +18 -18
- data/lib/head_music/rudiment/unpitched_sound.rb +2 -0
- data/lib/head_music/style/guideline/harmonic_context.rb +15 -11
- data/lib/head_music/style/guideline/voice_context.rb +8 -0
- data/lib/head_music/style/guideline/wording.rb +5 -0
- data/lib/head_music/style/guideline.rb +3 -3
- data/lib/head_music/style/guidelines/allow_fifth_species_rhythmic_values.rb +11 -6
- data/lib/head_music/style/guidelines/allow_whole_half_quarter_notes.rb +1 -1
- data/lib/head_music/style/guidelines/always_move.rb +3 -1
- data/lib/head_music/style/guidelines/contoured/direction_reading.rb +67 -0
- data/lib/head_music/style/guidelines/contoured/turn_reading.rb +66 -0
- data/lib/head_music/style/guidelines/contoured/wave_reading.rb +87 -0
- data/lib/head_music/style/guidelines/contoured.rb +56 -77
- data/lib/head_music/style/guidelines/dissonance_figure_detection.rb +35 -58
- data/lib/head_music/style/guidelines/embellished_suspension_treatment.rb +2 -2
- data/lib/head_music/style/guidelines/first_bar_entry.rb +5 -5
- data/lib/head_music/style/guidelines/first_bar_half_notes.rb +2 -2
- data/lib/head_music/style/guidelines/first_bar_quarter_notes.rb +2 -2
- data/lib/head_music/style/guidelines/first_bar_whole_note.rb +2 -2
- data/lib/head_music/style/guidelines/florid_dissonance_treatment.rb +1 -5
- data/lib/head_music/style/guidelines/limit_octave_leaps.rb +1 -4
- data/lib/head_music/style/guidelines/maximum_notes.rb +1 -6
- data/lib/head_music/style/guidelines/minimum_melodic_intervals.rb +2 -9
- data/lib/head_music/style/guidelines/minimum_notes.rb +1 -8
- data/lib/head_music/style/guidelines/minimum_threshold.rb +3 -1
- data/lib/head_music/style/guidelines/mix_species_textures.rb +3 -1
- data/lib/head_music/style/guidelines/no_parallel_perfect.rb +7 -2
- data/lib/head_music/style/guidelines/no_parallel_perfect_across_barline.rb +5 -36
- data/lib/head_music/style/guidelines/no_rests.rb +1 -1
- data/lib/head_music/style/guidelines/no_rests_after_note.rb +2 -2
- data/lib/head_music/style/guidelines/note_count_per_bar.rb +5 -7
- data/lib/head_music/style/guidelines/note_fills_final_bar.rb +1 -1
- data/lib/head_music/style/guidelines/notes_same_length.rb +16 -16
- data/lib/head_music/style/guidelines/prefer_long_before_short.rb +4 -2
- data/lib/head_music/style/guidelines/set_against_another_voice.rb +2 -2
- data/lib/head_music/style/guidelines/suspension_treatment.rb +3 -21
- data/lib/head_music/style/guidelines/weak_beat_dissonance_treatment.rb +0 -4
- data/lib/head_music/style/guides/fifth_species_melody.rb +3 -6
- data/lib/head_music/style/guides/first_species_melody.rb +2 -8
- data/lib/head_music/style/guides/first_three_species_melody.rb +2 -8
- data/lib/head_music/style/guides/fourth_species_melody.rb +1 -5
- data/lib/head_music/style/guides/second_species_melody.rb +1 -5
- data/lib/head_music/style/guides/species_melody.rb +10 -2
- data/lib/head_music/style/guides/third_species_melody.rb +1 -5
- data/lib/head_music/style/guides/third_species_triple_meter_melody.rb +1 -5
- data/lib/head_music/style/mark.rb +15 -15
- data/lib/head_music/time/a_tempo.rb +23 -0
- data/lib/head_music/time/change_map.rb +46 -0
- data/lib/head_music/time/conductor.rb +27 -2
- data/lib/head_music/time/event_map.rb +16 -9
- data/lib/head_music/time/fermata.rb +20 -0
- data/lib/head_music/time/key_signature_event.rb +30 -1
- data/lib/head_music/time/meter_map.rb +7 -27
- data/lib/head_music/time/meter_scale.rb +64 -0
- data/lib/head_music/time/musical_time_converter.rb +25 -78
- data/lib/head_music/time/tempo_event.rb +11 -4
- data/lib/head_music/time/tempo_map.rb +8 -34
- data/lib/head_music/time/tempo_piece.rb +69 -0
- data/lib/head_music/time/tempo_pieces.rb +112 -0
- data/lib/head_music/time/tempo_ramp.rb +34 -0
- data/lib/head_music/time.rb +8 -1
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +63 -3
- data/references/content-schema.md +52 -28
- data/references/fourth-species-counterpoint.md +21 -21
- data/references/wemi.md +1 -1
- data/user-stories/done/abc-voices-and-meter-changes.md +274 -0
- data/user-stories/done/articulations-ornaments-dynamics.md +386 -0
- data/user-stories/done/bar-markings.md +296 -0
- data/user-stories/done/expand-playing-techniques.md +10 -1
- data/user-stories/done/grade-contour-misses-by-degree.md +634 -0
- data/user-stories/done/hairpins.md +229 -0
- data/user-stories/done/humdrum-kern-import-and-export.md +508 -0
- data/user-stories/done/instrument-variant.md +10 -1
- data/user-stories/done/key-and-spelling-inference.md +480 -0
- data/user-stories/done/lyric-extenders.md +203 -0
- data/user-stories/done/markings-review-follow-ups.md +183 -0
- data/user-stories/done/mei-export.md +450 -0
- data/user-stories/done/mei-import.md +647 -0
- data/user-stories/done/midi-export.md +448 -0
- data/user-stories/done/midi-import.md +664 -0
- data/user-stories/done/musicxml-import.md +613 -0
- data/user-stories/done/place-dynamics-where-no-note-starts.md +145 -0
- data/user-stories/done/require-the-species-rhythm.md +1 -1
- data/user-stories/done/rhythmic-duration.md +220 -0
- data/user-stories/done/spans-across-notes.md +283 -0
- data/user-stories/done/superclass-for-note.md +11 -0
- data/user-stories/done/timeline-expressions.md +535 -0
- data/user-stories/done/tuplets-and-grace-notes.md +549 -0
- data/user-stories/epics/organizing-content.md +1 -1
- data/user-stories/index.html +85 -13
- metadata +424 -14
- data/lib/head_music/content/placement.rb +0 -175
- data/lib/head_music/notation/music_xml/xml_text.rb +0 -21
- data/lib/head_music/notation/placement_validation.rb +0 -19
- 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 (
|
|
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.
|
|
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.
|
|
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 (
|
|
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.
|
|
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.
|
|
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.
|
|
139
|
-
json (
|
|
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.
|
|
183
|
+
sexp_processor (4.17.6)
|
|
183
184
|
simplecov (1.0.3)
|
|
184
|
-
standard (1.
|
|
185
|
+
standard (1.57.0)
|
|
185
186
|
language_server-protocol (~> 3.17.0.2)
|
|
186
187
|
lint_roller (~> 1.0)
|
|
187
|
-
rubocop (~> 1.
|
|
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.
|
|
202
|
-
unicode-emoji (~> 4.
|
|
203
|
-
unicode-emoji (4.
|
|
204
|
-
uri (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
|
|
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,
|
|
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 "
|
|
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
|
|
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
|
-
|
|
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, :
|
|
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
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].
|
|
24
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
54
|
-
|
|
52
|
+
def named_number?
|
|
53
|
+
number <= NUMBER_NAMES.length
|
|
55
54
|
end
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
66
|
-
|
|
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,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
|