head_music 8.3.0 → 11.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/.github/workflows/ci.yml +9 -3
- data/CHANGELOG.md +71 -0
- data/CLAUDE.md +62 -25
- data/Gemfile +7 -1
- data/Gemfile.lock +91 -3
- data/MUSIC_THEORY.md +120 -0
- data/README.md +18 -0
- data/Rakefile +7 -2
- data/head_music.gemspec +1 -1
- data/lib/head_music/analysis/diatonic_interval.rb +29 -27
- data/lib/head_music/analysis/dyad.rb +229 -0
- data/lib/head_music/analysis/interval_consonance.rb +51 -0
- data/lib/head_music/analysis/melodic_interval.rb +1 -1
- data/lib/head_music/analysis/pitch_class_set.rb +111 -14
- data/lib/head_music/analysis/{pitch_set.rb → pitch_collection.rb} +11 -5
- data/lib/head_music/analysis/sonority.rb +50 -12
- 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/voice.rb +1 -1
- data/lib/head_music/instruments/alternate_tuning.rb +102 -0
- data/lib/head_music/instruments/alternate_tunings.yml +78 -0
- data/lib/head_music/instruments/instrument.rb +231 -72
- data/lib/head_music/instruments/instrument_configuration.rb +66 -0
- data/lib/head_music/instruments/instrument_configuration_option.rb +38 -0
- data/lib/head_music/instruments/instrument_configurations.yml +288 -0
- data/lib/head_music/instruments/instrument_families.yml +77 -0
- data/lib/head_music/instruments/instrument_family.rb +15 -5
- data/lib/head_music/instruments/instruments.yml +795 -965
- data/lib/head_music/instruments/playing_technique.rb +75 -0
- data/lib/head_music/instruments/playing_techniques.yml +826 -0
- data/lib/head_music/instruments/score_order.rb +136 -0
- data/lib/head_music/instruments/score_orders.yml +130 -0
- data/lib/head_music/instruments/staff.rb +61 -1
- data/lib/head_music/instruments/staff_scheme.rb +6 -4
- data/lib/head_music/instruments/stringing.rb +115 -0
- data/lib/head_music/instruments/stringing_course.rb +58 -0
- data/lib/head_music/instruments/stringings.yml +168 -0
- data/lib/head_music/instruments/variant.rb +6 -1
- data/lib/head_music/locales/de.yml +29 -0
- data/lib/head_music/locales/en.yml +106 -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/{rudiment → notation}/musical_symbol.rb +3 -3
- data/lib/head_music/notation/staff_mapping.rb +70 -0
- data/lib/head_music/notation/staff_position.rb +62 -0
- data/lib/head_music/notation.rb +7 -0
- data/lib/head_music/rudiment/alteration.rb +34 -49
- data/lib/head_music/rudiment/alterations.yml +32 -0
- data/lib/head_music/rudiment/base.rb +9 -0
- data/lib/head_music/rudiment/chromatic_interval.rb +4 -7
- data/lib/head_music/rudiment/clef.rb +2 -2
- data/lib/head_music/rudiment/consonance.rb +39 -5
- 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 +21 -8
- 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/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 +13 -5
- 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 +1 -1
- 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 +8 -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 +0 -39
- data/lib/head_music/rudiment/tuning/meantone.rb +0 -39
- data/lib/head_music/rudiment/tuning/pythagorean.rb +0 -39
- data/lib/head_music/rudiment/tuning.rb +21 -1
- data/lib/head_music/rudiment/unpitched_note.rb +62 -0
- data/lib/head_music/style/guidelines/consonant_climax.rb +2 -2
- data/lib/head_music/style/medieval_tradition.rb +26 -0
- data/lib/head_music/style/modern_tradition.rb +31 -0
- data/lib/head_music/style/renaissance_tradition.rb +26 -0
- data/lib/head_music/style/tradition.rb +21 -0
- data/lib/head_music/time/clock_position.rb +84 -0
- data/lib/head_music/time/conductor.rb +264 -0
- data/lib/head_music/time/meter_event.rb +37 -0
- data/lib/head_music/time/meter_map.rb +173 -0
- data/lib/head_music/time/musical_position.rb +188 -0
- data/lib/head_music/time/smpte_timecode.rb +164 -0
- data/lib/head_music/time/tempo_event.rb +40 -0
- data/lib/head_music/time/tempo_map.rb +187 -0
- data/lib/head_music/time.rb +32 -0
- data/lib/head_music/utilities/case.rb +27 -0
- data/lib/head_music/utilities/hash_key.rb +34 -2
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +71 -22
- data/user_stories/active/string-pitches.md +41 -0
- data/user_stories/backlog/notation-style.md +183 -0
- data/user_stories/backlog/organizing-content.md +80 -0
- data/user_stories/done/consonance-dissonance-classification.md +117 -0
- data/user_stories/{backlog → done}/dyad-analysis.md +6 -16
- data/user_stories/done/epic--score-order/PLAN.md +244 -0
- data/user_stories/done/expand-playing-techniques.md +38 -0
- data/user_stories/done/handle-time.md +7 -0
- data/user_stories/done/handle-time.rb +163 -0
- data/user_stories/done/instrument-architecture.md +238 -0
- data/user_stories/done/instrument-variant.md +65 -0
- data/user_stories/done/move-musical-symbol-to-notation.md +161 -0
- data/user_stories/done/move-staff-mapping-to-notation.md +158 -0
- data/user_stories/done/move-staff-position-to-notation.md +141 -0
- data/user_stories/done/notation-module-foundation.md +102 -0
- data/user_stories/done/percussion_set.md +260 -0
- data/user_stories/done/sonority-identification.md +37 -0
- data/user_stories/done/superclass-for-note.md +30 -0
- data/user_stories/epics/notation-module.md +135 -0
- data/user_stories/visioning/agentic-daw.md +2 -0
- metadata +84 -18
- data/TODO.md +0 -109
- data/check_instrument_consistency.rb +0 -0
- data/test_translations.rb +0 -15
- data/user_stories/backlog/consonance-dissonance-classification.md +0 -57
- data/user_stories/backlog/pitch-set-classification.md +0 -62
- data/user_stories/backlog/sonority-identification.md +0 -47
- /data/user_stories/{backlog → done/epic--score-order}/band-score-order.md +0 -0
- /data/user_stories/{backlog → done/epic--score-order}/chamber-ensemble-score-order.md +0 -0
- /data/user_stories/{backlog → done/epic--score-order}/orchestral-score-order.md +0 -0
- /data/user_stories/{backlog → done}/pitch-class-set-analysis.md +0 -0
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
module HeadMusic::Rudiment; end
|
|
3
3
|
|
|
4
4
|
# A scale contains ordered pitches starting at a tonal center.
|
|
5
|
-
class HeadMusic::Rudiment::Scale
|
|
5
|
+
class HeadMusic::Rudiment::Scale < HeadMusic::Rudiment::Base
|
|
6
6
|
SCALE_REGEX = /^[A-G][#b]?\s+\w+$/
|
|
7
7
|
|
|
8
8
|
def self.get(root_pitch, scale_type = nil)
|
|
9
|
-
root_pitch, scale_type = root_pitch.split(/\s+/) if root_pitch.is_a?(String) && scale_type =~ SCALE_REGEX
|
|
10
9
|
root_pitch = HeadMusic::Rudiment::Pitch.get(root_pitch)
|
|
11
|
-
scale_type = HeadMusic::Rudiment::ScaleType.get(scale_type ||
|
|
10
|
+
scale_type = HeadMusic::Rudiment::ScaleType.get(scale_type || HeadMusic::Rudiment::ScaleType::DEFAULT)
|
|
12
11
|
@scales ||= {}
|
|
13
12
|
hash_key = HeadMusic::Utilities::HashKey.for(
|
|
14
13
|
[root_pitch, scale_type].join(" ").gsub(/#|♯/, "sharp").gsub(/(\w)[b♭]/, '\\1flat')
|
|
@@ -36,7 +35,7 @@ class HeadMusic::Rudiment::Scale
|
|
|
36
35
|
end
|
|
37
36
|
|
|
38
37
|
def spellings(direction: :ascending, octaves: 1)
|
|
39
|
-
pitches(direction: direction, octaves: octaves).map(&:spelling)
|
|
38
|
+
pitches(direction: direction, octaves: octaves).map(&:spelling)
|
|
40
39
|
end
|
|
41
40
|
|
|
42
41
|
def pitch_names(direction: :ascending, octaves: 1)
|
|
@@ -83,7 +82,7 @@ class HeadMusic::Rudiment::Scale
|
|
|
83
82
|
end
|
|
84
83
|
|
|
85
84
|
def parent_scale_pitches
|
|
86
|
-
HeadMusic::Rudiment::Scale.get(root_pitch, scale_type.parent_name).pitches
|
|
85
|
+
HeadMusic::Rudiment::Scale.get(root_pitch, scale_type.parent_name).pitches
|
|
87
86
|
end
|
|
88
87
|
|
|
89
88
|
def parent_scale_pitch_for(semitones_from_root)
|
|
@@ -3,7 +3,7 @@ module HeadMusic::Rudiment; end
|
|
|
3
3
|
|
|
4
4
|
# A scale degree is a number indicating the ordinality of the spelling in the key.
|
|
5
5
|
# TODO: Rewrite to accept a tonal_center and a scale type.
|
|
6
|
-
class HeadMusic::Rudiment::ScaleDegree
|
|
6
|
+
class HeadMusic::Rudiment::ScaleDegree < HeadMusic::Rudiment::Base
|
|
7
7
|
include Comparable
|
|
8
8
|
|
|
9
9
|
NAME_FOR_DIATONIC_DEGREE = [nil, "tonic", "supertonic", "mediant", "subdominant", "dominant", "submediant"].freeze
|
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
module HeadMusic::Rudiment; end
|
|
3
3
|
|
|
4
4
|
# A ScaleType represents a particular scale pattern, such as major, lydian, or minor pentatonic.
|
|
5
|
-
class HeadMusic::Rudiment::ScaleType
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
class HeadMusic::Rudiment::ScaleType < HeadMusic::Rudiment::Base
|
|
6
|
+
# TODO: load scale types from yaml configuration file
|
|
7
|
+
# Include a system of aliasing, e.g. :natural_minor, :aeolian
|
|
8
|
+
# Include ascending and descending intervals for scales that differ
|
|
9
|
+
|
|
10
|
+
H = 1 # half step
|
|
11
|
+
W = 2 # whole step
|
|
8
12
|
|
|
9
13
|
# Modal
|
|
10
14
|
I = [W, W, H, W, W, W, H].freeze
|
|
@@ -33,6 +37,8 @@ class HeadMusic::Rudiment::ScaleType
|
|
|
33
37
|
|
|
34
38
|
MINOR_PENTATONIC = [3, 2, 2, 3, 2].freeze
|
|
35
39
|
|
|
40
|
+
DEFAULT = :major
|
|
41
|
+
|
|
36
42
|
def self._modes
|
|
37
43
|
{}.tap do |modes|
|
|
38
44
|
MODE_NAMES.each do |roman_numeral, aliases|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
module HeadMusic::Rudiment; end
|
|
3
3
|
|
|
4
4
|
# A solmization is the rendering of scale degrees as syllables.
|
|
5
|
-
class HeadMusic::Rudiment::Solmization
|
|
5
|
+
class HeadMusic::Rudiment::Solmization < HeadMusic::Rudiment::Base
|
|
6
6
|
include HeadMusic::Named
|
|
7
7
|
|
|
8
8
|
DEFAULT_SOLMIZATION = "solfège"
|
|
@@ -4,8 +4,14 @@ module HeadMusic::Rudiment; end
|
|
|
4
4
|
# Represents the spelling of a pitch, such as C# or Db.
|
|
5
5
|
# Composite of a LetterName and an optional Alteration.
|
|
6
6
|
# Does not include the octave. See Pitch for that.
|
|
7
|
-
class HeadMusic::Rudiment::Spelling
|
|
8
|
-
|
|
7
|
+
class HeadMusic::Rudiment::Spelling < HeadMusic::Rudiment::Base
|
|
8
|
+
LetterName = HeadMusic::Rudiment::LetterName
|
|
9
|
+
Alteration = HeadMusic::Rudiment::Alteration
|
|
10
|
+
|
|
11
|
+
MATCHER = /^\s*(#{LetterName::PATTERN})(#{Alteration::PATTERN})?(-?\d+)?\s*$/i
|
|
12
|
+
|
|
13
|
+
# All chromatic spellings using single sharps and flats (ASCII notation)
|
|
14
|
+
CHROMATIC_SPELLINGS = %w[C C# Db D D# Eb E F F# Gb G G# Ab A A# Bb B].freeze
|
|
9
15
|
|
|
10
16
|
attr_reader :pitch_class, :letter_name, :alteration
|
|
11
17
|
|
|
@@ -86,8 +92,6 @@ class HeadMusic::Rudiment::Spelling
|
|
|
86
92
|
!alteration || alteration.natural?
|
|
87
93
|
end
|
|
88
94
|
|
|
89
|
-
private_class_method :new
|
|
90
|
-
|
|
91
95
|
private
|
|
92
96
|
|
|
93
97
|
def enharmonic_equivalence
|
|
@@ -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
|
+
}.freeze
|
|
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
|
|
@@ -43,43 +43,4 @@ class HeadMusic::Rudiment::Tuning::JustIntonation < HeadMusic::Rudiment::Tuning
|
|
|
43
43
|
# Calculate the frequency
|
|
44
44
|
tonal_center_frequency * ratio
|
|
45
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
46
|
end
|
|
@@ -45,43 +45,4 @@ class HeadMusic::Rudiment::Tuning::Meantone < HeadMusic::Rudiment::Tuning
|
|
|
45
45
|
# Calculate the frequency
|
|
46
46
|
tonal_center_frequency * ratio
|
|
47
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
48
|
end
|
|
@@ -49,43 +49,4 @@ class HeadMusic::Rudiment::Tuning::Pythagorean < HeadMusic::Rudiment::Tuning
|
|
|
49
49
|
# Calculate the frequency
|
|
50
50
|
tonal_center_frequency * ratio
|
|
51
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
52
|
end
|
|
@@ -3,7 +3,7 @@ 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
|
|
@@ -32,4 +32,24 @@ class HeadMusic::Rudiment::Tuning
|
|
|
32
32
|
pitch = HeadMusic::Rudiment::Pitch.get(pitch)
|
|
33
33
|
reference_pitch_frequency * (2**(1.0 / 12))**(pitch - reference_pitch.pitch).semitones
|
|
34
34
|
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def calculate_tonal_center_frequency
|
|
39
|
+
# Use equal temperament to get the tonal center frequency from the reference pitch
|
|
40
|
+
interval_to_tonal_center = (tonal_center - reference_pitch.pitch).semitones
|
|
41
|
+
reference_pitch_frequency * (2**(interval_to_tonal_center / 12.0))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def ratio_for_interval(semitones)
|
|
45
|
+
# Handle octaves
|
|
46
|
+
octaves = semitones / 12
|
|
47
|
+
interval_within_octave = semitones % 12
|
|
48
|
+
|
|
49
|
+
# Get the base ratio from the subclass's INTERVAL_RATIOS constant
|
|
50
|
+
base_ratio = self.class::INTERVAL_RATIOS[self.class::INTERVAL_RATIOS.keys[interval_within_octave]]
|
|
51
|
+
|
|
52
|
+
# Apply octave adjustments
|
|
53
|
+
base_ratio * (2**octaves)
|
|
54
|
+
end
|
|
35
55
|
end
|
|
@@ -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
|
|
@@ -27,11 +27,11 @@ class HeadMusic::Style::Guidelines::ConsonantClimax < HeadMusic::Style::Annotati
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def highest_pitch_consonant_with_tonic?
|
|
30
|
-
diatonic_interval_to_highest_pitch.
|
|
30
|
+
!diatonic_interval_to_highest_pitch.dissonant?(:melodic)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def lowest_pitch_consonant_with_tonic?
|
|
34
|
-
diatonic_interval_to_lowest_pitch.
|
|
34
|
+
!diatonic_interval_to_lowest_pitch.dissonant?(:melodic)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def diatonic_interval_to_highest_pitch
|
|
@@ -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,31 @@
|
|
|
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 # 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
|
+
# Perfect fourth is contextual: consonant in upper voices, dissonant against bass
|
|
22
|
+
HeadMusic::Rudiment::Consonance::CONTEXTUAL
|
|
23
|
+
when 2, 10 # Major Second, Minor Seventh
|
|
24
|
+
HeadMusic::Rudiment::Consonance::MILD_DISSONANCE
|
|
25
|
+
when 1, 11 # Minor Second, Major Seventh
|
|
26
|
+
HeadMusic::Rudiment::Consonance::HARSH_DISSONANCE
|
|
27
|
+
when 6 # Tritone (Aug 4th/Dim 5th)
|
|
28
|
+
HeadMusic::Rudiment::Consonance::DISSONANCE
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
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
|
+
HeadMusic::Utilities::Case.to_snake_case(self.class.name.split("::").last.sub(/Tradition$/, "")).to_sym
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HeadMusic
|
|
4
|
+
module Time
|
|
5
|
+
# A value object representing elapsed nanoseconds of clock time
|
|
6
|
+
#
|
|
7
|
+
# ClockPosition provides a high-precision representation of time elapsed
|
|
8
|
+
# from a reference point, stored as nanoseconds. This allows for precise
|
|
9
|
+
# temporal calculations in musical contexts where millisecond-level accuracy
|
|
10
|
+
# is required for MIDI timing, audio synchronization, and SMPTE timecode.
|
|
11
|
+
#
|
|
12
|
+
# @example Creating a position at one second
|
|
13
|
+
# position = HeadMusic::Time::ClockPosition.new(1_000_000_000)
|
|
14
|
+
# position.to_seconds # => 1.0
|
|
15
|
+
#
|
|
16
|
+
# @example Adding two positions together
|
|
17
|
+
# pos1 = HeadMusic::Time::ClockPosition.new(500_000_000)
|
|
18
|
+
# pos2 = HeadMusic::Time::ClockPosition.new(300_000_000)
|
|
19
|
+
# result = pos1 + pos2
|
|
20
|
+
# result.to_milliseconds # => 800.0
|
|
21
|
+
#
|
|
22
|
+
# @example Comparing positions
|
|
23
|
+
# early = HeadMusic::Time::ClockPosition.new(1_000_000_000)
|
|
24
|
+
# late = HeadMusic::Time::ClockPosition.new(2_000_000_000)
|
|
25
|
+
# early < late # => true
|
|
26
|
+
class ClockPosition
|
|
27
|
+
include Comparable
|
|
28
|
+
|
|
29
|
+
# @return [Integer] the number of nanoseconds since the reference point
|
|
30
|
+
attr_reader :nanoseconds
|
|
31
|
+
|
|
32
|
+
# Create a new clock position
|
|
33
|
+
#
|
|
34
|
+
# @param nanoseconds [Integer] the number of nanoseconds elapsed
|
|
35
|
+
def initialize(nanoseconds)
|
|
36
|
+
@nanoseconds = nanoseconds
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Convert to integer representation (nanoseconds)
|
|
40
|
+
#
|
|
41
|
+
# @return [Integer] nanoseconds
|
|
42
|
+
def to_i
|
|
43
|
+
nanoseconds
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Convert nanoseconds to microseconds
|
|
47
|
+
#
|
|
48
|
+
# @return [Float] elapsed microseconds
|
|
49
|
+
def to_microseconds
|
|
50
|
+
nanoseconds / 1_000.0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Convert nanoseconds to milliseconds
|
|
54
|
+
#
|
|
55
|
+
# @return [Float] elapsed milliseconds
|
|
56
|
+
def to_milliseconds
|
|
57
|
+
nanoseconds / 1_000_000.0
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Convert nanoseconds to seconds
|
|
61
|
+
#
|
|
62
|
+
# @return [Float] elapsed seconds
|
|
63
|
+
def to_seconds
|
|
64
|
+
nanoseconds / 1_000_000_000.0
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Add another clock position to this one
|
|
68
|
+
#
|
|
69
|
+
# @param other [ClockPosition, #to_i] another position or value with nanoseconds
|
|
70
|
+
# @return [ClockPosition] a new position with the combined duration
|
|
71
|
+
def +(other)
|
|
72
|
+
self.class.new(nanoseconds + other.to_i)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Compare this position to another
|
|
76
|
+
#
|
|
77
|
+
# @param other [ClockPosition, #to_i] another position to compare
|
|
78
|
+
# @return [Integer] -1 if less than, 0 if equal, 1 if greater than
|
|
79
|
+
def <=>(other)
|
|
80
|
+
nanoseconds <=> other.to_i
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|