fet 0.2.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 +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
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fet
|
4
|
+
module Generator
|
5
|
+
# Class that generates MIDI files for the singing exercises
|
6
|
+
class Singing
|
7
|
+
def initialize(tempo:, pause:, directory_prefix: "")
|
8
|
+
self.tempo = tempo
|
9
|
+
self.pause = pause
|
10
|
+
self.midi_range = HIGH_SINGING_OCTAVE_RANGE
|
11
|
+
self.directory_prefix = directory_prefix
|
12
|
+
end
|
13
|
+
|
14
|
+
def generate
|
15
|
+
generate_major
|
16
|
+
generate_minor
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_accessor :tempo, :pause, :midi_range, :directory_prefix
|
22
|
+
|
23
|
+
def generate_major
|
24
|
+
MusicTheory::MAJOR_KEYS.each do |root_note_name|
|
25
|
+
root_midi_value = MAJOR_ROOT_MIDI_VALUES[root_note_name]
|
26
|
+
midi_range.each do |note_midi_value|
|
27
|
+
create_midi_file("major", root_note_name, root_midi_value, note_midi_value)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def generate_minor
|
33
|
+
MusicTheory::MINOR_KEYS.each do |root_note_name|
|
34
|
+
root_midi_value = MINOR_ROOT_MIDI_VALUES[root_note_name]
|
35
|
+
midi_range.each do |note_midi_value|
|
36
|
+
create_midi_file("minor", root_note_name, root_midi_value, note_midi_value)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def create_midi_file(key_type, root_note_name, root_midi_value, note_midi_value)
|
42
|
+
progression = Fet::ChordProgression.new(offset: root_midi_value, template_type: key_type).with_offset
|
43
|
+
Fet::MidilibInterface.new(
|
44
|
+
tempo: tempo, progression: progression, notes: [note_midi_value],
|
45
|
+
info: generate_midi_info(key_type, root_note_name, root_midi_value, note_midi_value),
|
46
|
+
filename: full_filename(key_type, root_note_name, root_midi_value, note_midi_value),
|
47
|
+
).create_singing_midi_file(pause)
|
48
|
+
end
|
49
|
+
|
50
|
+
def generate_midi_info(key_type, root_note_name, root_midi_value, note_midi_value)
|
51
|
+
note_name = note_name(root_note_name, root_midi_value, note_midi_value)
|
52
|
+
note_octave_value = Fet::MidiNote.new(note_midi_value).octave_number
|
53
|
+
result = [
|
54
|
+
"Key: [#{root_note_name} #{key_type}]",
|
55
|
+
"Degree: [#{degree_name(root_note_name, root_midi_value, note_midi_value)}]",
|
56
|
+
"Note: [#{note_name}#{note_octave_value}]",
|
57
|
+
]
|
58
|
+
return result.join(" ")
|
59
|
+
end
|
60
|
+
|
61
|
+
def full_filename(key_type, root_note_name, root_midi_value, note_midi_value)
|
62
|
+
result = File.join(*[directory_prefix, "singing", key_type].reject(&:empty?))
|
63
|
+
filename = root_note_name # note, e.g. Db
|
64
|
+
filename += key_type == "major" ? "M" : "m" # type of note, M or m
|
65
|
+
filename += "_" # delimiter
|
66
|
+
filename += degree_name(root_note_name, root_midi_value, note_midi_value) # degree, e.g. b7
|
67
|
+
filename += ".mid" # extension
|
68
|
+
return File.join(result, filename)
|
69
|
+
end
|
70
|
+
|
71
|
+
def root_degrees_instance(root_note_name, root_midi_value)
|
72
|
+
root_octave_value = Fet::MidiNote.new(root_midi_value).octave_number
|
73
|
+
return Fet::Degrees.new(root_name: root_note_name, octave_value: root_octave_value)
|
74
|
+
end
|
75
|
+
|
76
|
+
def degree_name(root_note_name, root_midi_value, note_midi_value)
|
77
|
+
degrees_instance = root_degrees_instance(root_note_name, root_midi_value)
|
78
|
+
return degrees_instance.degree_names_of_midi_value(note_midi_value).last
|
79
|
+
end
|
80
|
+
|
81
|
+
def note_name(root_note_name, root_midi_value, note_midi_value)
|
82
|
+
degrees_instance = root_degrees_instance(root_note_name, root_midi_value)
|
83
|
+
return degrees_instance.note_name_of_degree(
|
84
|
+
degree_name(root_note_name, root_midi_value, note_midi_value),
|
85
|
+
)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fet
|
4
|
+
module Generator
|
5
|
+
# Class that generates MIDI files for the single note listening exercises
|
6
|
+
class SingleNoteListening
|
7
|
+
def initialize(tempo:, directory_prefix: "")
|
8
|
+
self.tempo = tempo
|
9
|
+
self.note = Fet::Note.new("C")
|
10
|
+
self.octave_value = 4
|
11
|
+
self.midi_value = MidiNote.from_note(note.full_note, octave_value).midi_value
|
12
|
+
self.directory_prefix = directory_prefix
|
13
|
+
end
|
14
|
+
|
15
|
+
def generate
|
16
|
+
MusicTheory::MAJOR_KEYS.each do |root_note_name|
|
17
|
+
root_midi_value = MAJOR_ROOT_MIDI_VALUES[root_note_name]
|
18
|
+
create_midi_file("major", root_note_name, root_midi_value)
|
19
|
+
end
|
20
|
+
|
21
|
+
MusicTheory::MINOR_KEYS.each do |root_note_name|
|
22
|
+
root_midi_value = MINOR_ROOT_MIDI_VALUES[root_note_name]
|
23
|
+
create_midi_file("minor", root_note_name, root_midi_value)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_accessor :tempo, :note, :octave_value, :midi_value, :directory_prefix
|
30
|
+
|
31
|
+
def create_midi_file(key_type, root_note_name, root_midi_value)
|
32
|
+
progression = Fet::ChordProgression.new(offset: root_midi_value, template_type: key_type).with_offset
|
33
|
+
Fet::MidilibInterface.new(
|
34
|
+
tempo: tempo, progression: progression, notes: [midi_value],
|
35
|
+
info: generate_midi_info(key_type, root_note_name, root_midi_value),
|
36
|
+
filename: full_filename(key_type, root_note_name, root_midi_value),
|
37
|
+
).create_listening_midi_file
|
38
|
+
end
|
39
|
+
|
40
|
+
def generate_midi_info(key_type, root_note_name, root_midi_value)
|
41
|
+
result = [
|
42
|
+
"Key: [#{root_note_name} #{key_type}]",
|
43
|
+
"Degree: [#{degree_name(root_note_name, root_midi_value)}]",
|
44
|
+
"Note: [#{note.full_note}#{octave_value}]",
|
45
|
+
]
|
46
|
+
return result.join(" ")
|
47
|
+
end
|
48
|
+
|
49
|
+
def full_filename(key_type, root_note_name, root_midi_value)
|
50
|
+
result = File.join(*[directory_prefix, "listening_single_note", key_type].reject(&:empty?))
|
51
|
+
filename = root_note_name # note, e.g. Db
|
52
|
+
filename += key_type == "major" ? "M" : "m" # type of note, M or m
|
53
|
+
filename += "_" # delimiter
|
54
|
+
filename += degree_name(root_note_name, root_midi_value) # degree, e.g. b7
|
55
|
+
filename += ".mid" # extension
|
56
|
+
return File.join(result, filename)
|
57
|
+
end
|
58
|
+
|
59
|
+
def degree_name(root_note_name, root_midi_value)
|
60
|
+
root_octave_value = Fet::MidiNote.new(root_midi_value).octave_number
|
61
|
+
degrees_instance = Fet::Degrees.new(root_name: root_note_name, octave_value: root_octave_value)
|
62
|
+
degree_name = degrees_instance.degree_names_of_midi_value(midi_value).last
|
63
|
+
return degree_name
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "midi_note"
|
4
|
+
|
5
|
+
module Fet
|
6
|
+
MAJOR_ROOT_MIDI_VALUES = {
|
7
|
+
"C#" => MidiNote.from_note("C#", 4).midi_value,
|
8
|
+
"F#" => MidiNote.from_note("F#", 3).midi_value,
|
9
|
+
"B" => MidiNote.from_note("B", 3).midi_value,
|
10
|
+
"E" => MidiNote.from_note("E", 3).midi_value,
|
11
|
+
"A" => MidiNote.from_note("A", 3).midi_value,
|
12
|
+
"D" => MidiNote.from_note("D", 4).midi_value,
|
13
|
+
"G" => MidiNote.from_note("G", 3).midi_value,
|
14
|
+
"C" => MidiNote.from_note("C", 4).midi_value,
|
15
|
+
"F" => MidiNote.from_note("F", 3).midi_value,
|
16
|
+
"Bb" => MidiNote.from_note("Bb", 3).midi_value,
|
17
|
+
"Eb" => MidiNote.from_note("Eb", 4).midi_value,
|
18
|
+
"Ab" => MidiNote.from_note("Ab", 3).midi_value,
|
19
|
+
"Db" => MidiNote.from_note("Db", 4).midi_value,
|
20
|
+
"Gb" => MidiNote.from_note("Gb", 3).midi_value,
|
21
|
+
"Cb" => MidiNote.from_note("Cb", 3).midi_value,
|
22
|
+
}.deep_freeze
|
23
|
+
MINOR_ROOT_MIDI_VALUES = MusicTheory::MINOR_KEYS.zip(MAJOR_ROOT_MIDI_VALUES.values.map { |i| i - 3 }).to_h.deep_freeze
|
24
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "midi_note"
|
4
|
+
|
5
|
+
module Fet
|
6
|
+
PIANO_RANGE = (MidiNote.from_note("A", 0).midi_value..MidiNote.from_note("C", 8).midi_value).to_a.deep_freeze
|
7
|
+
REDUCED_BY_OCTAVE_PIANO_RANGE = (MidiNote.from_note("A", 1).midi_value..MidiNote.from_note("C", 7).midi_value).to_a.deep_freeze
|
8
|
+
GUITAR_RANGE = (MidiNote.from_note("E", 2).midi_value..MidiNote.from_note("E", 6).midi_value).to_a.deep_freeze
|
9
|
+
# Ranges according to Wikipedia:
|
10
|
+
# Bass: E2 - E4
|
11
|
+
# Baritone: G2 - G4
|
12
|
+
# Tenor: C3 - C5
|
13
|
+
# Countertenor: E3 - E5
|
14
|
+
# Contralto: F3 - F5
|
15
|
+
# Mezzo-soprano: A3 - A5
|
16
|
+
# Soprano: C4 - C6
|
17
|
+
LOW_SINGING_OCTAVE_RANGE = (MidiNote.from_note("D", 3).midi_value...MidiNote.from_note("D", 4).midi_value).to_a.deep_freeze
|
18
|
+
HIGH_SINGING_OCTAVE_RANGE = (MidiNote.from_note("D", 4).midi_value...MidiNote.from_note("D", 5).midi_value).to_a.deep_freeze
|
19
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "music_theory"
|
4
|
+
|
5
|
+
module Fet
|
6
|
+
# Class that handles MIDI values that represent notes
|
7
|
+
class MidiNote
|
8
|
+
attr_reader :midi_value
|
9
|
+
|
10
|
+
def initialize(midi_value)
|
11
|
+
self.midi_value = midi_value
|
12
|
+
validate_midi_value!
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.from_note(note_name, octave_number)
|
16
|
+
midi_value_for_c = 12 * (1 + octave_number)
|
17
|
+
midi_value = midi_value_for_c + MusicTheory.semitones_from_c(note_name)
|
18
|
+
return new(midi_value)
|
19
|
+
end
|
20
|
+
|
21
|
+
def octave_number
|
22
|
+
return (midi_value - 12) / 12
|
23
|
+
end
|
24
|
+
|
25
|
+
# NOTE: This is not strictly correct because e.g. the midi value of 63 can be D#4 or Eb4, which affects
|
26
|
+
# what degree it actually is. However, without additional information, this is good enough.
|
27
|
+
def degree(root_midi_value)
|
28
|
+
return (midi_value - root_midi_value) % 12
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_writer :midi_value
|
34
|
+
|
35
|
+
def validate_midi_value!
|
36
|
+
# In MIDI, the minimum note value is 0 (which is C(-1)) and the maximum note value is 127 (which is G(9))
|
37
|
+
raise InvalidMidiNote.new(midi_value) if midi_value.negative? || midi_value > 127
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "midilib/sequence"
|
4
|
+
require "midilib/consts"
|
5
|
+
|
6
|
+
module Fet
|
7
|
+
# Interface with the midilib library to generate MIDI files
|
8
|
+
class MidilibInterface
|
9
|
+
def initialize(tempo:, progression:, notes:, info:, filename:)
|
10
|
+
self.tempo = tempo
|
11
|
+
self.progression = progression
|
12
|
+
self.notes = notes
|
13
|
+
self.info = info
|
14
|
+
self.filename = filename
|
15
|
+
self.sequence = MIDI::Sequence.new
|
16
|
+
self.track = generate_instrument_track
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_listening_midi_file
|
20
|
+
# Play the chord progression
|
21
|
+
set_progression_on_track
|
22
|
+
|
23
|
+
add_rest(2 * quarter_note_length)
|
24
|
+
play_notes_as_chord(notes, quarter_note_length)
|
25
|
+
|
26
|
+
add_rest(6 * quarter_note_length)
|
27
|
+
play_notes_sequentially(notes, quarter_note_length)
|
28
|
+
|
29
|
+
write_sequence_to_file
|
30
|
+
end
|
31
|
+
|
32
|
+
def create_singing_midi_file(sleep_duration)
|
33
|
+
# Play the chord progression
|
34
|
+
set_progression_on_track
|
35
|
+
|
36
|
+
# Play the note after waiting for a specified amount of time
|
37
|
+
add_seconds_of_rest(sleep_duration) do
|
38
|
+
play_notes_sequentially(notes, quarter_note_length)
|
39
|
+
end
|
40
|
+
|
41
|
+
write_sequence_to_file
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
attr_accessor :tempo, :progression, :notes, :info, :filename, :sequence, :track
|
47
|
+
|
48
|
+
def write_sequence_to_file
|
49
|
+
directory_name = File.dirname(filename)
|
50
|
+
FileUtils.mkdir_p(directory_name)
|
51
|
+
File.open(filename, "wb") { |file| sequence.write(file) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_seconds_of_rest(seconds)
|
55
|
+
# Change tempo to 60 so that sleep_duration quarter notes corresponds to number of seconds
|
56
|
+
with_temporary_tempo_change(60) do
|
57
|
+
# Sleep for the requested duration
|
58
|
+
add_rest(seconds * quarter_note_length)
|
59
|
+
yield
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def play_notes_as_chord(the_notes, rest_after_chord)
|
64
|
+
the_notes.each do |note|
|
65
|
+
track.events << MIDI::NoteOn.new(0, note, 127, 0) # track number, note, volume, time to add
|
66
|
+
end
|
67
|
+
|
68
|
+
the_notes.each.with_index do |note, index|
|
69
|
+
time_interval = index.zero? ? rest_after_chord : 0
|
70
|
+
track.events << MIDI::NoteOff.new(0, note, 127, time_interval)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def play_notes_sequentially(the_notes, rest_between_notes)
|
75
|
+
the_notes.each do |note|
|
76
|
+
track.events << MIDI::NoteOn.new(0, note, 127, 0) # track number, note, volume, time to add
|
77
|
+
track.events << MIDI::NoteOff.new(0, note, 127, rest_between_notes)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def add_rest(duration)
|
82
|
+
track.events << MIDI::NoteOff.new(0, 0, 0, duration)
|
83
|
+
end
|
84
|
+
|
85
|
+
def with_temporary_tempo_change(new_tempo)
|
86
|
+
change_tempo(new_tempo)
|
87
|
+
yield
|
88
|
+
change_tempo(tempo)
|
89
|
+
end
|
90
|
+
|
91
|
+
def change_tempo(new_tempo)
|
92
|
+
track.events << MIDI::Tempo.new(MIDI::Tempo.bpm_to_mpq(new_tempo))
|
93
|
+
end
|
94
|
+
|
95
|
+
def generate_instrument_track
|
96
|
+
create_info_track
|
97
|
+
return create_instrument_track
|
98
|
+
end
|
99
|
+
|
100
|
+
def set_progression_on_track
|
101
|
+
# Create the progression
|
102
|
+
progression.each do |chord|
|
103
|
+
play_notes_as_chord(chord, quarter_note_length)
|
104
|
+
end
|
105
|
+
|
106
|
+
return track
|
107
|
+
end
|
108
|
+
|
109
|
+
def create_info_track
|
110
|
+
# Create a first track for the sequence. This holds tempo events and meta info.
|
111
|
+
track = MIDI::Track.new(sequence)
|
112
|
+
sequence.tracks << track
|
113
|
+
track.events << MIDI::Tempo.new(MIDI::Tempo.bpm_to_mpq(tempo))
|
114
|
+
track.events << MIDI::MetaEvent.new(MIDI::META_SEQ_NAME, info)
|
115
|
+
return track
|
116
|
+
end
|
117
|
+
|
118
|
+
def create_instrument_track
|
119
|
+
# Create a track to hold the notes. Add it to the sequence.
|
120
|
+
track = MIDI::Track.new(sequence)
|
121
|
+
sequence.tracks << track
|
122
|
+
track.name = info
|
123
|
+
track.instrument = MIDI::GM_PATCH_NAMES[0] # This is the piano patch
|
124
|
+
track.events << MIDI::ProgramChange.new(0, 1, 0) # Specify instrument as 2nd argument - see consts in midilib
|
125
|
+
return track
|
126
|
+
end
|
127
|
+
|
128
|
+
def quarter_note_length
|
129
|
+
# NOTE: magic constant will freeze strings, but apparently midilib needs "quarter" to NOT be frozen,
|
130
|
+
# so calling dup on it will generate a non-frozen version
|
131
|
+
return sequence.note_to_delta("quarter".dup)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "note"
|
4
|
+
|
5
|
+
module Fet
|
6
|
+
# Module in charge of handling music theory concepts
|
7
|
+
module MusicTheory
|
8
|
+
# All 12 degrees
|
9
|
+
DEGREES = ["1", "b2", "2", "b3", "3", "4", "b5", "5", "b6", "6", "b7", "7"].deep_freeze
|
10
|
+
|
11
|
+
# All 15 keys - 7 sharps...0 accidentals...7 flats.
|
12
|
+
MAJOR_KEYS = ["C#", "F#", "B", "E", "A", "D", "G", "C", "F", "Bb", "Eb", "Ab", "Db", "Gb", "Cb"].deep_freeze
|
13
|
+
MINOR_KEYS = ["A#", "D#", "G#", "C#", "F#", "B", "E", "A", "D", "G", "C", "F", "Bb", "Eb", "Ab"].deep_freeze
|
14
|
+
|
15
|
+
MODES_IN_ORDER_OF_DARKNESS = [
|
16
|
+
["lydian"],
|
17
|
+
["major", "ionian"],
|
18
|
+
["mixolydian"],
|
19
|
+
["dorian"],
|
20
|
+
["minor", "aeolian"],
|
21
|
+
["phrygian"],
|
22
|
+
["locrian"],
|
23
|
+
].deep_freeze
|
24
|
+
|
25
|
+
DEGREES_OF_MODE = {
|
26
|
+
["lydian"] => ["1", "2", "3", "#4", "5", "6", "7"],
|
27
|
+
["major", "ionian"] => ["1", "2", "3", "4", "5", "6", "7"],
|
28
|
+
["mixolydian"] => ["1", "2", "3", "4", "5", "6", "b7"],
|
29
|
+
["dorian"] => ["1", "2", "b3", "4", "5", "6", "b7"],
|
30
|
+
["minor", "aeolian"] => ["1", "2", "b3", "4", "5", "b6", "b7"],
|
31
|
+
["phrygian"] => ["1", "b2", "b3", "4", "5", "b6", "b7"],
|
32
|
+
["locrian"] => ["1", "b2", "b3", "4", "b5", "b6", "b7"],
|
33
|
+
}.deep_freeze
|
34
|
+
|
35
|
+
SEMITONES_FROM_C = {
|
36
|
+
"C" => 0,
|
37
|
+
"D" => 2,
|
38
|
+
"E" => 4,
|
39
|
+
"F" => 5,
|
40
|
+
"G" => 7,
|
41
|
+
"A" => 9,
|
42
|
+
"B" => 11,
|
43
|
+
}.deep_freeze
|
44
|
+
|
45
|
+
# NOTE: returns value from 0 to 11
|
46
|
+
def self.semitones_from_c(note_name)
|
47
|
+
note = Note.new(note_name)
|
48
|
+
return (SEMITONES_FROM_C[note.natural_note] + note.accidental_to_semitone_offset) % 12
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.degrees_of_mode(mode_name)
|
52
|
+
mode_info = DEGREES_OF_MODE.detect { |mode_names, _| mode_names.include?(mode_name) }
|
53
|
+
raise InvalidModeName.new(mode_name) if mode_info.nil?
|
54
|
+
|
55
|
+
return mode_info[1]
|
56
|
+
end
|
57
|
+
|
58
|
+
CIRCLE_OF_FIFTHS_WITHOUT_ACCIDENTALS = ["F", "C", "G", "D", "A", "E", "B"].deep_freeze
|
59
|
+
CIRCLE_OF_FIFTHS = [
|
60
|
+
*CIRCLE_OF_FIFTHS_WITHOUT_ACCIDENTALS.map { |note| Note.new(note).flattened_note.flattened_note.flattened_note.full_note },
|
61
|
+
*CIRCLE_OF_FIFTHS_WITHOUT_ACCIDENTALS.map { |note| Note.new(note).flattened_note.flattened_note.full_note },
|
62
|
+
*CIRCLE_OF_FIFTHS_WITHOUT_ACCIDENTALS.map { |note| Note.new(note).flattened_note.full_note },
|
63
|
+
*CIRCLE_OF_FIFTHS_WITHOUT_ACCIDENTALS,
|
64
|
+
*CIRCLE_OF_FIFTHS_WITHOUT_ACCIDENTALS.map { |note| Note.new(note).sharpened_note.full_note },
|
65
|
+
*CIRCLE_OF_FIFTHS_WITHOUT_ACCIDENTALS.map { |note| Note.new(note).sharpened_note.sharpened_note.full_note },
|
66
|
+
*CIRCLE_OF_FIFTHS_WITHOUT_ACCIDENTALS.map { |note| Note.new(note).sharpened_note.sharpened_note.sharpened_note.full_note },
|
67
|
+
].deep_freeze
|
68
|
+
|
69
|
+
ORDERED_NATURAL_NOTES = CIRCLE_OF_FIFTHS_WITHOUT_ACCIDENTALS.sort.deep_freeze
|
70
|
+
SEMITONES_TO_NEXT_NATURAL_NOTE = {
|
71
|
+
"A" => 2,
|
72
|
+
"B" => 1,
|
73
|
+
"C" => 2,
|
74
|
+
"D" => 2,
|
75
|
+
"E" => 1,
|
76
|
+
"F" => 2,
|
77
|
+
"G" => 2,
|
78
|
+
}.deep_freeze
|
79
|
+
|
80
|
+
# A aeolian -> ["A", "B", "C", "D", "E", "F", "G"]
|
81
|
+
def self.notes_of_mode(note_name, mode_name)
|
82
|
+
relative_major_note_name = relative_major(note_name, mode_name)
|
83
|
+
|
84
|
+
index = CIRCLE_OF_FIFTHS.index(relative_major_note_name)
|
85
|
+
result = CIRCLE_OF_FIFTHS[(index - 1)..(index + 5)]
|
86
|
+
raise UnsupportedRootName.new(note_name) unless result.size == 7
|
87
|
+
|
88
|
+
result = result.sort
|
89
|
+
return result.rotate(result.index(note_name))
|
90
|
+
end
|
91
|
+
|
92
|
+
# A aeolian -> C
|
93
|
+
def self.relative_major(note_name, mode_name)
|
94
|
+
note = Note.new(note_name)
|
95
|
+
index = CIRCLE_OF_FIFTHS.index(note.full_note)
|
96
|
+
raise UnsupportedRootName.new(note_name) if index.nil?
|
97
|
+
|
98
|
+
result_index = index - mode_offset_from_major(mode_name)
|
99
|
+
raise UnsupportedRootName.new(note_name) if result_index.negative?
|
100
|
+
|
101
|
+
result = CIRCLE_OF_FIFTHS[index - mode_offset_from_major(mode_name)]
|
102
|
+
return result
|
103
|
+
end
|
104
|
+
|
105
|
+
class << self
|
106
|
+
private
|
107
|
+
|
108
|
+
def mode_offset_from_major(mode_name)
|
109
|
+
MODES_IN_ORDER_OF_DARKNESS.each.with_index do |mode_names, index|
|
110
|
+
return (index - 1) if mode_names.include?(mode_name)
|
111
|
+
end
|
112
|
+
raise InvalidModeName.new(mode_name)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|