head_music 0.1.5 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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