head_music 17.3.0 → 17.4.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/CHANGELOG.md +8 -0
- data/Gemfile.lock +1 -1
- data/lib/head_music/analysis/chord_analysis.rb +123 -0
- data/lib/head_music/analysis/circle.rb +11 -3
- data/lib/head_music/analysis/diatonic_interval.rb +14 -11
- data/lib/head_music/analysis/dyad/chord_implication.rb +101 -0
- data/lib/head_music/analysis/dyad.rb +24 -104
- data/lib/head_music/analysis/interval_cycle.rb +9 -3
- data/lib/head_music/analysis/pitch_collection.rb +33 -107
- data/lib/head_music/analysis/sonority.rb +11 -21
- data/lib/head_music/content/composition/hash_deserializer.rb +110 -0
- data/lib/head_music/content/composition/schema_values.rb +127 -0
- data/lib/head_music/content/composition.rb +0 -216
- data/lib/head_music/content/placement.rb +1 -41
- data/lib/head_music/content/sound_resolver.rb +54 -0
- data/lib/head_music/content/voice/melodic_line.rb +32 -0
- data/lib/head_music/content/voice/melodic_note_pair.rb +33 -0
- data/lib/head_music/content/voice.rb +7 -58
- data/lib/head_music/instruments/alternate_tuning.rb +4 -6
- data/lib/head_music/instruments/catalog_lookup.rb +18 -0
- data/lib/head_music/instruments/instrument.rb +17 -117
- data/lib/head_music/instruments/instrument_catalog.rb +48 -0
- data/lib/head_music/instruments/instrument_configuration.rb +4 -6
- data/lib/head_music/instruments/instrument_family.rb +3 -8
- data/lib/head_music/instruments/instrument_name.rb +49 -0
- data/lib/head_music/instruments/score_order.rb +52 -42
- data/lib/head_music/instruments/staff_profile.rb +71 -0
- data/lib/head_music/instruments/stringing.rb +4 -6
- data/lib/head_music/instruments/stringing_course.rb +4 -6
- data/lib/head_music/instruments/variant.rb +4 -6
- data/lib/head_music/named/locale.rb +32 -0
- data/lib/head_music/named/localized_name.rb +15 -0
- data/lib/head_music/named.rb +1 -45
- data/lib/head_music/notation/abc/body_lexer.rb +48 -23
- data/lib/head_music/notation/abc/duration_resolver.rb +11 -1
- data/lib/head_music/notation/abc/duration_writer.rb +1 -8
- data/lib/head_music/notation/abc/header.rb +27 -16
- data/lib/head_music/notation/abc/parser.rb +44 -183
- data/lib/head_music/notation/abc/preflight.rb +48 -0
- data/lib/head_music/notation/abc/voice_state.rb +161 -0
- data/lib/head_music/notation/abc/writer.rb +34 -17
- data/lib/head_music/notation/dotted_duration.rb +19 -0
- data/lib/head_music/notation/instrument_notation.rb +5 -7
- data/lib/head_music/notation/music_xml/duration_writer.rb +3 -8
- data/lib/head_music/notation/music_xml/preflight.rb +90 -0
- data/lib/head_music/notation/music_xml/render_plan.rb +161 -0
- data/lib/head_music/notation/music_xml/writer.rb +20 -209
- data/lib/head_music/notation/placement_validation.rb +19 -0
- data/lib/head_music/rudiment/enharmonic_equivalence.rb +33 -0
- data/lib/head_music/rudiment/key.rb +8 -36
- data/lib/head_music/rudiment/key_signature/enharmonic_equivalence.rb +21 -15
- data/lib/head_music/rudiment/key_signature.rb +18 -19
- data/lib/head_music/rudiment/meter.rb +17 -13
- data/lib/head_music/rudiment/mode.rb +33 -65
- data/lib/head_music/rudiment/note.rb +18 -21
- data/lib/head_music/rudiment/pitch/enharmonic_equivalence.rb +4 -15
- data/lib/head_music/rudiment/pitch/natural_step.rb +36 -0
- data/lib/head_music/rudiment/pitch/parser.rb +16 -5
- data/lib/head_music/rudiment/pitch.rb +11 -33
- data/lib/head_music/rudiment/qualified_diatonic_context.rb +61 -0
- data/lib/head_music/rudiment/rhythmic_unit/parser.rb +16 -12
- data/lib/head_music/rudiment/rhythmic_unit.rb +21 -37
- data/lib/head_music/rudiment/rhythmic_value/parser.rb +53 -46
- data/lib/head_music/rudiment/spelling/enharmonic_equivalence.rb +13 -0
- data/lib/head_music/rudiment/spelling.rb +0 -25
- data/lib/head_music/rudiment/tuning/just_intonation.rb +2 -4
- data/lib/head_music/rudiment/tuning/meantone.rb +2 -4
- data/lib/head_music/rudiment/tuning/pythagorean.rb +2 -4
- data/lib/head_music/rudiment/tuning/tonal_centered.rb +10 -0
- data/lib/head_music/rudiment/unpitched_note.rb +3 -5
- data/lib/head_music/style/annotation/configured.rb +35 -0
- data/lib/head_music/style/annotation.rb +32 -44
- data/lib/head_music/style/guidelines/allowed_rhythmic_values_for_fifth_species.rb +0 -10
- data/lib/head_music/style/guidelines/contoured.rb +65 -53
- data/lib/head_music/style/guidelines/dissonance_figure_detection.rb +55 -16
- data/lib/head_music/style/guidelines/florid_dissonance_treatment.rb +0 -37
- data/lib/head_music/style/guidelines/large_leaps.rb +10 -2
- data/lib/head_music/style/guidelines/minimum_melodic_intervals.rb +3 -25
- data/lib/head_music/style/guidelines/minimum_notes.rb +3 -25
- data/lib/head_music/style/guidelines/minimum_threshold.rb +38 -0
- data/lib/head_music/style/guidelines/no_parallel_perfect.rb +31 -0
- data/lib/head_music/style/guidelines/no_parallel_perfect_on_downbeats.rb +3 -17
- data/lib/head_music/style/guidelines/no_parallel_perfect_with_syncopation.rb +3 -17
- data/lib/head_music/style/guidelines/prepare_octave_leaps.rb +10 -7
- data/lib/head_music/style/guidelines/singable_intervals.rb +22 -8
- data/lib/head_music/style/guidelines/suspension_treatment.rb +30 -12
- data/lib/head_music/style/guidelines/third_species_dissonance_treatment.rb +0 -7
- data/lib/head_music/style/guidelines/triple_meter_dissonance_treatment.rb +0 -12
- data/lib/head_music/style/guidelines/weak_beat_dissonance_treatment.rb +2 -42
- data/lib/head_music/style/guides/fifth_species_melody.rb +3 -13
- data/lib/head_music/style/guides/fourth_species_melody.rb +3 -13
- data/lib/head_music/style/guides/second_species_harmony.rb +2 -5
- data/lib/head_music/style/guides/second_species_melody.rb +2 -12
- data/lib/head_music/style/guides/species_harmony.rb +14 -0
- data/lib/head_music/style/guides/species_melody.rb +21 -0
- data/lib/head_music/style/guides/third_species_harmony.rb +2 -5
- data/lib/head_music/style/guides/third_species_melody.rb +3 -13
- data/lib/head_music/style/guides/third_species_triple_meter_harmony.rb +2 -5
- data/lib/head_music/style/guides/third_species_triple_meter_melody.rb +2 -12
- data/lib/head_music/time/conductor.rb +19 -125
- data/lib/head_music/time/musical_position.rb +26 -26
- data/lib/head_music/time/musical_time_converter.rb +104 -0
- data/lib/head_music/time/smpte_converter.rb +51 -0
- data/lib/head_music/time/smpte_timecode.rb +25 -23
- data/lib/head_music/time.rb +2 -0
- data/lib/head_music/value_equality.rb +37 -0
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +25 -1
- metadata +31 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4d075fb623ea50351c7ac422f44be04c265867253312b6b27faffaf76bb3b397
|
|
4
|
+
data.tar.gz: 5649553e987c0a98cd9388d24d6e15155f88da4e59eaf62f7a2d9f988e656b36
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a89a0dfc44c68e823e5c1eff6c48b950b759cdd059c504adacd8bd6b3d06dc0b23bc1a13b690e68086526a33ab3bebc1617825abecfc3ab108c89425a44d6f55
|
|
7
|
+
data.tar.gz: 921ce718ccc4b728ed57dce347fbce73ae7c120960931b32933a5893f52992b4aa40ec115e248421a3f53d3a66defff0b4d7eac15efaf34ae51fae23e5e2a328
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [17.4.0] - 2026-07-20
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Internal refactoring for clarity and maintainability, with no changes to public behavior or output. Complex classes were split along their natural seams by extracting focused collaborators and value objects, each with its own spec: `MusicXML::Preflight` and `MusicXML::RenderPlan` from the MusicXML `Writer`; `Instruments::InstrumentName`, `InstrumentCatalog`, and `StaffProfile` from `Instrument`; `Analysis::ChordAnalysis` from `PitchCollection`; `Content::SoundResolver` from `Placement`; `Time::SmpteConverter` and `MusicalTimeConverter` from `Conductor`; `Composition::SchemaValues` from `HashDeserializer`; `Pitch::NaturalStep` from `Pitch`; `Voice::MelodicLine` from `Voice`; `Dyad::ChordImplication` from `Dyad`; and `ABC::Preflight` plus per-voice note-assembly moved onto `ABC::VoiceState` in the ABC parser.
|
|
15
|
+
- Shared value-object equality was consolidated into a `ValueEquality` mixin, and duplication was reduced across the style guides, notation writers, and rudiments.
|
|
16
|
+
- Nested classes that had outgrown their host files were given their own files without changing their constant paths: `Named::Locale` and `Named::LocalizedName`, `Voice::MelodicNotePair`, `Spelling::EnharmonicEquivalence`, and `Style::Annotation::Configured`.
|
|
17
|
+
|
|
10
18
|
## [17.3.0] - 2026-07-19
|
|
11
19
|
|
|
12
20
|
### Added
|
data/Gemfile.lock
CHANGED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# A module for musical analysis
|
|
2
|
+
module HeadMusic::Analysis; end
|
|
3
|
+
|
|
4
|
+
# Classifies a PitchCollection as a tertian chord: whether it is a triad,
|
|
5
|
+
# seventh, or extended chord; its triad quality (major/minor/diminished/
|
|
6
|
+
# augmented); and its inversion. "Tertian" means the pitches, once reduced to
|
|
7
|
+
# within an octave and rotated, stack in thirds.
|
|
8
|
+
class HeadMusic::Analysis::ChordAnalysis
|
|
9
|
+
# Scale-degree signatures (above the bass) of each tertian chord size, used
|
|
10
|
+
# to recognize a stack of thirds in any rotation.
|
|
11
|
+
TERTIAN_SONORITIES = {
|
|
12
|
+
implied_triad: [3],
|
|
13
|
+
triad: [3, 5],
|
|
14
|
+
seventh_chord: [3, 5, 7],
|
|
15
|
+
ninth_chord: [2, 3, 5, 7],
|
|
16
|
+
eleventh_chord: [2, 3, 4, 5, 7],
|
|
17
|
+
thirteenth_chord: [2, 3, 4, 5, 6, 7] # a.k.a. diatonic scale
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
# The interval-shorthand pairs (bass-to-third, third-to-fifth) that spell each
|
|
21
|
+
# triad quality, in every inversion.
|
|
22
|
+
TRIAD_PATTERNS = {
|
|
23
|
+
major: [%w[M3 m3], %w[m3 P4], %w[P4 M3]],
|
|
24
|
+
minor: [%w[m3 M3], %w[M3 P4], %w[P4 m3]],
|
|
25
|
+
diminished: [%w[m3 m3], %w[m3 A4], %w[A4 m3]],
|
|
26
|
+
augmented: [%w[M3 M3], %w[M3 d4], %w[d4 M3]]
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
attr_reader :collection
|
|
30
|
+
|
|
31
|
+
def initialize(collection)
|
|
32
|
+
@collection = collection
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def triad?
|
|
36
|
+
collection.trichord? && tertian?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def consonant_triad?
|
|
40
|
+
major_triad? || minor_triad?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def major_triad?
|
|
44
|
+
triad_type?(:major)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def minor_triad?
|
|
48
|
+
triad_type?(:minor)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def diminished_triad?
|
|
52
|
+
triad_type?(:diminished)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def augmented_triad?
|
|
56
|
+
triad_type?(:augmented)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def root_position_triad?
|
|
60
|
+
collection.trichord? && stacked_in_thirds?(collection.reduction)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def first_inversion_triad?
|
|
64
|
+
collection.trichord? && stacked_in_thirds?(collection.reduction.uninvert)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def second_inversion_triad?
|
|
68
|
+
collection.trichord? && stacked_in_thirds?(collection.reduction.invert)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def seventh_chord?
|
|
72
|
+
collection.tetrachord? && tertian?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def root_position_seventh_chord?
|
|
76
|
+
collection.tetrachord? && stacked_in_thirds?(collection.reduction)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def first_inversion_seventh_chord?
|
|
80
|
+
collection.tetrachord? && stacked_in_thirds?(collection.reduction.uninvert)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def second_inversion_seventh_chord?
|
|
84
|
+
collection.tetrachord? && stacked_in_thirds?(collection.reduction.uninvert.uninvert)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def third_inversion_seventh_chord?
|
|
88
|
+
collection.tetrachord? && stacked_in_thirds?(collection.reduction.invert)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def ninth_chord?
|
|
92
|
+
collection.pentachord? && tertian?
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def eleventh_chord?
|
|
96
|
+
collection.hexachord? && tertian?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def thirteenth_chord?
|
|
100
|
+
collection.heptachord? && tertian?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def tertian?
|
|
104
|
+
return false unless collection.diatonic_intervals.any?
|
|
105
|
+
|
|
106
|
+
inversion = collection.reduction
|
|
107
|
+
collection.pitches.length.times do
|
|
108
|
+
return true if TERTIAN_SONORITIES.value?(inversion.scale_degrees_above_bass_pitch)
|
|
109
|
+
inversion = inversion.invert
|
|
110
|
+
end
|
|
111
|
+
false
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def triad_type?(type)
|
|
117
|
+
TRIAD_PATTERNS[type].include?(collection.reduction_diatonic_intervals.map(&:shorthand))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def stacked_in_thirds?(reduced_collection)
|
|
121
|
+
reduced_collection.diatonic_intervals.all?(&:third?)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -46,14 +46,22 @@ class HeadMusic::Analysis::Circle < HeadMusic::Analysis::IntervalCycle
|
|
|
46
46
|
def pitches_down
|
|
47
47
|
@pitches_down ||= [starting_pitch].tap do |list|
|
|
48
48
|
loop do
|
|
49
|
-
next_pitch = list.last
|
|
50
|
-
|
|
51
|
-
break if next_pitch.pitch_class == list.first.pitch_class
|
|
49
|
+
next_pitch = folded_down(list.last)
|
|
50
|
+
break if next_pitch.pitch_class == starting_pitch.pitch_class
|
|
52
51
|
|
|
53
52
|
list << next_pitch
|
|
54
53
|
end
|
|
55
54
|
end
|
|
56
55
|
end
|
|
57
56
|
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# The next pitch a cycle-interval below, folded up by octaves to stay within an octave of the start.
|
|
60
|
+
def folded_down(pitch)
|
|
61
|
+
next_pitch = pitch - interval
|
|
62
|
+
next_pitch += octave while starting_pitch - next_pitch > 12
|
|
63
|
+
next_pitch
|
|
64
|
+
end
|
|
65
|
+
|
|
58
66
|
private_class_method :new
|
|
59
67
|
end
|
|
@@ -59,15 +59,17 @@ class HeadMusic::Analysis::DiatonicInterval
|
|
|
59
59
|
|
|
60
60
|
# Override Named module method to try I18n and fall back to computed name
|
|
61
61
|
def name(locale_code: nil)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
computed_name = naming.name
|
|
63
|
+
return computed_name unless locale_code
|
|
64
|
+
|
|
65
|
+
translations = I18n.backend.translations[locale_code]
|
|
66
|
+
return computed_name unless translations
|
|
67
|
+
|
|
68
|
+
name_key = HeadMusic::Utilities::HashKey.for(computed_name)
|
|
69
|
+
locale_data = translations[:head_music] || {}
|
|
70
|
+
locale_data.dig(:diatonic_intervals, name_key) ||
|
|
71
|
+
locale_data.dig(:chromatic_intervals, name_key) ||
|
|
72
|
+
computed_name
|
|
71
73
|
end
|
|
72
74
|
|
|
73
75
|
def to_s
|
|
@@ -79,8 +81,9 @@ class HeadMusic::Analysis::DiatonicInterval
|
|
|
79
81
|
if identifier.is_a?(String) || identifier.is_a?(Symbol)
|
|
80
82
|
name = Parser.new(identifier)
|
|
81
83
|
semitones = Semitones.new(name.degree_name.to_sym, name.quality_name).count
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
middle_c = HeadMusic::Rudiment::Pitch.middle_c
|
|
85
|
+
higher_pitch = HeadMusic::Rudiment::Pitch.from_number_and_letter(middle_c + semitones, name.higher_letter)
|
|
86
|
+
interval = new(middle_c, higher_pitch)
|
|
84
87
|
interval.ensure_localized_name(name: identifier.to_s)
|
|
85
88
|
interval
|
|
86
89
|
else
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
class HeadMusic::Analysis::Dyad
|
|
2
|
+
# Enumerates the chords (as PitchCollections) that contain a given dyad.
|
|
3
|
+
# Trichords and seventh chords are built at every chromatic root from a fixed
|
|
4
|
+
# table of interval stacks, then kept only when they actually span the dyad.
|
|
5
|
+
# An optional key narrows the results to diatonic chords and ranks the
|
|
6
|
+
# survivors by how many of their pitches the key contains.
|
|
7
|
+
class ChordImplication
|
|
8
|
+
TRICHORD_INTERVALS = [
|
|
9
|
+
%w[M3 P5], # major triad
|
|
10
|
+
%w[m3 P5], # minor triad
|
|
11
|
+
%w[m3 d5], # diminished triad
|
|
12
|
+
%w[M3 A5], # augmented triad
|
|
13
|
+
%w[P4 P5], # sus4 (not a triad)
|
|
14
|
+
%w[M2 P5] # sus2 (not a triad)
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
SEVENTH_CHORD_INTERVALS = [
|
|
18
|
+
%w[M3 P5 M7], # major seventh
|
|
19
|
+
%w[M3 P5 m7], # dominant seventh (major-minor)
|
|
20
|
+
%w[m3 P5 m7], # minor seventh
|
|
21
|
+
%w[m3 P5 M7], # minor-major seventh
|
|
22
|
+
%w[m3 d5 m7], # half-diminished seventh
|
|
23
|
+
%w[m3 d5 d7], # diminished seventh
|
|
24
|
+
%w[M2 M3 P5 m7], # dominant ninth
|
|
25
|
+
%w[m2 M3 P5 m7], # dominant minor ninth
|
|
26
|
+
%w[M2 m3 P5 m7], # minor ninth
|
|
27
|
+
%w[M2 M3 P5 M7] # major ninth
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
attr_reader :dyad_pitch_classes, :key
|
|
31
|
+
|
|
32
|
+
def initialize(dyad_pitch_classes, key)
|
|
33
|
+
@dyad_pitch_classes = dyad_pitch_classes
|
|
34
|
+
@key = key
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def trichords
|
|
38
|
+
@trichords ||= ranked(chords_from(TRICHORD_INTERVALS))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def seventh_chords
|
|
42
|
+
@seventh_chords ||= ranked(chords_from(SEVENTH_CHORD_INTERVALS))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# Without a key every chord stands; with one, keep the diatonic chords and
|
|
48
|
+
# order them by how much they agree with the key. Concentrating the key
|
|
49
|
+
# check here keeps filter and sort free of their own guards.
|
|
50
|
+
def ranked(chords)
|
|
51
|
+
return chords unless key
|
|
52
|
+
|
|
53
|
+
sort_by_diatonic_agreement(filter_by_key(chords))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def chords_from(interval_sets)
|
|
57
|
+
candidate_chords(interval_sets)
|
|
58
|
+
.select { |chord| includes_dyad?(chord) }
|
|
59
|
+
.uniq { |chord| chord.pitch_classes.sort.map(&:to_i) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def candidate_chords(interval_sets)
|
|
63
|
+
candidate_roots.flat_map do |root_pitch|
|
|
64
|
+
interval_sets.map { |intervals| build_chord(root_pitch, intervals) }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def candidate_roots
|
|
69
|
+
HeadMusic::Rudiment::Spelling::CHROMATIC_SPELLINGS.map do |root_spelling|
|
|
70
|
+
HeadMusic::Rudiment::Pitch.get("#{root_spelling}4")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def build_chord(root_pitch, intervals)
|
|
75
|
+
chord_pitches = [root_pitch] + intervals.map do |name|
|
|
76
|
+
HeadMusic::Analysis::DiatonicInterval.get(name).above(root_pitch)
|
|
77
|
+
end
|
|
78
|
+
HeadMusic::Analysis::PitchCollection.new(chord_pitches)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def includes_dyad?(pitch_collection)
|
|
82
|
+
dyad_pitch_classes.all? { |pitch_class| pitch_collection.pitch_classes.include?(pitch_class) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def filter_by_key(pitch_collections)
|
|
86
|
+
pitch_collections.select do |pitch_collection|
|
|
87
|
+
pitch_collection.pitches.all? { |pitch| diatonic_spellings.include?(pitch.spelling) }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def sort_by_diatonic_agreement(pitch_collections)
|
|
92
|
+
pitch_collections.sort_by do |pitch_collection|
|
|
93
|
+
-pitch_collection.pitches.count { |pitch| diatonic_spellings.include?(pitch.spelling) }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def diatonic_spellings
|
|
98
|
+
@diatonic_spellings ||= key.scale.spellings
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -31,11 +31,7 @@ class HeadMusic::Analysis::Dyad
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def possible_trichords
|
|
34
|
-
|
|
35
|
-
trichords = generate_possible_trichords
|
|
36
|
-
trichords = filter_by_key(trichords) if key
|
|
37
|
-
sort_by_diatonic_agreement(trichords)
|
|
38
|
-
end
|
|
34
|
+
chord_implication.trichords
|
|
39
35
|
end
|
|
40
36
|
|
|
41
37
|
def possible_triads
|
|
@@ -43,11 +39,7 @@ class HeadMusic::Analysis::Dyad
|
|
|
43
39
|
end
|
|
44
40
|
|
|
45
41
|
def possible_seventh_chords
|
|
46
|
-
|
|
47
|
-
seventh_chords = generate_possible_seventh_chords
|
|
48
|
-
seventh_chords = filter_by_key(seventh_chords) if key
|
|
49
|
-
sort_by_diatonic_agreement(seventh_chords)
|
|
50
|
-
end
|
|
42
|
+
chord_implication.seventh_chords
|
|
51
43
|
end
|
|
52
44
|
|
|
53
45
|
def enharmonic_respellings
|
|
@@ -68,111 +60,39 @@ class HeadMusic::Analysis::Dyad
|
|
|
68
60
|
|
|
69
61
|
private
|
|
70
62
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
%w[m3 P5], # minor triad
|
|
74
|
-
%w[m3 d5], # diminished triad
|
|
75
|
-
%w[M3 A5], # augmented triad
|
|
76
|
-
%w[P4 P5], # sus4 (not a triad)
|
|
77
|
-
%w[M2 P5] # sus2 (not a triad)
|
|
78
|
-
].freeze
|
|
79
|
-
|
|
80
|
-
SEVENTH_CHORD_INTERVALS = [
|
|
81
|
-
%w[M3 P5 M7], # major seventh
|
|
82
|
-
%w[M3 P5 m7], # dominant seventh (major-minor)
|
|
83
|
-
%w[m3 P5 m7], # minor seventh
|
|
84
|
-
%w[m3 P5 M7], # minor-major seventh
|
|
85
|
-
%w[m3 d5 m7], # half-diminished seventh
|
|
86
|
-
%w[m3 d5 d7], # diminished seventh
|
|
87
|
-
%w[M2 M3 P5 m7], # dominant ninth
|
|
88
|
-
%w[m2 M3 P5 m7], # dominant minor ninth
|
|
89
|
-
%w[M2 m3 P5 m7], # minor ninth
|
|
90
|
-
%w[M2 M3 P5 M7] # major ninth
|
|
91
|
-
].freeze
|
|
92
|
-
|
|
93
|
-
def generate_possible_trichords
|
|
94
|
-
generate_possible_chords(TRICHORD_INTERVALS)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def generate_possible_seventh_chords
|
|
98
|
-
generate_possible_chords(SEVENTH_CHORD_INTERVALS)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def generate_possible_chords(interval_sets)
|
|
102
|
-
dyad_pitch_classes = [lower_pitch.pitch_class, upper_pitch.pitch_class]
|
|
103
|
-
chords = []
|
|
104
|
-
|
|
105
|
-
HeadMusic::Rudiment::Spelling::CHROMATIC_SPELLINGS.each do |root_spelling|
|
|
106
|
-
root_pitch = HeadMusic::Rudiment::Pitch.get("#{root_spelling}4")
|
|
107
|
-
|
|
108
|
-
interval_sets.each do |intervals|
|
|
109
|
-
chord_pitches = [root_pitch] + intervals.map { |name| HeadMusic::Analysis::DiatonicInterval.get(name).above(root_pitch) }
|
|
110
|
-
pitch_collection = HeadMusic::Analysis::PitchCollection.new(chord_pitches)
|
|
111
|
-
|
|
112
|
-
if dyad_pitch_classes.all? { |pc| pitch_collection.pitch_classes.include?(pc) }
|
|
113
|
-
chords << pitch_collection
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
chords.uniq { |chord| chord.pitch_classes.sort.map(&:to_i) }
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def filter_by_key(pitch_collections)
|
|
122
|
-
return pitch_collections unless key
|
|
123
|
-
|
|
124
|
-
pitch_collections.select do |pitch_collection|
|
|
125
|
-
pitch_collection.pitches.all? { |pitch| diatonic_spellings.include?(pitch.spelling) }
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def sort_by_diatonic_agreement(pitch_collections)
|
|
130
|
-
return pitch_collections unless key
|
|
131
|
-
|
|
132
|
-
pitch_collections.sort_by do |pitch_collection|
|
|
133
|
-
-pitch_collection.pitches.count { |pitch| diatonic_spellings.include?(pitch.spelling) }
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def diatonic_spellings
|
|
138
|
-
@diatonic_spellings ||= key.scale.spellings
|
|
63
|
+
def chord_implication
|
|
64
|
+
@chord_implication ||= ChordImplication.new([lower_pitch.pitch_class, upper_pitch.pitch_class], key)
|
|
139
65
|
end
|
|
140
66
|
|
|
141
67
|
def generate_enharmonic_respellings
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
# Get enharmonic equivalents for each pitch
|
|
145
|
-
pitch1_equivalents = enharmonic_equivalents_for(pitch1)
|
|
146
|
-
pitch2_equivalents = enharmonic_equivalents_for(pitch2)
|
|
68
|
+
lower_equivalents = enharmonic_equivalents_for(pitch1)
|
|
69
|
+
upper_equivalents = enharmonic_equivalents_for(pitch2)
|
|
147
70
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
respellings << self.class.new(lower, upper, key: key)
|
|
154
|
-
end
|
|
155
|
-
end
|
|
71
|
+
lower_equivalents.product(upper_equivalents)
|
|
72
|
+
.reject { |lower, upper| original_spelling?(lower, upper) }
|
|
73
|
+
.map { |lower, upper| self.class.new(lower, upper, key: key) }
|
|
74
|
+
end
|
|
156
75
|
|
|
157
|
-
|
|
76
|
+
def original_spelling?(lower, upper)
|
|
77
|
+
lower.spelling == pitch1.spelling && upper.spelling == pitch2.spelling
|
|
158
78
|
end
|
|
159
79
|
|
|
160
80
|
ALTERATION_SIGNS = {-2 => "bb", -1 => "b", 0 => "", 1 => "#", 2 => "##"}.freeze
|
|
161
81
|
|
|
162
82
|
def enharmonic_equivalents_for(pitch)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
HeadMusic::Rudiment::LetterName.all.each do |letter_name|
|
|
167
|
-
ALTERATION_SIGNS.each_value do |sign|
|
|
168
|
-
spelling = HeadMusic::Rudiment::Spelling.get("#{letter_name}#{sign}")
|
|
169
|
-
next unless spelling && spelling.pitch_class == target_pitch_class
|
|
170
|
-
next if equivalents.any? { |equiv| equiv.spelling == spelling }
|
|
171
|
-
|
|
172
|
-
equivalents << HeadMusic::Rudiment::Pitch.fetch_or_create(spelling, pitch.register)
|
|
173
|
-
end
|
|
83
|
+
equivalent_pitches = enharmonic_spellings_for(pitch.pitch_class).map do |spelling|
|
|
84
|
+
HeadMusic::Rudiment::Pitch.fetch_or_create(spelling, pitch.register)
|
|
174
85
|
end
|
|
86
|
+
[pitch, *equivalent_pitches].uniq(&:spelling)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def enharmonic_spellings_for(target_pitch_class)
|
|
90
|
+
all_spellings.select { |spelling| spelling.pitch_class == target_pitch_class }
|
|
91
|
+
end
|
|
175
92
|
|
|
176
|
-
|
|
93
|
+
def all_spellings
|
|
94
|
+
HeadMusic::Rudiment::LetterName.all.flat_map do |letter_name|
|
|
95
|
+
ALTERATION_SIGNS.each_value.map { |sign| HeadMusic::Rudiment::Spelling.get("#{letter_name}#{sign}") }
|
|
96
|
+
end.compact
|
|
177
97
|
end
|
|
178
98
|
end
|
|
@@ -41,15 +41,21 @@ class HeadMusic::Analysis::IntervalCycle
|
|
|
41
41
|
def pitches_up
|
|
42
42
|
@pitches_up ||= [starting_pitch].tap do |list|
|
|
43
43
|
loop do
|
|
44
|
-
next_pitch = list.last
|
|
45
|
-
|
|
46
|
-
break if next_pitch.pitch_class == list.first.pitch_class
|
|
44
|
+
next_pitch = folded_up(list.last)
|
|
45
|
+
break if next_pitch.pitch_class == starting_pitch.pitch_class
|
|
47
46
|
|
|
48
47
|
list << next_pitch
|
|
49
48
|
end
|
|
50
49
|
end
|
|
51
50
|
end
|
|
52
51
|
|
|
52
|
+
# The next pitch a cycle-interval above, folded down by octaves to stay within an octave of the start.
|
|
53
|
+
def folded_up(pitch)
|
|
54
|
+
next_pitch = pitch + interval
|
|
55
|
+
next_pitch -= octave while next_pitch - starting_pitch > 12
|
|
56
|
+
next_pitch
|
|
57
|
+
end
|
|
58
|
+
|
|
53
59
|
def octave
|
|
54
60
|
@octave ||= HeadMusic::Analysis::DiatonicInterval.get(:perfect_octave)
|
|
55
61
|
end
|