fet 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/fet/note.rb ADDED
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "note_validations"
4
+
5
+ module Fet
6
+ # Class responsible for parsing and validating musical notes from a string, e.g. "Eb"
7
+ class Note
8
+ include NoteValidations
9
+
10
+ ACCIDENTAL_TO_SEMITONES_MAP = {
11
+ "b" => -1,
12
+ "#" => 1,
13
+ "x" => 2,
14
+ }.deep_freeze
15
+
16
+ # TODO: this class could also potentially handle the octave number - note that e.g. Bb,B,Bx... are in octave 3 while Cb,C,C# are in octave 4
17
+ attr_accessor :full_note,
18
+ :natural_note,
19
+ :accidental
20
+
21
+ def initialize(note)
22
+ self.full_note = note
23
+ validate_full_note!
24
+
25
+ self.natural_note = note[0]
26
+ validate_natural_note!
27
+
28
+ self.accidental = note[1..]
29
+ validate_accidental!
30
+ end
31
+
32
+ def self.accidental_from_semitone_offset(semitone_offset)
33
+ return "" if semitone_offset.zero?
34
+ return "b" * -semitone_offset if semitone_offset.negative?
35
+
36
+ number_of_hashes = (semitone_offset % 2).zero? ? 0 : 1
37
+ number_of_xs = semitone_offset / 2
38
+ return "#" * number_of_hashes + "x" * number_of_xs
39
+ end
40
+
41
+ # NOTE: performs the following conversions:
42
+ # Fbb -> Fb -> F -> F# ->Fx -> F#x -> Fxx
43
+ def sharpened_note
44
+ note_as_string = case
45
+ when accidental.start_with?("b")
46
+ "#{natural_note}#{accidental[1..]}"
47
+ when accidental.start_with?("#")
48
+ "#{natural_note}x#{accidental[1..]}"
49
+ else
50
+ "#{natural_note}##{accidental}"
51
+ end
52
+
53
+ return Note.new(note_as_string)
54
+ end
55
+
56
+ # NOTE: performs the following conversions:
57
+ # Fxx -> F#x -> Fx -> F# -> F -> Fb -> Fbb
58
+ def flattened_note
59
+ note_as_string = case
60
+ when accidental.start_with?("x")
61
+ "#{natural_note}##{accidental[1..]}"
62
+ when accidental.start_with?("#")
63
+ "#{natural_note}#{accidental[1..]}"
64
+ else
65
+ "#{natural_note}#{accidental}b"
66
+ end
67
+
68
+ return Note.new(note_as_string)
69
+ end
70
+
71
+ # NOTE: normalizing the note means:
72
+ # - converting the natural note + accidentals such that the remaining accidental is either "b", "", or "#", or
73
+ # - if a note name is provided, then convert the accidental such that the natural note matches the pitch of the original
74
+ def normalized_note
75
+ remaining_semitones = accidental_to_semitone_offset
76
+ next_note = remaining_semitones.positive? ? next_natural_note : previous_natural_note
77
+ next_note_offset = remaining_semitones.positive? ? semitone_offset_to_next_natural_note : semitone_offset_to_previous_natural_note
78
+ return Note.new(full_note) if next_note_offset.abs > remaining_semitones.abs
79
+
80
+ return Note.new("#{next_note}#{self.class.accidental_from_semitone_offset(remaining_semitones - next_note_offset)}").normalized_note
81
+ end
82
+
83
+ # NOTE: Note.new("E").change_natural_note("D") -> Note.new("Dx")
84
+ # def change_natural_note(new_natural_note)
85
+ # semitone_offset = accidental_to_semitone_offset
86
+ # return Note.new("#{natural_note}#{self.class.accidental_from_semitone_offset(semitone_offset)}") if new_natural_note == natural_note
87
+ # end
88
+
89
+ def degree(root_name)
90
+ notes_array = Fet::MusicTheory.notes_of_mode(root_name, "major")
91
+ index = notes_array.index { |note| Note.new(note).natural_note == natural_note }
92
+
93
+ degree = index + 1
94
+ degree_note = Note.new(notes_array[index])
95
+
96
+ accidental_difference = accidental_to_semitone_offset - degree_note.accidental_to_semitone_offset
97
+ return "#{self.class.accidental_from_semitone_offset(accidental_difference)}#{degree}"
98
+ end
99
+
100
+ def accidental_to_semitone_offset
101
+ return accidental.chars.map { |char| ACCIDENTAL_TO_SEMITONES_MAP[char] }.sum
102
+ end
103
+
104
+ def natural?
105
+ return accidental.chars.empty?
106
+ end
107
+
108
+ def flattened?
109
+ return accidental.chars.include?("b")
110
+ end
111
+
112
+ def sharpened?
113
+ return accidental.chars.include?("#") || accidental.chars.include?("x")
114
+ end
115
+
116
+ private
117
+
118
+ def next_natural_note
119
+ index = Fet::MusicTheory::ORDERED_NATURAL_NOTES.index(natural_note)
120
+ index = (index + 1) % Fet::MusicTheory::ORDERED_NATURAL_NOTES.size
121
+ return Fet::MusicTheory::ORDERED_NATURAL_NOTES[index]
122
+ end
123
+
124
+ def previous_natural_note
125
+ index = Fet::MusicTheory::ORDERED_NATURAL_NOTES.index(natural_note)
126
+ index = (index - 1) % Fet::MusicTheory::ORDERED_NATURAL_NOTES.size
127
+ return Fet::MusicTheory::ORDERED_NATURAL_NOTES[index]
128
+ end
129
+
130
+ def semitone_offset_to_next_natural_note
131
+ return Fet::MusicTheory::SEMITONES_TO_NEXT_NATURAL_NOTE[natural_note]
132
+ end
133
+
134
+ def semitone_offset_to_previous_natural_note
135
+ return -Fet::MusicTheory::SEMITONES_TO_NEXT_NATURAL_NOTE[previous_natural_note]
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fet
4
+ # Module holding validations for the Note class
5
+ module NoteValidations
6
+ private
7
+
8
+ def validate_full_note!
9
+ raise InvalidNote.new(full_note) unless full_note.is_a?(String)
10
+ end
11
+
12
+ def validate_natural_note!
13
+ raise InvalidNote.new(full_note) unless natural_note.is_a?(String)
14
+ raise InvalidNote.new(full_note) unless MusicTheory::CIRCLE_OF_FIFTHS_WITHOUT_ACCIDENTALS.include?(natural_note)
15
+ end
16
+
17
+ def validate_accidental!
18
+ raise InvalidNote.new(full_note) unless valid_accidental_type?
19
+ return if accidental.empty?
20
+
21
+ raise InvalidNote.new(full_note) unless valid_accidental_logic?
22
+ end
23
+
24
+ def valid_accidental_type?
25
+ return accidental.is_a?(String)
26
+ end
27
+
28
+ def valid_accidental_logic?
29
+ return false unless valid_accidental_characters?
30
+ return false if can_be_flat_accidental? && !valid_flat_accidental?
31
+ return false if can_be_sharp_accidental? && !valid_sharp_accidental?
32
+
33
+ return true
34
+ end
35
+
36
+ def valid_accidental_characters?
37
+ return accidental.chars.uniq.all? { |char| ["b", "#", "x"].include?(char) }
38
+ end
39
+
40
+ def can_be_flat_accidental?
41
+ return accidental.chars.uniq.include?("b")
42
+ end
43
+
44
+ def valid_flat_accidental?
45
+ return accidental.chars.uniq.all? { |char| ["b"].include?(char) } # there can only be "b" characters
46
+ end
47
+
48
+ def can_be_sharp_accidental?
49
+ return accidental.chars.uniq.include?("#") || accidental.chars.uniq.include?("x")
50
+ end
51
+
52
+ def valid_sharp_accidental?
53
+ return accidental.chars.uniq.all? { |char| ["#", "x"].include?(char) } && # only allow "#" and "x" characters
54
+ [0, 1].include?(accidental.chars.select { |char| char == "#" }.size) && # there can be at most one "#" character
55
+ accidental.chars.sort.join == accidental # the "#" character must come before any "x" characters
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fet
4
+ VERSION = "0.2.0"
5
+ end
data/lib/fet.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ice_nine"
4
+ require "ice_nine/core_ext/object"
5
+
6
+ Dir["#{__dir__}/fet/**/*.rb"].each { |file| require_relative(file.delete_prefix("#{__dir__}/")) }
7
+
8
+ # Base Gem module
9
+ module Fet
10
+ end
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fet
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Dimitrios Lisenko
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-09-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: gli
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.20'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 2.20.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '2.20'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 2.20.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: ice_nine
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 0.11.2
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 0.11.2
47
+ - !ruby/object:Gem::Dependency
48
+ name: midilib
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.0'
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 2.0.5
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: '2.0'
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: 2.0.5
67
+ description: Teaches your ear to recognize notes based on their function in a key.
68
+ email:
69
+ - dimitrioslisenko@gmail.com
70
+ executables:
71
+ - fet
72
+ extensions: []
73
+ extra_rdoc_files:
74
+ - README.rdoc
75
+ - fet.rdoc
76
+ files:
77
+ - ".github/workflows/main.yml"
78
+ - ".gitignore"
79
+ - ".rubocop.yml"
80
+ - ".ruby_version"
81
+ - CHANGELOG.md
82
+ - Gemfile
83
+ - Gemfile.lock
84
+ - LICENSE
85
+ - LICENSE.txt
86
+ - README.md
87
+ - README.rdoc
88
+ - Rakefile
89
+ - bin/bash_scripts/cleanup_listening_exercises.sh
90
+ - bin/bash_scripts/cleanup_singing_exercises.sh
91
+ - bin/bash_scripts/midi_to_mp3.sh
92
+ - bin/console
93
+ - bin/fet
94
+ - bin/setup
95
+ - fet.gemspec
96
+ - fet.rdoc
97
+ - lib/fet.rb
98
+ - lib/fet/chord_progression.rb
99
+ - lib/fet/cli/generate/listening.rb
100
+ - lib/fet/cli/generate/singing.rb
101
+ - lib/fet/cli/generate/single_note_listening.rb
102
+ - lib/fet/degree.rb
103
+ - lib/fet/degrees.rb
104
+ - lib/fet/exceptions.rb
105
+ - lib/fet/generator/listening.rb
106
+ - lib/fet/generator/singing.rb
107
+ - lib/fet/generator/single_note_listening.rb
108
+ - lib/fet/hardcoded_midi_values.rb
109
+ - lib/fet/instrument_ranges.rb
110
+ - lib/fet/midi_note.rb
111
+ - lib/fet/midilib_interface.rb
112
+ - lib/fet/music_theory.rb
113
+ - lib/fet/note.rb
114
+ - lib/fet/note_validations.rb
115
+ - lib/fet/version.rb
116
+ homepage: https://github.com/DimitriosLisenko/fet
117
+ licenses:
118
+ - MIT
119
+ metadata:
120
+ homepage_uri: https://github.com/DimitriosLisenko/fet
121
+ source_code_uri: https://github.com/DimitriosLisenko/fet
122
+ changelog_uri: https://github.com/DimitriosLisenko/fet/blob/master/CHANGELOG.md
123
+ post_install_message:
124
+ rdoc_options:
125
+ - "--title"
126
+ - fet
127
+ - "--main"
128
+ - README.rdoc
129
+ - "-ri"
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: 3.0.0
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubygems_version: 3.2.22
144
+ signing_key:
145
+ specification_version: 4
146
+ summary: A functional ear trainer.
147
+ test_files: []