head_music 0.1.5 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8379f9a907ea8dd73018b1cd9a8da6a055510735
4
- data.tar.gz: f3de473d170a910e26a5b4318232a5a9d7eb4258
3
+ metadata.gz: 67e0581fd0dc08759395cc97286a1e96b538147d
4
+ data.tar.gz: dc985557ff8d0e1aa4720a4732a8b29a643251d8
5
5
  SHA512:
6
- metadata.gz: 05c161185607fd4f4ac48b5ad9389ee6d15fda24deaed1bf1a8f4e88b7140615ef60cdf695266c6e6a714cf66dc9db3a5f47e056c7c6096a9c0fde6e0d5fb0fa
7
- data.tar.gz: f7142b9a29669c3a885c6ee9edf68330c9912ff8e71622efb1d95f7c3f314dea05dc2443c5f1c092988ceb236ba7a7768ac28a0ba43651c5954085595e57b295
6
+ metadata.gz: 09fcb0a4675636f67580e095b73e5640d122d7ad72df17b0677456f0234778df1a43137eb1d3c7c5e2852a0f8acada492a179bb0e8da3e6b64252164cc554fe2
7
+ data.tar.gz: b1b1809ed935b1a03688220039cdce9ecd8c98a55d31d71a818077ce2b2a4e416d8a00ae6dbbba7fe742cf0dc4f507ce173b4a395d4638b4d0adef497f64fb18
@@ -9,15 +9,16 @@ class HeadMusic::Accidental
9
9
  attr_reader :string
10
10
 
11
11
  def self.get(identifier)
12
- @accidentals ||= {}
13
12
  for_symbol(identifier) || for_interval(identifier)
14
13
  end
15
14
 
16
15
  def self.for_symbol(identifier)
16
+ @accidentals ||= {}
17
17
  @accidentals[identifier.to_s] ||= new(identifier.to_s) if ACCIDENTAL_SEMITONES.keys.include?(identifier.to_s)
18
18
  end
19
19
 
20
20
  def self.for_interval(semitones)
21
+ @accidentals ||= {}
21
22
  @accidentals[semitones.to_i] ||= ACCIDENTAL_SEMITONES.key(semitones.to_i)
22
23
  end
23
24
 
@@ -8,8 +8,8 @@ class HeadMusic::Interval
8
8
  attr_reader :semitones
9
9
 
10
10
  def self.get(semitones)
11
- @intervals_memo ||= {}
12
- @intervals_memo[semitones.to_i] ||= new(semitones.to_i)
11
+ @intervals ||= {}
12
+ @intervals[semitones.to_i] ||= new(semitones.to_i)
13
13
  end
14
14
 
15
15
  def self.named(name)
@@ -18,11 +18,11 @@ class HeadMusic::Letter
18
18
  end
19
19
 
20
20
  def self.get(identifier)
21
- @letters ||= {}
22
21
  from_name(identifier) || from_pitch_class(identifier)
23
22
  end
24
23
 
25
24
  def self.from_name(name)
25
+ @letters ||= {}
26
26
  name = name.to_s.first.upcase
27
27
  @letters[name] ||= new(name) if NAMES.include?(name)
28
28
  end
@@ -6,19 +6,20 @@ class HeadMusic::Octave
6
6
  MATCHER = /(-?\d+)$/
7
7
 
8
8
  def self.get(identifier)
9
- @octaves ||= {}
10
9
  from_number(identifier) || from_name(identifier) || default
11
10
  end
12
11
 
13
12
  def self.from_number(identifier)
14
13
  return nil unless identifier.to_s == identifier.to_i.to_s
15
14
  return nil unless (-2..12).include?(identifier.to_i)
15
+ @octaves ||= {}
16
16
  @octaves[identifier.to_i] ||= new(identifier.to_i)
17
17
  end
18
18
 
19
19
  def self.from_name(string)
20
20
  if string.to_s.match(HeadMusic::Spelling::MATCHER)
21
21
  _letter, _accidental, octave_string = string.to_s.match(HeadMusic::Spelling::MATCHER).captures
22
+ @octaves ||= {}
22
23
  @octaves[octave_string.to_i] ||= new(octave_string.to_i) if octave_string
23
24
  end
24
25
  end
@@ -4,11 +4,14 @@ class HeadMusic::Pitch
4
4
  attr_reader :spelling
5
5
  attr_reader :octave
6
6
 
7
- delegate :letter, :accidental, :pitch_class, to: :spelling
7
+ delegate :letter, to: :spelling
8
+ delegate :accidental, :sharp?, :flat?, to: :spelling
9
+ delegate :pitch_class, to: :spelling
10
+
11
+ delegate :smallest_interval_to, to: :pitch_class
8
12
 
9
13
  def self.get(value)
10
- @pitches ||= {}
11
- @pitches[value] ||= from_name(value) || from_number(value)
14
+ from_name(value) || from_number(value)
12
15
  end
13
16
 
14
17
  def self.from_name(name)
@@ -23,7 +26,19 @@ class HeadMusic::Pitch
23
26
  fetch_or_create(spelling, octave)
24
27
  end
25
28
 
29
+ def self.from_number_and_letter(number, letter)
30
+ letter = HeadMusic::Letter.get(letter)
31
+ natural_letter_pitch = get(HeadMusic::Letter.get(letter).pitch_class)
32
+ natural_letter_pitch += 12 while (number - natural_letter_pitch.to_i) >= 12
33
+ natural_letter_pitch = get(natural_letter_pitch)
34
+ accidental_interval = natural_letter_pitch.smallest_interval_to(HeadMusic::PitchClass.get(number))
35
+ accidental = HeadMusic::Accidental.for_interval(accidental_interval)
36
+ spelling = HeadMusic::Spelling.fetch_or_create(letter, accidental)
37
+ fetch_or_create(spelling, natural_letter_pitch.octave)
38
+ end
39
+
26
40
  def self.fetch_or_create(spelling, octave)
41
+ @pitches ||= {}
27
42
  if spelling && (-1..9).include?(octave)
28
43
  key = [spelling, octave].join
29
44
  @pitches[key] ||= new(spelling, octave)
@@ -57,6 +72,14 @@ class HeadMusic::Pitch
57
72
  self.midi_note_number == other.midi_note_number
58
73
  end
59
74
 
75
+ def +(value)
76
+ Pitch.get(self.to_i + value.to_i)
77
+ end
78
+
79
+ def -(value)
80
+ Pitch.get(self.to_i - value.to_i)
81
+ end
82
+
60
83
  def ==(value)
61
84
  to_s == value.to_s
62
85
  end
@@ -2,6 +2,8 @@ class HeadMusic::PitchClass
2
2
  attr_reader :number
3
3
 
4
4
  PREFERRED_SPELLINGS = %w[C C# D Eb E F F# G Ab A Bb B]
5
+ SHARP_SPELLINGS = %w[C C# D D# E F F# G G# A A# B]
6
+ FLAT_SPELLINGS = %w[C Db D Eb E F Gb G Ab A Bb B]
5
7
 
6
8
  def self.get(identifier)
7
9
  @pitch_classes ||= {}
@@ -1,29 +1,52 @@
1
1
  class HeadMusic::Scale
2
- PATTERNS = {
3
- major: [0, 2, 4, 5, 7, 9, 11, 12],
4
- minor: [0, 2, 3, 5, 7, 8, 10, 12],
5
- minor_pentatonic: [0, nil, 3, 5, 7, nil, 10, 12],
6
- }
7
-
8
- attr_reader :pattern
9
-
10
- def self.major
2
+ def self.get(root_pitch, scale_type_name = nil)
3
+ root_pitch = HeadMusic::Pitch.get(root_pitch)
4
+ scale_type_name ||= :major
5
+ scale_type ||= HeadMusic::ScaleType.get(scale_type_name)
11
6
  @scales ||= {}
12
- @scales[:major] ||= new(PATTERNS[:major])
7
+ @scales[root_pitch.to_s] ||= {}
8
+ @scales[root_pitch.to_s][scale_type.name] ||= new(root_pitch, scale_type)
13
9
  end
14
10
 
15
- def self.minor
16
- @scales ||= {}
17
- @scales[:minor] ||= new(PATTERNS[:minor])
11
+ attr_reader :root_pitch, :scale_type
12
+
13
+ def initialize(root_pitch, scale_type)
14
+ @root_pitch = HeadMusic::Pitch.get(root_pitch)
15
+ @scale_type = HeadMusic::ScaleType.get(scale_type)
18
16
  end
19
17
 
20
- def self.minor_pentatonic
21
- @scales ||= {}
22
- @scales[:minor_pentatonic] ||= new(PATTERNS[:minor_pentatonic])
18
+ def pitches
19
+ @pitches ||= begin
20
+ pitches = [root_pitch]
21
+ letters_cycle = HeadMusic::Letter::NAMES
22
+ letters_cycle = letters_cycle.rotate while letters_cycle.first != root_pitch.letter.to_s
23
+ semitones_from_root = 0
24
+ if scale_type.parent
25
+ parent_scale_pitches = HeadMusic::Scale.get(root_pitch, scale_type.parent_name).pitches
26
+ end
27
+ scale_type.intervals.each_with_index do |semitones, i|
28
+ semitones_from_root += semitones
29
+ pitch_number = root_pitch.pitch_class.to_i + semitones_from_root
30
+ if scale_type.intervals.length == 7
31
+ current_letter = letters_cycle[(i + 1) % 7]
32
+ elsif scale_type.intervals.length < 7 && scale_type.parent
33
+ current_letter = parent_scale_pitches.detect { |parent_scale_pitches|
34
+ parent_scale_pitches.pitch_class == (root_pitch + semitones_from_root).to_i % 12
35
+ }.letter
36
+ elsif root_pitch.flat?
37
+ current_letter = HeadMusic::PitchClass::FLAT_SPELLINGS[pitch_number % 12]
38
+ else
39
+ current_letter = HeadMusic::PitchClass::SHARP_SPELLINGS[pitch_number % 12]
40
+ end
41
+ pitch = HeadMusic::Pitch.from_number_and_letter(pitch_number, current_letter)
42
+ pitches << pitch
43
+ end
44
+ pitches
45
+ end
23
46
  end
24
47
 
25
- def initialize(pattern)
26
- @pattern = pattern
48
+ def pitch_names
49
+ pitches.map(&:spelling).map(&:to_s)
27
50
  end
28
51
 
29
52
  def in(spelling)
@@ -16,17 +16,13 @@ class HeadMusic::ScaleType
16
16
  HARMONIC_MINOR = [W, H, W, W, H, WH, H]
17
17
  MELODIC_MINOR_ASCENDING = [W, H, W, W, W, W, H]
18
18
 
19
- CHROMATIC = [H, H, H, H, H, H, H, H, H, H, H, H]
20
- MINOR_PENTATONIC = [3, 2, 2, 3, 2]
21
- MAJOR_PENTATONIC = MINOR_PENTATONIC.rotate
22
-
23
19
  MODE_NAMES = {
24
- i: [:ionian, :major, :maj],
20
+ i: [:ionian, :major],
25
21
  ii: [:dorian],
26
22
  iii: [:phrygian],
27
23
  iv: [:lydian],
28
24
  v: [:mixolydian],
29
- vi: [:aeolian, :minor, :natural_minor, :min],
25
+ vi: [:aeolian, :minor, :natural_minor],
30
26
  vii: [:locrian],
31
27
  }
32
28
  SCALE_TYPES = {}
@@ -40,14 +36,19 @@ class HeadMusic::ScaleType
40
36
  SCALE_TYPES[:harmonic_minor] = { ascending: HARMONIC_MINOR }
41
37
  SCALE_TYPES[:melodic_minor] = { ascending: MELODIC_MINOR_ASCENDING, descending: VI.reverse }
42
38
 
39
+ CHROMATIC = [H, H, H, H, H, H, H, H, H, H, H, H]
43
40
  SCALE_TYPES[:chromatic] = { ascending: CHROMATIC }
44
41
 
45
- SCALE_TYPES[:minor_pentatonic] = { ascending: MINOR_PENTATONIC }
46
- SCALE_TYPES[:major_pentatonic] = { ascending: MAJOR_PENTATONIC }
42
+ MINOR_PENTATONIC = [3, 2, 2, 3, 2]
43
+ SCALE_TYPES[:minor_pentatonic] = { ascending: MINOR_PENTATONIC, parent_name: :minor }
44
+ SCALE_TYPES[:major_pentatonic] = { ascending: MINOR_PENTATONIC.rotate, parent_name: :major }
45
+ SCALE_TYPES[:egyptian_pentatonic] = { ascending: MINOR_PENTATONIC.rotate(2), parent_name: :minor }
46
+ SCALE_TYPES[:blues_minor_pentatonic] = { ascending: MINOR_PENTATONIC.rotate(3), parent_name: :minor }
47
+ SCALE_TYPES[:blues_major_pentatonic] = { ascending: MINOR_PENTATONIC.rotate(4), parent_name: :major }
47
48
 
49
+ # exotic scales
48
50
  SCALE_TYPES[:octatonic] = { ascending: [W, H, W, H, W, H, W, H] }
49
51
  SCALE_TYPES[:whole_tone] = { ascending: [W, W, W, W, W, W] }
50
- SCALE_TYPES[:monotonic] = { ascending: [12] }
51
52
 
52
53
  class << self
53
54
  SCALE_TYPES.keys.each do |name|
@@ -60,17 +61,20 @@ class HeadMusic::ScaleType
60
61
  def self.get(name)
61
62
  @scale_types ||= {}
62
63
  name = name.to_s.to_sym
63
- intervals = SCALE_TYPES[name]
64
- @scale_types[name] ||= new(name, intervals[:ascending], intervals[:descending])
64
+ attributes = SCALE_TYPES[name]
65
+ @scale_types[name] ||= new(name, attributes)
65
66
  end
66
67
 
67
- attr_reader :name, :ascending_intervals, :descending_intervals
68
+ attr_reader :name, :ascending_intervals, :descending_intervals, :parent_name
69
+ alias_method :intervals, :ascending_intervals
70
+
68
71
  delegate :to_s, to: :name
69
72
 
70
- def initialize(name, ascending_intervals, descending_intervals = nil)
73
+ def initialize(name, attributes)
71
74
  @name = name
72
- @ascending_intervals = ascending_intervals
73
- @descending_intervals = descending_intervals || ascending_intervals.reverse
75
+ @ascending_intervals = attributes[:ascending]
76
+ @descending_intervals = attributes[:descending] || ascending_intervals.reverse
77
+ @parent_name = attributes[:parent_name]
74
78
  end
75
79
 
76
80
  def ==(other)
@@ -80,4 +84,8 @@ class HeadMusic::ScaleType
80
84
  def state
81
85
  [ascending_intervals, descending_intervals]
82
86
  end
87
+
88
+ def parent
89
+ @parent ||= self.class.get(parent_name) if parent_name
90
+ end
83
91
  end
@@ -10,7 +10,6 @@ class HeadMusic::Spelling
10
10
  delegate :number, to: :pitch_class, prefix: true
11
11
 
12
12
  def self.get(identifier)
13
- @spellings ||= {}
14
13
  from_name(identifier) || from_number(identifier)
15
14
  end
16
15
 
@@ -32,11 +31,19 @@ class HeadMusic::Spelling
32
31
  return nil unless number == number.to_i
33
32
  pitch_class_number = number.to_i % 12
34
33
  letter = HeadMusic::Letter.from_pitch_class(pitch_class_number)
35
- accidental = HeadMusic::Accidental.for_interval(pitch_class_number - letter.pitch_class.to_i)
34
+ from_number_and_letter(number, letter)
35
+ end
36
+
37
+ def self.from_number_and_letter(number, letter)
38
+ letter = HeadMusic::Letter.get(letter)
39
+ natural_letter_pitch_class = HeadMusic::Letter.get(letter).pitch_class
40
+ accidental_interval = letter.pitch_class.smallest_interval_to(HeadMusic::PitchClass.get(number))
41
+ accidental = HeadMusic::Accidental.for_interval(accidental_interval)
36
42
  fetch_or_create(letter, accidental)
37
43
  end
38
44
 
39
45
  def self.fetch_or_create(letter, accidental)
46
+ @spellings ||= {}
40
47
  key = [letter, accidental].join
41
48
  @spellings[key] ||= new(letter, accidental)
42
49
  end
@@ -56,6 +63,14 @@ class HeadMusic::Spelling
56
63
  name
57
64
  end
58
65
 
66
+ def sharp?
67
+ accidental && accidental == '#'
68
+ end
69
+
70
+ def flat?
71
+ accidental && accidental == 'b'
72
+ end
73
+
59
74
  def ==(value)
60
75
  to_s == value.to_s
61
76
  end
@@ -1,3 +1,3 @@
1
1
  module HeadMusic
2
- VERSION = "0.1.5"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: head_music
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rob Head
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-02-18 00:00:00.000000000 Z
11
+ date: 2017-02-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport