head_music 8.3.0 → 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/CHANGELOG.md +53 -0
- data/CLAUDE.md +32 -15
- data/Gemfile.lock +1 -1
- data/MUSIC_THEORY.md +120 -0
- data/lib/head_music/analysis/diatonic_interval.rb +29 -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_family.rb +13 -2
- data/lib/head_music/instruments/instrument_type.rb +188 -0
- 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 +6 -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 +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 +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.rb +1 -1
- data/lib/head_music/rudiment/unpitched_note.rb +62 -0
- 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 +31 -10
- 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/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/{backlog → todo}/dyad-analysis.md +2 -10
- 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/{backlog → todo}/pitch-class-set-analysis.md +40 -0
- data/user_stories/{backlog → todo}/pitch-set-classification.md +10 -0
- data/user_stories/{backlog → todo}/sonority-identification.md +20 -0
- metadata +43 -12
- data/TODO.md +0 -109
- /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 → todo}/consonance-dissonance-classification.md +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
class HeadMusic::Rudiment::RhythmicValue::Parser
|
|
2
|
+
attr_reader :identifier, :rhythmic_value
|
|
3
|
+
|
|
4
|
+
RhythmicUnit = HeadMusic::Rudiment::RhythmicUnit
|
|
5
|
+
RhythmicValue = HeadMusic::Rudiment::RhythmicValue
|
|
6
|
+
|
|
7
|
+
PATTERN = /((double|triple)\W?)?(dotted)?.?(#{HeadMusic::Rudiment::RhythmicUnit::PATTERN})/
|
|
8
|
+
|
|
9
|
+
# For stuff like the "q." in "q. = 108"
|
|
10
|
+
SHORTHAND_PATTERN = /\A(#{HeadMusic::Rudiment::RhythmicUnit::Parser::TEMPO_SHORTHAND_PATTERN})(\.*)?\z/i
|
|
11
|
+
|
|
12
|
+
# Parse a rhythmic value identifier and return a RhythmicValue object
|
|
13
|
+
# Returns nil if the identifier cannot be parsed
|
|
14
|
+
def self.parse(identifier)
|
|
15
|
+
return nil if identifier.nil?
|
|
16
|
+
new(identifier).rhythmic_value
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(identifier)
|
|
20
|
+
@identifier = identifier.to_s.strip
|
|
21
|
+
parse_components
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def parse_components
|
|
27
|
+
# First check for shorthand patterns like "q." to avoid infinite recursion
|
|
28
|
+
match = identifier.match(SHORTHAND_PATTERN)
|
|
29
|
+
if match && match[1]
|
|
30
|
+
unit_name = RhythmicUnit::Parser.parse(match[1].to_s.strip)
|
|
31
|
+
dots = match[2] ? match[2].strip.length : 0
|
|
32
|
+
@rhythmic_value = RhythmicValue.new(unit_name, dots: dots) if unit_name
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Try RhythmicUnit::Parser directly first (handles fractions, decimals, British names, etc.)
|
|
37
|
+
parser = RhythmicUnit::Parser.new(identifier)
|
|
38
|
+
if parser.american_name
|
|
39
|
+
@rhythmic_value = RhythmicValue.new(parser.american_name, dots: 0)
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Then try to parse with dots extracted for formats like "1/4."
|
|
44
|
+
# Count and strip dots (e.g., "1/4." -> "1/4" with 1 dot)
|
|
45
|
+
# But skip this if identifier looks like a decimal number
|
|
46
|
+
unless identifier.match?(/^\d+\.\d+$/)
|
|
47
|
+
dots = identifier.scan(".").count
|
|
48
|
+
base_identifier = identifier.gsub(".", "").strip
|
|
49
|
+
|
|
50
|
+
# Try RhythmicUnit::Parser on the base identifier
|
|
51
|
+
parser = RhythmicUnit::Parser.new(base_identifier)
|
|
52
|
+
if parser.american_name
|
|
53
|
+
@rhythmic_value = RhythmicValue.new(parser.american_name, dots: dots)
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Finally check the word pattern for things like "dotted quarter"
|
|
59
|
+
match = identifier.match(PATTERN)
|
|
60
|
+
if match
|
|
61
|
+
matched_string = match[0].to_s.strip
|
|
62
|
+
# Extract unit and dots from the matched string
|
|
63
|
+
unit_part = matched_string.gsub(/^\W*(double|triple)?\W*(dotted)?\W*/, "")
|
|
64
|
+
unit = RhythmicUnit.get(unit_part)
|
|
65
|
+
if unit
|
|
66
|
+
dots = if matched_string.include?("triple")
|
|
67
|
+
3
|
|
68
|
+
elsif matched_string.include?("double")
|
|
69
|
+
2
|
|
70
|
+
else
|
|
71
|
+
matched_string.include?("dotted") ? 1 : 0
|
|
72
|
+
end
|
|
73
|
+
@rhythmic_value = RhythmicValue.new(unit, dots: dots)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# A module for musical content
|
|
2
|
-
module HeadMusic::
|
|
2
|
+
module HeadMusic::Rudiment; end
|
|
3
3
|
|
|
4
4
|
# A rhythmic value is a duration composed of a rhythmic unit, any number of dots, and a tied value.
|
|
5
|
-
class HeadMusic::
|
|
5
|
+
class HeadMusic::Rudiment::RhythmicValue
|
|
6
|
+
include Comparable
|
|
7
|
+
|
|
8
|
+
RhythmicUnit = HeadMusic::Rudiment::RhythmicUnit
|
|
9
|
+
|
|
6
10
|
attr_reader :unit, :dots, :tied_value
|
|
7
11
|
|
|
8
12
|
delegate :name, to: :unit, prefix: true
|
|
@@ -10,13 +14,23 @@ class HeadMusic::Content::RhythmicValue
|
|
|
10
14
|
|
|
11
15
|
def self.get(identifier)
|
|
12
16
|
case identifier
|
|
13
|
-
when HeadMusic::
|
|
17
|
+
when HeadMusic::Rudiment::RhythmicValue
|
|
14
18
|
identifier
|
|
15
19
|
when HeadMusic::Rudiment::RhythmicUnit
|
|
16
20
|
new(identifier)
|
|
17
21
|
when Symbol, String
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
original_identifier = identifier.to_s.strip
|
|
23
|
+
# First try the new parser which handles all formats
|
|
24
|
+
parsed = Parser.parse(original_identifier)
|
|
25
|
+
return parsed if parsed
|
|
26
|
+
|
|
27
|
+
# Then try the word-based approach as fallback
|
|
28
|
+
identifier = original_identifier.downcase.gsub(/\W+/, "_")
|
|
29
|
+
begin
|
|
30
|
+
from_words(identifier)
|
|
31
|
+
rescue
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
20
34
|
end
|
|
21
35
|
end
|
|
22
36
|
|
|
@@ -102,4 +116,8 @@ class HeadMusic::Content::RhythmicValue
|
|
|
102
116
|
def to_s
|
|
103
117
|
name.tr("_", "-")
|
|
104
118
|
end
|
|
119
|
+
|
|
120
|
+
def <=>(other)
|
|
121
|
+
total_value <=> other.total_value
|
|
122
|
+
end
|
|
105
123
|
end
|
|
@@ -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,11 @@ 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
|
|
9
12
|
|
|
10
13
|
attr_reader :pitch_class, :letter_name, :alteration
|
|
11
14
|
|
|
@@ -86,8 +89,6 @@ class HeadMusic::Rudiment::Spelling
|
|
|
86
89
|
!alteration || alteration.natural?
|
|
87
90
|
end
|
|
88
91
|
|
|
89
|
-
private_class_method :new
|
|
90
|
-
|
|
91
92
|
private
|
|
92
93
|
|
|
93
94
|
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
|
+
}
|
|
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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -2,8 +2,40 @@
|
|
|
2
2
|
module HeadMusic::Utilities; end
|
|
3
3
|
|
|
4
4
|
# Util for converting an object to a consistent hash key
|
|
5
|
-
|
|
5
|
+
class HeadMusic::Utilities::HashKey
|
|
6
6
|
def self.for(identifier)
|
|
7
|
-
|
|
7
|
+
@hash_keys ||= {}
|
|
8
|
+
@hash_keys[identifier] ||= new(identifier).to_sym
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :original
|
|
12
|
+
|
|
13
|
+
def initialize(identifier)
|
|
14
|
+
@original = identifier
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_sym
|
|
18
|
+
normalized_string.to_sym
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def normalized_string
|
|
24
|
+
@normalized_string ||=
|
|
25
|
+
transliterated_string.downcase.gsub(/\W+/, "_")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def transliterated_string
|
|
29
|
+
I18n.transliterate(desymbolized_string)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def desymbolized_string
|
|
33
|
+
original.to_s
|
|
34
|
+
.gsub("𝄫", "_double_flat")
|
|
35
|
+
.gsub("♭", "_flat")
|
|
36
|
+
.gsub("♮", "_natural")
|
|
37
|
+
.gsub("♯", "_sharp")
|
|
38
|
+
.gsub("#", "_sharp")
|
|
39
|
+
.gsub("𝄪", "_double_sharp")
|
|
8
40
|
end
|
|
9
41
|
end
|
data/lib/head_music/version.rb
CHANGED