head_music 8.2.1 → 9.0.1
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/.github/workflows/ci.yml +1 -1
- data/.github/workflows/release.yml +1 -1
- data/CHANGELOG.md +53 -0
- data/CLAUDE.md +151 -0
- data/Gemfile.lock +25 -25
- data/MUSIC_THEORY.md +120 -0
- data/Rakefile +2 -2
- data/bin/check_instrument_consistency.rb +86 -0
- data/check_instrument_consistency.rb +0 -0
- data/head_music.gemspec +1 -1
- data/lib/head_music/analysis/diatonic_interval/naming.rb +1 -1
- data/lib/head_music/analysis/diatonic_interval.rb +50 -27
- data/lib/head_music/analysis/interval_consonance.rb +51 -0
- data/lib/head_music/content/note.rb +1 -1
- data/lib/head_music/content/placement.rb +1 -1
- data/lib/head_music/content/position.rb +1 -1
- data/lib/head_music/content/staff.rb +1 -1
- data/lib/head_music/instruments/instrument.rb +103 -113
- data/lib/head_music/instruments/instrument_families.yml +10 -9
- data/lib/head_music/instruments/instrument_family.rb +13 -2
- data/lib/head_music/instruments/instrument_type.rb +188 -0
- data/lib/head_music/instruments/instruments.yml +350 -368
- data/lib/head_music/instruments/score_order.rb +139 -0
- data/lib/head_music/instruments/score_orders.yml +130 -0
- data/lib/head_music/instruments/variant.rb +6 -0
- data/lib/head_music/locales/de.yml +6 -0
- data/lib/head_music/locales/en.yml +98 -0
- data/lib/head_music/locales/es.yml +6 -0
- data/lib/head_music/locales/fr.yml +6 -0
- data/lib/head_music/locales/it.yml +6 -0
- data/lib/head_music/locales/ru.yml +6 -0
- data/lib/head_music/rudiment/alteration.rb +23 -8
- data/lib/head_music/rudiment/base.rb +9 -0
- data/lib/head_music/rudiment/chromatic_interval.rb +3 -6
- data/lib/head_music/rudiment/clef.rb +1 -1
- data/lib/head_music/rudiment/consonance.rb +37 -4
- data/lib/head_music/rudiment/diatonic_context.rb +25 -0
- data/lib/head_music/rudiment/key.rb +77 -0
- data/lib/head_music/rudiment/key_signature/enharmonic_equivalence.rb +1 -1
- data/lib/head_music/rudiment/key_signature.rb +46 -7
- data/lib/head_music/rudiment/letter_name.rb +3 -3
- data/lib/head_music/rudiment/meter.rb +19 -9
- data/lib/head_music/rudiment/mode.rb +92 -0
- data/lib/head_music/rudiment/musical_symbol.rb +1 -1
- data/lib/head_music/rudiment/note.rb +112 -0
- data/lib/head_music/rudiment/pitch/parser.rb +52 -0
- data/lib/head_music/rudiment/pitch.rb +5 -6
- data/lib/head_music/rudiment/pitch_class.rb +1 -1
- data/lib/head_music/rudiment/quality.rb +1 -1
- data/lib/head_music/rudiment/reference_pitch.rb +1 -1
- data/lib/head_music/rudiment/register.rb +4 -1
- data/lib/head_music/rudiment/rest.rb +36 -0
- data/lib/head_music/rudiment/rhythmic_element.rb +53 -0
- data/lib/head_music/rudiment/rhythmic_unit/parser.rb +86 -0
- data/lib/head_music/rudiment/rhythmic_unit.rb +104 -29
- data/lib/head_music/rudiment/rhythmic_units.yml +80 -0
- data/lib/head_music/rudiment/rhythmic_value/parser.rb +77 -0
- data/lib/head_music/{content → rudiment}/rhythmic_value.rb +23 -5
- data/lib/head_music/rudiment/scale.rb +4 -5
- data/lib/head_music/rudiment/scale_degree.rb +9 -4
- data/lib/head_music/rudiment/scale_type.rb +9 -3
- data/lib/head_music/rudiment/solmization.rb +1 -1
- data/lib/head_music/rudiment/spelling.rb +5 -4
- data/lib/head_music/rudiment/tempo.rb +85 -0
- data/lib/head_music/rudiment/tonal_context.rb +35 -0
- data/lib/head_music/rudiment/tuning/just_intonation.rb +85 -0
- data/lib/head_music/rudiment/tuning/meantone.rb +87 -0
- data/lib/head_music/rudiment/tuning/pythagorean.rb +91 -0
- data/lib/head_music/rudiment/tuning.rb +18 -4
- data/lib/head_music/rudiment/unpitched_note.rb +62 -0
- data/lib/head_music/style/annotation.rb +4 -4
- data/lib/head_music/style/guidelines/notes_same_length.rb +16 -16
- data/lib/head_music/style/medieval_tradition.rb +26 -0
- data/lib/head_music/style/modern_tradition.rb +34 -0
- data/lib/head_music/style/renaissance_tradition.rb +26 -0
- data/lib/head_music/style/tradition.rb +21 -0
- data/lib/head_music/utilities/hash_key.rb +34 -2
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +33 -9
- data/user_stories/active/handle-time.md +7 -0
- data/user_stories/active/handle-time.rb +177 -0
- data/user_stories/done/epic--score-order/PLAN.md +244 -0
- data/user_stories/done/epic--score-order/band-score-order.md +38 -0
- data/user_stories/done/epic--score-order/chamber-ensemble-score-order.md +33 -0
- data/user_stories/done/epic--score-order/orchestral-score-order.md +43 -0
- data/user_stories/done/instrument-variant.md +65 -0
- data/user_stories/done/superclass-for-note.md +30 -0
- data/user_stories/todo/agentic-daw.md +3 -0
- data/user_stories/todo/consonance-dissonance-classification.md +57 -0
- data/user_stories/todo/dyad-analysis.md +57 -0
- data/user_stories/todo/material-and-scores.md +10 -0
- data/user_stories/todo/organizing-content.md +72 -0
- data/user_stories/todo/percussion_set.md +1 -0
- data/user_stories/todo/pitch-class-set-analysis.md +79 -0
- data/user_stories/todo/pitch-set-classification.md +72 -0
- data/user_stories/todo/sonority-identification.md +67 -0
- metadata +51 -6
- data/TODO.md +0 -218
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
module HeadMusic::Rudiment; end
|
|
2
|
+
|
|
3
|
+
# Represents a musical tempo with a beat value and beats per minute
|
|
4
|
+
class HeadMusic::Rudiment::Tempo
|
|
5
|
+
SECONDS_PER_MINUTE = 60
|
|
6
|
+
NANOSECONDS_PER_SECOND = 1_000_000_000
|
|
7
|
+
NANOSECONDS_PER_MINUTE = (NANOSECONDS_PER_SECOND * SECONDS_PER_MINUTE).freeze
|
|
8
|
+
|
|
9
|
+
attr_reader :beat_value, :beats_per_minute
|
|
10
|
+
|
|
11
|
+
delegate :ticks, to: :beat_value, prefix: true
|
|
12
|
+
alias_method :ticks_per_beat, :beat_value_ticks
|
|
13
|
+
|
|
14
|
+
NAMED_TEMPO_DEFAULTS = {
|
|
15
|
+
larghissimo: ["quarter", 24], # 24–40 bpm
|
|
16
|
+
adagissimo: ["quarter", 32], # 24–40 bpm
|
|
17
|
+
grave: ["quarter", 32], # 24–40 bpm
|
|
18
|
+
largo: ["quarter", 54], # 40–66 bpm
|
|
19
|
+
larghetto: ["quarter", 54], # 44–66 bpm
|
|
20
|
+
adagio: ["quarter", 60], # 44–66 bpm
|
|
21
|
+
adagietto: ["quarter", 68], # 46–80 bpm
|
|
22
|
+
lento: ["quarter", 72], # 52–108 bpm
|
|
23
|
+
marcia_moderato: ["quarter", 72], # 66–80 bpm
|
|
24
|
+
andante: ["quarter", 78], # 56–108 bpm
|
|
25
|
+
andante_moderato: ["quarter", 88], # 80–108 bpm
|
|
26
|
+
andantino: ["quarter", 92], # 80–108 bpm
|
|
27
|
+
moderato: ["quarter", 108], # 108–120 bpm
|
|
28
|
+
allegretto: ["quarter", 112], # 112–120 bpm
|
|
29
|
+
allegro_moderato: ["quarter", 116], # 116–120 bpm
|
|
30
|
+
allegro: ["quarter", 120], # 120–156 bpm
|
|
31
|
+
molto_allegro: ["quarter", 132], # 124–156 bpm
|
|
32
|
+
allegro_vivace: ["quarter", 132], # 124–156 bpm
|
|
33
|
+
vivace: ["quarter", 156], # 156–176 bpm
|
|
34
|
+
vivacissimo: ["quarter", 172], # 172–176 bpm
|
|
35
|
+
allegrissimo: ["quarter", 172], # 172–176 bpm
|
|
36
|
+
presto: ["quarter", 180], # 168–200 bpm
|
|
37
|
+
prestissimo: ["quarter", 200] # 200 bpm and over
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
def self.get(identifier)
|
|
41
|
+
@tempos ||= {}
|
|
42
|
+
key = HeadMusic::Utilities::HashKey.for(identifier)
|
|
43
|
+
if NAMED_TEMPO_DEFAULTS.key?(identifier.to_s.to_sym)
|
|
44
|
+
beat_value, beats_per_minute = NAMED_TEMPO_DEFAULTS[identifier.to_s.to_sym]
|
|
45
|
+
@tempos[key] ||= new(beat_value, beats_per_minute)
|
|
46
|
+
elsif identifier.to_s.match?(/=|at/)
|
|
47
|
+
parts = identifier.to_s.split(/\s*(=|at)\s*/)
|
|
48
|
+
unit = parts[0]
|
|
49
|
+
bpm = parts[2] || parts[1] # Handle both "q = 120" and "q at 120bpm"
|
|
50
|
+
bpm_value = bpm.to_s.gsub(/[^0-9]/, "").to_i
|
|
51
|
+
@tempos[key] ||= new(standardized_unit(unit), bpm_value)
|
|
52
|
+
else
|
|
53
|
+
@tempos[key] ||= new("quarter", 120)
|
|
54
|
+
end
|
|
55
|
+
@tempos[key]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def initialize(beat_value, beats_per_minute)
|
|
59
|
+
@beat_value = HeadMusic::Rudiment::RhythmicValue.get(beat_value)
|
|
60
|
+
@beats_per_minute = beats_per_minute.to_f
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def beat_duration_in_seconds
|
|
64
|
+
@beat_duration_in_seconds ||=
|
|
65
|
+
SECONDS_PER_MINUTE / beats_per_minute
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def beat_duration_in_nanoseconds
|
|
69
|
+
@beat_duration_in_nanoseconds ||=
|
|
70
|
+
NANOSECONDS_PER_MINUTE / beats_per_minute
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def tick_duration_in_nanoseconds
|
|
74
|
+
@tick_duration_in_nanoseconds ||=
|
|
75
|
+
beat_duration_in_nanoseconds / ticks_per_beat
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.standardized_unit(unit)
|
|
79
|
+
return "quarter" if unit.nil?
|
|
80
|
+
|
|
81
|
+
# Use RhythmicValue parser to handle all formats (shorthand, fractions, British names, dots, etc.)
|
|
82
|
+
rhythmic_value = HeadMusic::Rudiment::RhythmicValue.get(unit)
|
|
83
|
+
rhythmic_value&.unit ? rhythmic_value.to_s : "quarter"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# A module for music rudiments
|
|
2
|
+
module HeadMusic::Rudiment; end
|
|
3
|
+
|
|
4
|
+
# Abstract base class representing a tonal context (system of pitches with a tonal center)
|
|
5
|
+
class HeadMusic::Rudiment::TonalContext < HeadMusic::Rudiment::Base
|
|
6
|
+
attr_reader :tonic_spelling
|
|
7
|
+
|
|
8
|
+
def initialize(tonic_spelling)
|
|
9
|
+
@tonic_spelling = HeadMusic::Rudiment::Spelling.get(tonic_spelling)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def tonic_pitch(octave = 4)
|
|
13
|
+
HeadMusic::Rudiment::Pitch.get("#{tonic_spelling}#{octave}")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def scale
|
|
17
|
+
raise NotImplementedError, "Subclasses must implement #scale"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def pitches(octave = nil)
|
|
21
|
+
scale.pitches(direction: :ascending, octaves: 1)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def pitch_classes
|
|
25
|
+
scale.pitch_classes
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def spellings
|
|
29
|
+
scale.spellings
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def key_signature
|
|
33
|
+
raise NotImplementedError, "Subclasses must implement #key_signature"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# A module for music rudiments
|
|
2
|
+
module HeadMusic::Rudiment; end
|
|
3
|
+
|
|
4
|
+
# Just Intonation tuning system based on whole number frequency ratios
|
|
5
|
+
class HeadMusic::Rudiment::Tuning::JustIntonation < HeadMusic::Rudiment::Tuning
|
|
6
|
+
# Frequency ratios for intervals in just intonation (relative to tonic)
|
|
7
|
+
# Based on the major scale with pure intervals
|
|
8
|
+
INTERVAL_RATIOS = {
|
|
9
|
+
unison: Rational(1, 1),
|
|
10
|
+
minor_second: Rational(16, 15),
|
|
11
|
+
major_second: Rational(9, 8),
|
|
12
|
+
minor_third: Rational(6, 5),
|
|
13
|
+
major_third: Rational(5, 4),
|
|
14
|
+
perfect_fourth: Rational(4, 3),
|
|
15
|
+
tritone: Rational(45, 32),
|
|
16
|
+
perfect_fifth: Rational(3, 2),
|
|
17
|
+
minor_sixth: Rational(8, 5),
|
|
18
|
+
major_sixth: Rational(5, 3),
|
|
19
|
+
minor_seventh: Rational(16, 9),
|
|
20
|
+
major_seventh: Rational(15, 8),
|
|
21
|
+
octave: Rational(2, 1)
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
attr_reader :tonal_center
|
|
25
|
+
|
|
26
|
+
def initialize(reference_pitch: :a440, tonal_center: nil)
|
|
27
|
+
super
|
|
28
|
+
@tonal_center = HeadMusic::Rudiment::Pitch.get(tonal_center || "C4")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def frequency_for(pitch)
|
|
32
|
+
pitch = HeadMusic::Rudiment::Pitch.get(pitch)
|
|
33
|
+
|
|
34
|
+
# Calculate the frequency of the tonal center using equal temperament from reference pitch
|
|
35
|
+
tonal_center_frequency = calculate_tonal_center_frequency
|
|
36
|
+
|
|
37
|
+
# Calculate the interval from the tonal center to the requested pitch
|
|
38
|
+
interval_from_tonal_center = (pitch - tonal_center).semitones
|
|
39
|
+
|
|
40
|
+
# Get the just intonation ratio for this interval
|
|
41
|
+
ratio = ratio_for_interval(interval_from_tonal_center)
|
|
42
|
+
|
|
43
|
+
# Calculate the frequency
|
|
44
|
+
tonal_center_frequency * ratio
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def calculate_tonal_center_frequency
|
|
50
|
+
# Use equal temperament to get the tonal center frequency from the reference pitch
|
|
51
|
+
interval_to_tonal_center = (tonal_center - reference_pitch.pitch).semitones
|
|
52
|
+
reference_pitch_frequency * (2**(interval_to_tonal_center / 12.0))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def ratio_for_interval(semitones)
|
|
56
|
+
# Handle octaves
|
|
57
|
+
octaves = semitones / 12
|
|
58
|
+
interval_within_octave = semitones % 12
|
|
59
|
+
|
|
60
|
+
# Make sure we handle negative intervals
|
|
61
|
+
if interval_within_octave < 0
|
|
62
|
+
interval_within_octave += 12
|
|
63
|
+
octaves -= 1
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get the base ratio
|
|
67
|
+
base_ratio = case interval_within_octave
|
|
68
|
+
when 0 then INTERVAL_RATIOS[:unison]
|
|
69
|
+
when 1 then INTERVAL_RATIOS[:minor_second]
|
|
70
|
+
when 2 then INTERVAL_RATIOS[:major_second]
|
|
71
|
+
when 3 then INTERVAL_RATIOS[:minor_third]
|
|
72
|
+
when 4 then INTERVAL_RATIOS[:major_third]
|
|
73
|
+
when 5 then INTERVAL_RATIOS[:perfect_fourth]
|
|
74
|
+
when 6 then INTERVAL_RATIOS[:tritone]
|
|
75
|
+
when 7 then INTERVAL_RATIOS[:perfect_fifth]
|
|
76
|
+
when 8 then INTERVAL_RATIOS[:minor_sixth]
|
|
77
|
+
when 9 then INTERVAL_RATIOS[:major_sixth]
|
|
78
|
+
when 10 then INTERVAL_RATIOS[:minor_seventh]
|
|
79
|
+
when 11 then INTERVAL_RATIOS[:major_seventh]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Apply octave adjustments
|
|
83
|
+
base_ratio * (2**octaves)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# A module for music rudiments
|
|
2
|
+
module HeadMusic::Rudiment; end
|
|
3
|
+
|
|
4
|
+
# Quarter-comma meantone temperament
|
|
5
|
+
# Optimizes major thirds to be pure (5:4) at the expense of perfect fifths
|
|
6
|
+
class HeadMusic::Rudiment::Tuning::Meantone < HeadMusic::Rudiment::Tuning
|
|
7
|
+
# Frequency ratios for intervals in quarter-comma meantone temperament
|
|
8
|
+
# The defining characteristic is that major thirds are pure (5:4)
|
|
9
|
+
# and the syntonic comma is distributed equally among the four fifths
|
|
10
|
+
INTERVAL_RATIOS = {
|
|
11
|
+
unison: Rational(1, 1),
|
|
12
|
+
minor_second: 5.0**(1.0 / 4) / 2.0**(1.0 / 2), # ~1.0697
|
|
13
|
+
major_second: 5.0**(1.0 / 4), # ~1.1892 (fourth root of 5)
|
|
14
|
+
minor_third: 5.0**(1.0 / 2) / 2.0**(1.0 / 2), # ~1.5811
|
|
15
|
+
major_third: Rational(5, 4), # Pure major third (1.25)
|
|
16
|
+
perfect_fourth: 2.0**(1.0 / 2) / 5.0**(1.0 / 4), # ~1.3375
|
|
17
|
+
tritone: 5.0**(3.0 / 4) / 2.0**(1.0 / 2), # ~1.6719
|
|
18
|
+
perfect_fifth: Rational(3, 2), # ~1.4953 (slightly flat)
|
|
19
|
+
minor_sixth: 2.0**(3.0 / 2) / 5.0**(1.0 / 4), # ~1.6818
|
|
20
|
+
major_sixth: 5.0**(3.0 / 4), # ~1.8877
|
|
21
|
+
minor_seventh: 2.0**(3.0 / 2) / 5.0**(1.0 / 2), # ~1.8877
|
|
22
|
+
major_seventh: Rational(25, 16), # ~1.5625
|
|
23
|
+
octave: Rational(2, 1) # Octave (2.0)
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
attr_reader :tonal_center
|
|
27
|
+
|
|
28
|
+
def initialize(reference_pitch: :a440, tonal_center: nil)
|
|
29
|
+
super
|
|
30
|
+
@tonal_center = HeadMusic::Rudiment::Pitch.get(tonal_center || "C4")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def frequency_for(pitch)
|
|
34
|
+
pitch = HeadMusic::Rudiment::Pitch.get(pitch)
|
|
35
|
+
|
|
36
|
+
# Calculate the frequency of the tonal center using equal temperament from reference pitch
|
|
37
|
+
tonal_center_frequency = calculate_tonal_center_frequency
|
|
38
|
+
|
|
39
|
+
# Calculate the interval from the tonal center to the requested pitch
|
|
40
|
+
interval_from_tonal_center = (pitch - tonal_center).semitones
|
|
41
|
+
|
|
42
|
+
# Get the meantone ratio for this interval
|
|
43
|
+
ratio = ratio_for_interval(interval_from_tonal_center)
|
|
44
|
+
|
|
45
|
+
# Calculate the frequency
|
|
46
|
+
tonal_center_frequency * ratio
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def calculate_tonal_center_frequency
|
|
52
|
+
# Use equal temperament to get the tonal center frequency from the reference pitch
|
|
53
|
+
interval_to_tonal_center = (tonal_center - reference_pitch.pitch).semitones
|
|
54
|
+
reference_pitch_frequency * (2**(interval_to_tonal_center / 12.0))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def ratio_for_interval(semitones)
|
|
58
|
+
# Handle octaves
|
|
59
|
+
octaves = semitones / 12
|
|
60
|
+
interval_within_octave = semitones % 12
|
|
61
|
+
|
|
62
|
+
# Make sure we handle negative intervals
|
|
63
|
+
if interval_within_octave < 0
|
|
64
|
+
interval_within_octave += 12
|
|
65
|
+
octaves -= 1
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Get the base ratio
|
|
69
|
+
base_ratio = case interval_within_octave
|
|
70
|
+
when 0 then INTERVAL_RATIOS[:unison]
|
|
71
|
+
when 1 then INTERVAL_RATIOS[:minor_second]
|
|
72
|
+
when 2 then INTERVAL_RATIOS[:major_second]
|
|
73
|
+
when 3 then INTERVAL_RATIOS[:minor_third]
|
|
74
|
+
when 4 then INTERVAL_RATIOS[:major_third]
|
|
75
|
+
when 5 then INTERVAL_RATIOS[:perfect_fourth]
|
|
76
|
+
when 6 then INTERVAL_RATIOS[:tritone]
|
|
77
|
+
when 7 then INTERVAL_RATIOS[:perfect_fifth]
|
|
78
|
+
when 8 then INTERVAL_RATIOS[:minor_sixth]
|
|
79
|
+
when 9 then INTERVAL_RATIOS[:major_sixth]
|
|
80
|
+
when 10 then INTERVAL_RATIOS[:minor_seventh]
|
|
81
|
+
when 11 then INTERVAL_RATIOS[:major_seventh]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Apply octave adjustments
|
|
85
|
+
base_ratio * (2**octaves)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# A module for music rudiments
|
|
2
|
+
module HeadMusic::Rudiment; end
|
|
3
|
+
|
|
4
|
+
# Pythagorean tuning system based on stacking perfect fifths (3:2 ratio)
|
|
5
|
+
class HeadMusic::Rudiment::Tuning::Pythagorean < HeadMusic::Rudiment::Tuning
|
|
6
|
+
# Frequency ratios for intervals in Pythagorean tuning (relative to tonic)
|
|
7
|
+
# Generated by stacking perfect fifths and reducing to within one octave
|
|
8
|
+
INTERVAL_RATIOS = {
|
|
9
|
+
unison: Rational(1, 1),
|
|
10
|
+
minor_second: Rational(256, 243), # Pythagorean minor second
|
|
11
|
+
major_second: Rational(9, 8), # Pythagorean major second
|
|
12
|
+
minor_third: Rational(32, 27), # Pythagorean minor third
|
|
13
|
+
major_third: Rational(81, 64), # Pythagorean major third (ditone)
|
|
14
|
+
perfect_fourth: Rational(4, 3), # Perfect fourth
|
|
15
|
+
tritone: Rational(729, 512), # Pythagorean tritone (augmented fourth)
|
|
16
|
+
perfect_fifth: Rational(3, 2), # Perfect fifth
|
|
17
|
+
minor_sixth: Rational(128, 81), # Pythagorean minor sixth
|
|
18
|
+
major_sixth: Rational(27, 16), # Pythagorean major sixth
|
|
19
|
+
minor_seventh: Rational(16, 9), # Pythagorean minor seventh
|
|
20
|
+
major_seventh: Rational(243, 128), # Pythagorean major seventh
|
|
21
|
+
octave: Rational(2, 1) # Octave
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
# Additional chromatic intervals for enharmonic equivalents
|
|
25
|
+
CHROMATIC_RATIOS = {
|
|
26
|
+
augmented_unison: Rational(2187, 2048), # Pythagorean augmented unison (sharp)
|
|
27
|
+
diminished_second: Rational(256, 243) # Same as minor second in Pythagorean
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
attr_reader :tonal_center
|
|
31
|
+
|
|
32
|
+
def initialize(reference_pitch: :a440, tonal_center: nil)
|
|
33
|
+
super
|
|
34
|
+
@tonal_center = HeadMusic::Rudiment::Pitch.get(tonal_center || "C4")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def frequency_for(pitch)
|
|
38
|
+
pitch = HeadMusic::Rudiment::Pitch.get(pitch)
|
|
39
|
+
|
|
40
|
+
# Calculate the frequency of the tonal center using equal temperament from reference pitch
|
|
41
|
+
tonal_center_frequency = calculate_tonal_center_frequency
|
|
42
|
+
|
|
43
|
+
# Calculate the interval from the tonal center to the requested pitch
|
|
44
|
+
interval_from_tonal_center = (pitch - tonal_center).semitones
|
|
45
|
+
|
|
46
|
+
# Get the Pythagorean ratio for this interval
|
|
47
|
+
ratio = ratio_for_interval(interval_from_tonal_center)
|
|
48
|
+
|
|
49
|
+
# Calculate the frequency
|
|
50
|
+
tonal_center_frequency * ratio
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def calculate_tonal_center_frequency
|
|
56
|
+
# Use equal temperament to get the tonal center frequency from the reference pitch
|
|
57
|
+
interval_to_tonal_center = (tonal_center - reference_pitch.pitch).semitones
|
|
58
|
+
reference_pitch_frequency * (2**(interval_to_tonal_center / 12.0))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def ratio_for_interval(semitones)
|
|
62
|
+
# Handle octaves
|
|
63
|
+
octaves = semitones / 12
|
|
64
|
+
interval_within_octave = semitones % 12
|
|
65
|
+
|
|
66
|
+
# Make sure we handle negative intervals
|
|
67
|
+
if interval_within_octave < 0
|
|
68
|
+
interval_within_octave += 12
|
|
69
|
+
octaves -= 1
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Get the base ratio
|
|
73
|
+
base_ratio = case interval_within_octave
|
|
74
|
+
when 0 then INTERVAL_RATIOS[:unison]
|
|
75
|
+
when 1 then INTERVAL_RATIOS[:minor_second]
|
|
76
|
+
when 2 then INTERVAL_RATIOS[:major_second]
|
|
77
|
+
when 3 then INTERVAL_RATIOS[:minor_third]
|
|
78
|
+
when 4 then INTERVAL_RATIOS[:major_third]
|
|
79
|
+
when 5 then INTERVAL_RATIOS[:perfect_fourth]
|
|
80
|
+
when 6 then INTERVAL_RATIOS[:tritone]
|
|
81
|
+
when 7 then INTERVAL_RATIOS[:perfect_fifth]
|
|
82
|
+
when 8 then INTERVAL_RATIOS[:minor_sixth]
|
|
83
|
+
when 9 then INTERVAL_RATIOS[:major_sixth]
|
|
84
|
+
when 10 then INTERVAL_RATIOS[:minor_seventh]
|
|
85
|
+
when 11 then INTERVAL_RATIOS[:major_seventh]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Apply octave adjustments
|
|
89
|
+
base_ratio * (2**octaves)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -3,13 +3,29 @@ module HeadMusic::Rudiment; end
|
|
|
3
3
|
|
|
4
4
|
# A tuning has a reference pitch and frequency and provides frequencies for all pitches
|
|
5
5
|
# The base class assumes equal temperament tuning. By default, A4 = 440.0 Hz
|
|
6
|
-
class HeadMusic::Rudiment::Tuning
|
|
6
|
+
class HeadMusic::Rudiment::Tuning < HeadMusic::Rudiment::Base
|
|
7
7
|
attr_accessor :reference_pitch
|
|
8
8
|
|
|
9
9
|
delegate :pitch, :frequency, to: :reference_pitch, prefix: true
|
|
10
10
|
|
|
11
|
-
def
|
|
11
|
+
def self.get(tuning_type = :equal_temperament, **options)
|
|
12
|
+
case tuning_type.to_s.downcase
|
|
13
|
+
when "just_intonation", "just", "ji"
|
|
14
|
+
HeadMusic::Rudiment::Tuning::JustIntonation.new(**options)
|
|
15
|
+
when "pythagorean", "pythag"
|
|
16
|
+
HeadMusic::Rudiment::Tuning::Pythagorean.new(**options)
|
|
17
|
+
when "meantone", "quarter_comma_meantone", "1/4_comma"
|
|
18
|
+
HeadMusic::Rudiment::Tuning::Meantone.new(**options)
|
|
19
|
+
when "equal_temperament", "equal", "et", "12tet"
|
|
20
|
+
new(**options)
|
|
21
|
+
else
|
|
22
|
+
new(**options)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(reference_pitch: :a440, tonal_center: nil)
|
|
12
27
|
@reference_pitch = HeadMusic::Rudiment::ReferencePitch.get(reference_pitch)
|
|
28
|
+
@tonal_center = tonal_center
|
|
13
29
|
end
|
|
14
30
|
|
|
15
31
|
def frequency_for(pitch)
|
|
@@ -17,5 +33,3 @@ class HeadMusic::Rudiment::Tuning
|
|
|
17
33
|
reference_pitch_frequency * (2**(1.0 / 12))**(pitch - reference_pitch.pitch).semitones
|
|
18
34
|
end
|
|
19
35
|
end
|
|
20
|
-
|
|
21
|
-
# TODO: other tunings
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# A module for music rudiments
|
|
2
|
+
module HeadMusic::Rudiment; end
|
|
3
|
+
|
|
4
|
+
# An UnpitchedNote represents a percussion note with rhythm but no specific pitch.
|
|
5
|
+
# It inherits from RhythmicElement and can optionally have an instrument name.
|
|
6
|
+
class HeadMusic::Rudiment::UnpitchedNote < HeadMusic::Rudiment::RhythmicElement
|
|
7
|
+
include HeadMusic::Named
|
|
8
|
+
|
|
9
|
+
attr_reader :instrument_name
|
|
10
|
+
|
|
11
|
+
# Make new public for this concrete class
|
|
12
|
+
public_class_method :new
|
|
13
|
+
|
|
14
|
+
def self.get(rhythmic_value, instrument: nil)
|
|
15
|
+
return rhythmic_value if rhythmic_value.is_a?(HeadMusic::Rudiment::UnpitchedNote)
|
|
16
|
+
|
|
17
|
+
rhythmic_value = HeadMusic::Rudiment::RhythmicValue.get(rhythmic_value)
|
|
18
|
+
return nil unless rhythmic_value
|
|
19
|
+
|
|
20
|
+
fetch_or_create(rhythmic_value, instrument)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.fetch_or_create(rhythmic_value, instrument_name)
|
|
24
|
+
@unpitched_notes ||= {}
|
|
25
|
+
hash_key = [rhythmic_value.to_s, instrument_name].compact.join("_")
|
|
26
|
+
@unpitched_notes[hash_key] ||= new(rhythmic_value, instrument_name)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(rhythmic_value, instrument_name = nil)
|
|
30
|
+
super(rhythmic_value)
|
|
31
|
+
@instrument_name = instrument_name
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def name
|
|
35
|
+
if instrument_name
|
|
36
|
+
"#{rhythmic_value} #{instrument_name}"
|
|
37
|
+
else
|
|
38
|
+
"#{rhythmic_value} unpitched note"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Override with_rhythmic_value to preserve instrument
|
|
43
|
+
def with_rhythmic_value(new_rhythmic_value)
|
|
44
|
+
self.class.get(new_rhythmic_value, instrument: instrument_name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Create a new unpitched note with a different instrument
|
|
48
|
+
def with_instrument(new_instrument_name)
|
|
49
|
+
self.class.get(rhythmic_value, instrument: new_instrument_name)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def ==(other)
|
|
53
|
+
return false unless other.is_a?(self.class)
|
|
54
|
+
rhythmic_value == other.rhythmic_value && instrument_name == other.instrument_name
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def sounded?
|
|
58
|
+
true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private_class_method :fetch_or_create
|
|
62
|
+
end
|
|
@@ -51,16 +51,16 @@ class HeadMusic::Style::Annotation
|
|
|
51
51
|
self.class::MESSAGE
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
protected
|
|
55
|
-
|
|
56
54
|
def first_note
|
|
57
|
-
notes.first
|
|
55
|
+
notes.first
|
|
58
56
|
end
|
|
59
57
|
|
|
60
58
|
def last_note
|
|
61
|
-
notes.last
|
|
59
|
+
notes.last
|
|
62
60
|
end
|
|
63
61
|
|
|
62
|
+
protected
|
|
63
|
+
|
|
64
64
|
def voices
|
|
65
65
|
@voices ||= voice.composition.voices
|
|
66
66
|
end
|
|
@@ -9,6 +9,22 @@ class HeadMusic::Style::Guidelines::NotesSameLength < HeadMusic::Style::Annotati
|
|
|
9
9
|
HeadMusic::Style::Mark.for_each(all_wrong_length_notes)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
def first_most_common_rhythmic_value
|
|
13
|
+
@first_most_common_rhythmic_value ||= begin
|
|
14
|
+
candidates = most_common_rhythmic_values
|
|
15
|
+
first_match = notes.detect { |note| candidates.include?(note.rhythmic_value) }
|
|
16
|
+
first_match&.rhythmic_value
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def most_common_rhythmic_values
|
|
21
|
+
return [] if notes.empty?
|
|
22
|
+
|
|
23
|
+
occurrences = occurrences_by_rhythmic_value
|
|
24
|
+
highest_count = occurrences.values.max
|
|
25
|
+
occurrences.select { |_rhythmic_value, count| count == highest_count }.keys
|
|
26
|
+
end
|
|
27
|
+
|
|
12
28
|
private
|
|
13
29
|
|
|
14
30
|
def all_wrong_length_notes
|
|
@@ -37,22 +53,6 @@ class HeadMusic::Style::Guidelines::NotesSameLength < HeadMusic::Style::Annotati
|
|
|
37
53
|
notes[0..-2]
|
|
38
54
|
end
|
|
39
55
|
|
|
40
|
-
def first_most_common_rhythmic_value
|
|
41
|
-
@first_most_common_rhythmic_value ||= begin
|
|
42
|
-
candidates = most_common_rhythmic_values
|
|
43
|
-
first_match = notes.detect { |note| candidates.include?(note.rhythmic_value) }
|
|
44
|
-
first_match&.rhythmic_value
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def most_common_rhythmic_values
|
|
49
|
-
return [] if notes.empty?
|
|
50
|
-
|
|
51
|
-
occurrences = occurrences_by_rhythmic_value
|
|
52
|
-
highest_count = occurrences.values.max
|
|
53
|
-
occurrences.select { |_rhythmic_value, count| count == highest_count }.keys
|
|
54
|
-
end
|
|
55
|
-
|
|
56
56
|
def occurrences_by_rhythmic_value
|
|
57
57
|
rhythmic_values.each_with_object(Hash.new(0)) { |value, hash| hash[value] += 1 }
|
|
58
58
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Medieval tradition for interval consonance classification
|
|
2
|
+
class HeadMusic::Style::MedievalTradition < HeadMusic::Style::Tradition
|
|
3
|
+
def consonance_classification(interval)
|
|
4
|
+
interval_mod = interval.simple_semitones
|
|
5
|
+
|
|
6
|
+
# Check for augmented or diminished intervals
|
|
7
|
+
if interval.augmented? || interval.diminished?
|
|
8
|
+
return HeadMusic::Rudiment::Consonance::DISSONANCE
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
case interval_mod
|
|
12
|
+
when 0, 12 # Unison, Octave
|
|
13
|
+
HeadMusic::Rudiment::Consonance::PERFECT_CONSONANCE
|
|
14
|
+
when 7 # Perfect Fifth
|
|
15
|
+
HeadMusic::Rudiment::Consonance::PERFECT_CONSONANCE
|
|
16
|
+
when 5 # Perfect Fourth - consonant in medieval music
|
|
17
|
+
HeadMusic::Rudiment::Consonance::PERFECT_CONSONANCE
|
|
18
|
+
when 3, 4 # Minor Third, Major Third
|
|
19
|
+
HeadMusic::Rudiment::Consonance::IMPERFECT_CONSONANCE
|
|
20
|
+
when 8, 9 # Minor Sixth, Major Sixth
|
|
21
|
+
HeadMusic::Rudiment::Consonance::IMPERFECT_CONSONANCE
|
|
22
|
+
else
|
|
23
|
+
HeadMusic::Rudiment::Consonance::DISSONANCE
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Modern/standard practice tradition for interval consonance classification
|
|
2
|
+
class HeadMusic::Style::ModernTradition < HeadMusic::Style::Tradition
|
|
3
|
+
def consonance_classification(interval)
|
|
4
|
+
interval_mod = interval.simple_semitones
|
|
5
|
+
|
|
6
|
+
# Check for augmented or diminished intervals (except diminished fifth/augmented fourth)
|
|
7
|
+
if (interval.augmented? || interval.diminished?) && interval_mod != 6
|
|
8
|
+
return HeadMusic::Rudiment::Consonance::DISSONANCE
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
case interval_mod
|
|
12
|
+
when 0, 12 # Unison, Octave
|
|
13
|
+
HeadMusic::Rudiment::Consonance::PERFECT_CONSONANCE
|
|
14
|
+
when 7 # Perfect Fifth
|
|
15
|
+
HeadMusic::Rudiment::Consonance::PERFECT_CONSONANCE
|
|
16
|
+
when 3, 4 # Minor Third, Major Third
|
|
17
|
+
HeadMusic::Rudiment::Consonance::IMPERFECT_CONSONANCE
|
|
18
|
+
when 8, 9 # Minor Sixth, Major Sixth
|
|
19
|
+
HeadMusic::Rudiment::Consonance::IMPERFECT_CONSONANCE
|
|
20
|
+
when 5 # Perfect Fourth
|
|
21
|
+
# In standard practice, perfect fourth is considered consonant
|
|
22
|
+
# but contextual would be more accurate
|
|
23
|
+
HeadMusic::Rudiment::Consonance::PERFECT_CONSONANCE
|
|
24
|
+
when 2, 10 # Major Second, Minor Seventh
|
|
25
|
+
HeadMusic::Rudiment::Consonance::MILD_DISSONANCE
|
|
26
|
+
when 1, 11 # Minor Second, Major Seventh
|
|
27
|
+
HeadMusic::Rudiment::Consonance::HARSH_DISSONANCE
|
|
28
|
+
when 6 # Tritone (Aug 4th/Dim 5th)
|
|
29
|
+
HeadMusic::Rudiment::Consonance::DISSONANCE
|
|
30
|
+
else
|
|
31
|
+
HeadMusic::Rudiment::Consonance::DISSONANCE
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Renaissance counterpoint tradition for interval consonance classification
|
|
2
|
+
class HeadMusic::Style::RenaissanceTradition < HeadMusic::Style::Tradition
|
|
3
|
+
def consonance_classification(interval)
|
|
4
|
+
interval_mod = interval.simple_semitones
|
|
5
|
+
|
|
6
|
+
# Check for augmented or diminished intervals
|
|
7
|
+
if interval.augmented? || interval.diminished?
|
|
8
|
+
return HeadMusic::Rudiment::Consonance::DISSONANCE
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
case interval_mod
|
|
12
|
+
when 0, 12 # Unison, Octave
|
|
13
|
+
HeadMusic::Rudiment::Consonance::PERFECT_CONSONANCE
|
|
14
|
+
when 7 # Perfect Fifth
|
|
15
|
+
HeadMusic::Rudiment::Consonance::PERFECT_CONSONANCE
|
|
16
|
+
when 3, 4 # Minor Third, Major Third
|
|
17
|
+
HeadMusic::Rudiment::Consonance::IMPERFECT_CONSONANCE
|
|
18
|
+
when 8, 9 # Minor Sixth, Major Sixth
|
|
19
|
+
HeadMusic::Rudiment::Consonance::IMPERFECT_CONSONANCE
|
|
20
|
+
when 5 # Perfect Fourth - dissonant in Renaissance counterpoint
|
|
21
|
+
HeadMusic::Rudiment::Consonance::DISSONANCE
|
|
22
|
+
else
|
|
23
|
+
HeadMusic::Rudiment::Consonance::DISSONANCE
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# A style tradition represents a historical or theoretical approach to music
|
|
2
|
+
module HeadMusic::Style
|
|
3
|
+
class Tradition
|
|
4
|
+
def self.get(name)
|
|
5
|
+
case name&.to_sym
|
|
6
|
+
when :modern, :standard_practice then ModernTradition.new
|
|
7
|
+
when :renaissance_counterpoint, :two_part_harmony then RenaissanceTradition.new
|
|
8
|
+
when :medieval then MedievalTradition.new
|
|
9
|
+
else ModernTradition.new
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def consonance_classification(interval)
|
|
14
|
+
raise NotImplementedError, "#{self.class} must implement consonance_classification"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def name
|
|
18
|
+
self.class.name.split("::").last.sub(/Tradition$/, "").downcase.gsub(" ", "_").to_sym
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|