fet 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/main.yml +18 -0
- data/.gitignore +11 -0
- data/.rubocop.yml +47 -0
- data/.ruby_version +1 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +19 -0
- data/Gemfile.lock +71 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +113 -0
- data/README.rdoc +6 -0
- data/Rakefile +23 -0
- data/bin/bash_scripts/cleanup_listening_exercises.sh +2 -0
- data/bin/bash_scripts/cleanup_singing_exercises.sh +2 -0
- data/bin/bash_scripts/midi_to_mp3.sh +13 -0
- data/bin/console +15 -0
- data/bin/fet +87 -0
- data/bin/setup +8 -0
- data/fet.gemspec +41 -0
- data/fet.rdoc +94 -0
- data/lib/fet/chord_progression.rb +55 -0
- data/lib/fet/cli/generate/listening.rb +20 -0
- data/lib/fet/cli/generate/singing.rb +18 -0
- data/lib/fet/cli/generate/single_note_listening.rb +17 -0
- data/lib/fet/degree.rb +44 -0
- data/lib/fet/degrees.rb +61 -0
- data/lib/fet/exceptions.rb +18 -0
- data/lib/fet/generator/listening.rb +107 -0
- data/lib/fet/generator/singing.rb +89 -0
- data/lib/fet/generator/single_note_listening.rb +67 -0
- data/lib/fet/hardcoded_midi_values.rb +24 -0
- data/lib/fet/instrument_ranges.rb +19 -0
- data/lib/fet/midi_note.rb +40 -0
- data/lib/fet/midilib_interface.rb +134 -0
- data/lib/fet/music_theory.rb +116 -0
- data/lib/fet/note.rb +138 -0
- data/lib/fet/note_validations.rb +58 -0
- data/lib/fet/version.rb +5 -0
- data/lib/fet.rb +10 -0
- metadata +147 -0
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
|
data/lib/fet/version.rb
ADDED
data/lib/fet.rb
ADDED
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: []
|