wtf_chord 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +22 -0
- data/lib/wtf_chord.rb +5 -0
- data/lib/wtf_chord/chord.rb +25 -4
- data/lib/wtf_chord/cli.rb +10 -3
- data/lib/wtf_chord/collectors.rb +10 -0
- data/lib/wtf_chord/collectors/generic.rb +33 -0
- data/lib/wtf_chord/collectors/guitar.rb +116 -0
- data/lib/wtf_chord/collectors/piano.rb +36 -0
- data/lib/wtf_chord/fingering.rb +4 -0
- data/lib/wtf_chord/formatter.rb +2 -0
- data/lib/wtf_chord/formatters/base.rb +5 -3
- data/lib/wtf_chord/formatters/piano.rb +7 -11
- data/lib/wtf_chord/formatters/piano1.rb +46 -0
- data/lib/wtf_chord/fretboard.rb +4 -2
- data/lib/wtf_chord/guitar_string.rb +11 -6
- data/lib/wtf_chord/instrument_pitch.rb +53 -0
- data/lib/wtf_chord/keyboard.rb +9 -36
- data/lib/wtf_chord/keyboard_presentation.rb +90 -0
- data/lib/wtf_chord/piano_key.rb +19 -0
- data/lib/wtf_chord/scale.rb +15 -5
- data/lib/wtf_chord/version.rb +1 -1
- metadata +10 -3
- data/lib/wtf_chord/fingerings_generator.rb +0 -120
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8d764a30038a11df351e6d3d9dfce09084aa89af352d92b36677ed5eac31e425
|
4
|
+
data.tar.gz: 0c713ee0f4e3b36720014f56c1c55ca6686981b49e228402d8c2f826463affce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e83f9de2c3913c4206d7d4f424f46f1082fcd16a50f8f19d4d78f2d83f3a66f610d23852ac66b890190f444db14e74d3fc9249514e6ece64626685c4c7434de2
|
7
|
+
data.tar.gz: 3ac9226adb773858a51c474c0f1b299e3bf2601ce567d9967c96b297ba4b0895da08c6b50e404221ba636be01d83740fea20b3f5684ebb24a2053b4b81235425
|
data/README.md
CHANGED
@@ -91,6 +91,28 @@ You can get simpler output, using the `--output` option:
|
|
91
91
|
[ 10 8 7 7 6 10 ]
|
92
92
|
[ 10 8 7 7 10 10 ]
|
93
93
|
|
94
|
+
Starting with 0.7.0, we have the very nice piano formatting:
|
95
|
+
|
96
|
+
$ wtfchord Fdim -n 2 --output=piano
|
97
|
+
F1 - G♯1 - B1
|
98
|
+
┌─┬─┬┬─┬─┬─┬─┬┬─┬┬─┬─┐
|
99
|
+
│ │ ││ │ │ │ ││█││ │ │
|
100
|
+
│ └┬┘└┬┘ │ └┬┘└┬┘└┬┘ │
|
101
|
+
│ │ │ │▐▌│ │ │▐▌│
|
102
|
+
└──┴──┴──┴──┴──┴──┴──┘
|
103
|
+
↑
|
104
|
+
Ⅰ
|
105
|
+
|
106
|
+
|
107
|
+
G♯1 - B1 - F2
|
108
|
+
┌─┬─┬┬─┬─┬─┬─┬┬─┬┬─┬─┬─┬─┬┬─┬─┬─┬─┬┬─┬┬─┬─┐
|
109
|
+
│ │ ││ │ │ │ ││█││ │ │ │ ││ │ │ │ ││ ││ │ │
|
110
|
+
│ └┬┘└┬┘ │ └┬┘└┬┘└┬┘ │ └┬┘└┬┘ │ └┬┘└┬┘└┬┘ │
|
111
|
+
│ │ │ │ │ │ │▐▌│ │ │ │▐▌│ │ │ │
|
112
|
+
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
|
113
|
+
↑ ↑
|
114
|
+
Ⅰ Ⅱ
|
115
|
+
|
94
116
|
## Development
|
95
117
|
|
96
118
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. Run `bundle exec wtf_chord` to use the gem in this directory, ignoring other installed copies of this gem.
|
data/lib/wtf_chord.rb
CHANGED
@@ -1,8 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "wtf_chord/scale"
|
2
4
|
require "wtf_chord/rules"
|
3
5
|
require "wtf_chord/version"
|
4
6
|
|
5
7
|
module WTFChord
|
8
|
+
autoload :Fretboard, "wtf_chord/fretboard"
|
9
|
+
autoload :Keyboard, "wtf_chord/keyboard"
|
10
|
+
|
6
11
|
DEFAULTS = {
|
7
12
|
:rules_file => File.expand_path("../wtf_chord.yml", __FILE__)
|
8
13
|
}.freeze
|
data/lib/wtf_chord/chord.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'wtf_chord/fingering'
|
2
|
-
require 'wtf_chord/
|
4
|
+
require 'wtf_chord/collectors'
|
3
5
|
|
4
6
|
module WTFChord
|
5
7
|
class Chord
|
@@ -9,7 +11,7 @@ module WTFChord
|
|
9
11
|
|
10
12
|
def initialize(note, name)
|
11
13
|
@pitch = note.is_a?(Pitch) ? note : WTFChord.note(note)
|
12
|
-
@name = "#{@pitch.key}#{name}"
|
14
|
+
@name = "#{@pitch.key}#{name}"
|
13
15
|
@steps = Array(WTFChord.rules[name])
|
14
16
|
@notes = @steps.map { |dist| (@pitch + dist).note }.tap(&:uniq!)
|
15
17
|
|
@@ -18,8 +20,12 @@ module WTFChord
|
|
18
20
|
end
|
19
21
|
end
|
20
22
|
|
21
|
-
def fingerings(limit = nil)
|
22
|
-
|
23
|
+
def fingerings(limit = nil, collector: Collectors::Guitar)
|
24
|
+
collector.new(self).call[0, limit || 5]
|
25
|
+
end
|
26
|
+
|
27
|
+
def tone
|
28
|
+
bass || original_bass
|
23
29
|
end
|
24
30
|
|
25
31
|
def third_tone
|
@@ -30,10 +36,25 @@ module WTFChord
|
|
30
36
|
!!@bass
|
31
37
|
end
|
32
38
|
|
39
|
+
def size
|
40
|
+
bass? ? -~@notes.size : @notes.size
|
41
|
+
end
|
42
|
+
|
33
43
|
def original_bass
|
34
44
|
@notes[0]
|
35
45
|
end
|
36
46
|
|
47
|
+
def === other
|
48
|
+
case other
|
49
|
+
when InstrumentPitch
|
50
|
+
self === other.note
|
51
|
+
when Note
|
52
|
+
notes.include?(other) || (bass? && bass == other)
|
53
|
+
else
|
54
|
+
super
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
37
58
|
def inspect
|
38
59
|
"#{name} (#{@notes.map(&:key) * ' - '})"
|
39
60
|
end
|
data/lib/wtf_chord/cli.rb
CHANGED
@@ -12,10 +12,17 @@ module WTFChord
|
|
12
12
|
|
13
13
|
debug { "Output using formatter: #{formatter.formatter.to_s}\n" }
|
14
14
|
|
15
|
-
options['amount'] = 1 if options['output'] == '
|
15
|
+
options['amount'] = 1 if options['output'] == 'piano1'
|
16
16
|
|
17
|
-
|
18
|
-
|
17
|
+
collector =
|
18
|
+
case formatter.name when 'piano', 'piano1'
|
19
|
+
Collectors::Piano
|
20
|
+
else
|
21
|
+
Collectors::Guitar
|
22
|
+
end
|
23
|
+
|
24
|
+
fingerings = chord.fingerings(options['amount'], collector: collector)
|
25
|
+
puts formatter[*fingerings].uniq * formatter.separator
|
19
26
|
end
|
20
27
|
|
21
28
|
version VERSION
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WTFChord
|
4
|
+
class Chord
|
5
|
+
end
|
6
|
+
|
7
|
+
module Collectors
|
8
|
+
class Generic < DelegateClass(Chord)
|
9
|
+
singleton_class.attr_accessor :max_dist
|
10
|
+
|
11
|
+
def fingerings
|
12
|
+
@fingerings ||= []
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
reset!
|
17
|
+
collect!
|
18
|
+
end
|
19
|
+
|
20
|
+
def reset!
|
21
|
+
fingerings.clear
|
22
|
+
end
|
23
|
+
|
24
|
+
def collect!
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
def max_dist
|
29
|
+
self.class.max_dist
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WTFChord
|
4
|
+
module Collectors
|
5
|
+
class Guitar < Generic
|
6
|
+
self.max_dist = 5
|
7
|
+
|
8
|
+
MAX_FRET = 7
|
9
|
+
|
10
|
+
def collect!
|
11
|
+
(0...MAX_FRET).each do |from|
|
12
|
+
to = from + max_dist
|
13
|
+
generate(from...to) do |variant|
|
14
|
+
fingerings << set_bass(variant) if filter_variant(variant)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
fingerings.uniq!
|
19
|
+
fingerings.sort_by!(&:complexity)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def generate(fret_range)
|
25
|
+
combinations = WTFChord.guitar.strings.map.with_index do |s, index|
|
26
|
+
fret_range.
|
27
|
+
select { |dist| pitch = s.original + dist; has_note?(pitch) }.
|
28
|
+
tap { |frets| frets << nil if frets.size == 0 }
|
29
|
+
end
|
30
|
+
|
31
|
+
combinations.inject(&:product).each do |fingers|
|
32
|
+
fingers.flatten!
|
33
|
+
|
34
|
+
Fingering.new(WTFChord.guitar, fingers) do |variant|
|
35
|
+
adjust(variant)
|
36
|
+
yield(variant)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def filter_variant(variant)
|
42
|
+
used_strings = variant.used_strings
|
43
|
+
used_notes = used_strings.map(&:note)
|
44
|
+
tones_count = notes.map { |n| used_notes.count(n) }
|
45
|
+
|
46
|
+
!fingerings.include?(variant) &&
|
47
|
+
basetone?(used_strings[0]) &&
|
48
|
+
(
|
49
|
+
(third?(used_strings[1]) || third?(used_strings[2])) ||
|
50
|
+
(last?(used_strings[1]) || last?(used_strings[2]))
|
51
|
+
) &&
|
52
|
+
all_notes?(used_notes) &&
|
53
|
+
used_notes.each_cons(2).none? { |(l, r)| l == r } &&
|
54
|
+
tones_count.all? { |n| tones_count[0] >= n || tones_count[notes.index(third_tone)] >= n } &&
|
55
|
+
variant.complexity <= 2.25
|
56
|
+
end
|
57
|
+
|
58
|
+
def adjust(fingering)
|
59
|
+
while (string = fingering.used_strings[0]) && !basetone?(string)
|
60
|
+
string.dead if !string.dead?
|
61
|
+
break if fingering.used_strings.size == 4
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def set_bass(variant)
|
66
|
+
if bass?
|
67
|
+
variant.find_used_string_for(original_bass) do |idx, bass_string|
|
68
|
+
try_set_bass_on(variant, idx) or begin
|
69
|
+
4.times do |i|
|
70
|
+
next if i == idx
|
71
|
+
if try_set_bass_on(variant, i)
|
72
|
+
bass_string.dead
|
73
|
+
break
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
variant
|
81
|
+
end
|
82
|
+
|
83
|
+
def try_set_bass_on(variant, idx)
|
84
|
+
bass_string = variant.strings[idx].dup
|
85
|
+
distance = bass_string.distance_to(bass)
|
86
|
+
|
87
|
+
if distance >= 0 && distance < 5 && (distance - variant.min_fret) > -3
|
88
|
+
variant.extra_complexity += 0.5 if distance < variant.min_fret
|
89
|
+
variant.strings[idx].hold_on(distance)
|
90
|
+
true
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def all_notes?(used_notes)
|
95
|
+
notes.all? { |n| used_notes.include?(n) }
|
96
|
+
end
|
97
|
+
|
98
|
+
def has_note?(value)
|
99
|
+
value = value.note if !value.is_a?(Note) && value.respond_to?(:note)
|
100
|
+
notes.include?(value)
|
101
|
+
end
|
102
|
+
|
103
|
+
def basetone?(string)
|
104
|
+
string && string.note == notes[0]
|
105
|
+
end
|
106
|
+
|
107
|
+
def third?(string)
|
108
|
+
string && (string.note == third_tone)
|
109
|
+
end
|
110
|
+
|
111
|
+
def last?(string)
|
112
|
+
string && (string.note == notes[-1])
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WTFChord
|
4
|
+
module Collectors
|
5
|
+
class Piano < Generic
|
6
|
+
self.max_dist = 12
|
7
|
+
MAX_OCTAVE = 5
|
8
|
+
|
9
|
+
CheckDist = -> ((a, b)) { (a.to_i - b.to_i).abs <= max_dist }
|
10
|
+
|
11
|
+
FilterProc = -> (actual_size, notes) {
|
12
|
+
notes.uniq(&:key).size == actual_size && notes.sort.each_cons(2).all?(&CheckDist)
|
13
|
+
}.curry(2).freeze
|
14
|
+
|
15
|
+
Distances = -> (notes) {
|
16
|
+
notes.each_cons(2).map { |(a, b)| (a.to_i - b.to_i).abs }
|
17
|
+
}
|
18
|
+
|
19
|
+
FirstTone = -> (tone, notes) {
|
20
|
+
Distances[notes]
|
21
|
+
}.curry(2).freeze
|
22
|
+
|
23
|
+
def collect!
|
24
|
+
@keyboard = Keyboard.new(1..MAX_OCTAVE)
|
25
|
+
|
26
|
+
@fingerings = @keyboard.keys.
|
27
|
+
grep(__getobj__).
|
28
|
+
combination(size).
|
29
|
+
filter(&FilterProc[size])
|
30
|
+
@fingerings.uniq! { |x| x.map(&:key) }
|
31
|
+
@fingerings.sort_by!(&FirstTone[tone])
|
32
|
+
@fingerings
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/wtf_chord/fingering.rb
CHANGED
data/lib/wtf_chord/formatter.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'forwardable'
|
2
4
|
|
3
5
|
module WTFChord
|
@@ -9,7 +11,7 @@ module WTFChord
|
|
9
11
|
def_delegators :@fret, :strings
|
10
12
|
|
11
13
|
def self.separator
|
12
|
-
"\n"
|
14
|
+
"\n"
|
13
15
|
end
|
14
16
|
|
15
17
|
def self.to_proc
|
@@ -31,8 +33,8 @@ module WTFChord
|
|
31
33
|
def draw
|
32
34
|
end
|
33
35
|
|
34
|
-
def
|
35
|
-
strings.reject(&:dead?)
|
36
|
+
def actual_strings
|
37
|
+
strings.reject(&:dead?)
|
36
38
|
end
|
37
39
|
|
38
40
|
def with_rates?
|
@@ -1,24 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "wtf_chord/formatters/piano1"
|
3
4
|
require "wtf_chord/keyboard"
|
4
5
|
|
5
6
|
module WTFChord
|
6
7
|
module Formatters
|
7
|
-
class Piano <
|
8
|
+
class Piano < Piano1
|
8
9
|
def draw
|
9
10
|
[
|
10
|
-
|
11
|
-
|
12
|
-
].join("\n
|
11
|
+
pressed_keys.map(&:to_s).join(" - "),
|
12
|
+
keyboard_presentation
|
13
|
+
].join("\n")
|
13
14
|
end
|
14
15
|
|
15
|
-
def
|
16
|
-
|
17
|
-
unless positions.include?(string.note.position)
|
18
|
-
positions << string.note.position
|
19
|
-
notes << string.note
|
20
|
-
end
|
21
|
-
end[1]
|
16
|
+
def keyboard_presentation
|
17
|
+
KeyboardPresentation.press(*pressed_keys)
|
22
18
|
end
|
23
19
|
end
|
24
20
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "wtf_chord/keyboard_presentation"
|
4
|
+
|
5
|
+
module WTFChord
|
6
|
+
module Formatters
|
7
|
+
class Piano1
|
8
|
+
def self.separator
|
9
|
+
"\n"
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.to_proc
|
13
|
+
proc { |keys| new(keys).draw }
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.with_rates(*)
|
17
|
+
yield(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :pressed_keys
|
21
|
+
|
22
|
+
def initialize(keys)
|
23
|
+
@pressed_keys = keys
|
24
|
+
end
|
25
|
+
|
26
|
+
def draw
|
27
|
+
[
|
28
|
+
unique_notes.sort.join(" - "),
|
29
|
+
keyboard_presentation
|
30
|
+
].join("\n\n")
|
31
|
+
end
|
32
|
+
|
33
|
+
def actual_notes
|
34
|
+
actual_strings.map(&:note)
|
35
|
+
end
|
36
|
+
|
37
|
+
def unique_notes
|
38
|
+
actual_notes.uniq(&:position)
|
39
|
+
end
|
40
|
+
|
41
|
+
def keyboard_presentation
|
42
|
+
KeyboardPresentation.press(*unique_notes.map(&:position))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/wtf_chord/fretboard.rb
CHANGED
@@ -1,14 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'wtf_chord/guitar_string'
|
2
4
|
|
3
5
|
module WTFChord
|
4
6
|
class Fretboard
|
5
|
-
DEAD = "×"
|
7
|
+
DEAD = "×"
|
6
8
|
|
7
9
|
attr_reader :strings, :capo
|
8
10
|
|
9
11
|
def initialize(*strings)
|
10
12
|
@capo = 0
|
11
|
-
@strings = strings.map
|
13
|
+
@strings = strings.map { |string| GuitarString.new(string, @capo) }
|
12
14
|
end
|
13
15
|
|
14
16
|
def [] idx
|
@@ -1,14 +1,19 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'wtf_chord/instrument_pitch'
|
2
4
|
|
3
5
|
module WTFChord
|
4
|
-
class GuitarString <
|
6
|
+
class GuitarString < InstrumentPitch
|
5
7
|
attr_reader :capo, :fret, :original
|
6
8
|
|
7
9
|
def initialize(note, capo = 0)
|
8
|
-
@original = WTFChord.note(note)
|
9
10
|
@fret = nil
|
10
11
|
@capo = capo
|
11
|
-
super(
|
12
|
+
super(note)
|
13
|
+
end
|
14
|
+
|
15
|
+
def init_pitch
|
16
|
+
@original + capo
|
12
17
|
end
|
13
18
|
|
14
19
|
def initialize_dup(other)
|
@@ -58,8 +63,8 @@ module WTFChord
|
|
58
63
|
def <=> other
|
59
64
|
return if dead?
|
60
65
|
case other
|
61
|
-
when Integer
|
62
|
-
when GuitarString
|
66
|
+
when Integer then @fret <=> other
|
67
|
+
when GuitarString then @fret <=> other.fret
|
63
68
|
end
|
64
69
|
end
|
65
70
|
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'wtf_chord/pitch'
|
4
|
+
|
5
|
+
module WTFChord
|
6
|
+
class InstrumentPitch < DelegateClass(Pitch)
|
7
|
+
attr_reader :interval, :original
|
8
|
+
|
9
|
+
def initialize(note)
|
10
|
+
@original = WTFChord.note(note)
|
11
|
+
super(init_pitch)
|
12
|
+
end
|
13
|
+
|
14
|
+
def init_pitch
|
15
|
+
@original
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize_dup(other)
|
19
|
+
super
|
20
|
+
__setobj__(@original.dup)
|
21
|
+
end
|
22
|
+
|
23
|
+
def dead
|
24
|
+
end
|
25
|
+
|
26
|
+
def dead?
|
27
|
+
raise NotImplementedError
|
28
|
+
end
|
29
|
+
|
30
|
+
def holded?
|
31
|
+
raise NotImplementedError
|
32
|
+
end
|
33
|
+
|
34
|
+
def distance_to(pitch)
|
35
|
+
pos = 0
|
36
|
+
pos += 1 while (original + pos) != pitch
|
37
|
+
pos
|
38
|
+
end
|
39
|
+
|
40
|
+
def code
|
41
|
+
to_i
|
42
|
+
end
|
43
|
+
|
44
|
+
def <=> other
|
45
|
+
case other
|
46
|
+
when Integer
|
47
|
+
to_i <=> other
|
48
|
+
when InstrumentPitch
|
49
|
+
to_i <=> other.to_i
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/wtf_chord/keyboard.rb
CHANGED
@@ -1,47 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "wtf_chord/piano_key"
|
4
|
+
|
3
5
|
module WTFChord
|
4
6
|
class Keyboard
|
5
|
-
|
6
|
-
┌─┬─┬┬─┬─┬─┬─┬┬─┬┬─┬─┐
|
7
|
-
│ │ ││ │ │ │ ││ ││ │ │
|
8
|
-
│ └┬┘└┬┘ │ └┬┘└┬┘└┬┘ │
|
9
|
-
│ │ │ │ │ │ │ │
|
10
|
-
└──┴──┴──┴──┴──┴──┴──┘
|
11
|
-
EOF
|
7
|
+
attr_reader :keys
|
12
8
|
|
13
|
-
|
14
|
-
class Key
|
15
|
-
singleton_class.attr_accessor :select_symbol
|
16
|
-
singleton_class.alias_method :[], :new
|
9
|
+
KEYS = %w(C C# D D# E F F# G G# A Bb B)
|
17
10
|
|
18
|
-
|
19
|
-
|
20
|
-
frame
|
21
|
-
end
|
11
|
+
def initialize(octaves = 1..5)
|
12
|
+
@keys = octaves.to_a.product(KEYS).map { |(o, k)| PianoKey.new("#{k}#{o}") }
|
22
13
|
end
|
23
14
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
KEYS = {
|
28
|
-
1 => W[70...72],
|
29
|
-
2 => B[26...27],
|
30
|
-
3 => W[73...75],
|
31
|
-
4 => B[29...30],
|
32
|
-
5 => W[76...78],
|
33
|
-
6 => W[79...81],
|
34
|
-
7 => B[35...36],
|
35
|
-
8 => W[82...84],
|
36
|
-
9 => B[38...39],
|
37
|
-
10 => W[85...87],
|
38
|
-
11 => B[41...42],
|
39
|
-
12 => W[88...90]
|
40
|
-
}
|
41
|
-
|
42
|
-
def self.press(*positions)
|
43
|
-
positions.each_with_object(FRAME.dup) do |pos, frame|
|
44
|
-
KEYS.fetch(pos).select(frame)
|
15
|
+
def [] idx
|
16
|
+
case idx
|
17
|
+
when 1..@keys.size then @keys[~-idx]
|
45
18
|
end
|
46
19
|
end
|
47
20
|
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'wtf_chord/helpers/roman_numbers_helper'
|
4
|
+
|
5
|
+
module WTFChord
|
6
|
+
class KeyboardPresentation
|
7
|
+
FRAME = <<~EOF
|
8
|
+
┌─┬─┬┬─┬─┬─┬─┬┬─┬┬─┬─┐┬
|
9
|
+
│ │ ││ │ │ │ ││ ││ │ ││
|
10
|
+
│ └┬┘└┬┘ │ └┬┘└┬┘└┬┘ ││
|
11
|
+
│ │ │ │ │ │ │ ││
|
12
|
+
└──┴──┴──┴──┴──┴──┴──┘┴
|
13
|
+
↑ |
|
14
|
+
%s |
|
15
|
+
|
|
16
|
+
EOF
|
17
|
+
|
18
|
+
include RomanNumbersHelper
|
19
|
+
|
20
|
+
KeyStruct = Struct.new(:index) do
|
21
|
+
singleton_class.attr_accessor :mark
|
22
|
+
singleton_class.alias_method :[], :new
|
23
|
+
|
24
|
+
def call(frame)
|
25
|
+
frame[index, mark.length] = mark
|
26
|
+
end
|
27
|
+
|
28
|
+
private def mark
|
29
|
+
self.class.mark
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
Key = -> (mark) { Class.new(KeyStruct).tap { |c| c.mark = mark } }
|
34
|
+
W, B = Key["▐▌"], Key["█"]
|
35
|
+
|
36
|
+
KEYS = [W[73], B[27], W[76], B[30], W[79], W[82], B[36], W[85], B[39], W[88], B[42], W[91]].
|
37
|
+
map.with_index(1).to_h.invert.freeze
|
38
|
+
|
39
|
+
def self.press(*positions)
|
40
|
+
if positions.all? { |v| Integer === v }
|
41
|
+
new.press(*positions)
|
42
|
+
else
|
43
|
+
positions.uniq(&:key).group_by(&:octave).inject(nil) do |a, (octave, strings)|
|
44
|
+
pos = strings.map { |s| s.note.position }
|
45
|
+
kb = new(octave).press(*pos)
|
46
|
+
a.nil? ? kb : a + kb
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_reader :frame
|
52
|
+
|
53
|
+
def initialize(octave = 1)
|
54
|
+
@octave = octave
|
55
|
+
@frame = FRAME.dup % romanize(octave)
|
56
|
+
end
|
57
|
+
|
58
|
+
def +@
|
59
|
+
shift!
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def +(other)
|
64
|
+
pop!
|
65
|
+
@frame.replace(@frame.lines(chomp: true).zip((+other).frame.lines).map(&:join).join)
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
def press(*positions)
|
70
|
+
positions.each { |pos| KEYS.fetch(pos).(@frame) }
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_s
|
75
|
+
finalize!
|
76
|
+
end
|
77
|
+
|
78
|
+
def finalize!
|
79
|
+
@frame.gsub("|", " ").gsub(/[^\s](?=\n)/, "")
|
80
|
+
end
|
81
|
+
|
82
|
+
def pop!
|
83
|
+
@frame.replace(@frame.lines.each { |line| line[-3, 1] = "" }.join)
|
84
|
+
end
|
85
|
+
|
86
|
+
def shift!
|
87
|
+
@frame.replace(@frame.lines.each { |line| line[0, 1] = "" }.join)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "wtf_chord/instrument_pitch"
|
4
|
+
|
5
|
+
module WTFChord
|
6
|
+
class PianoKey < InstrumentPitch
|
7
|
+
def pressed?
|
8
|
+
!!@pressed
|
9
|
+
end
|
10
|
+
|
11
|
+
def dead?
|
12
|
+
!pressed?
|
13
|
+
end
|
14
|
+
|
15
|
+
def press!
|
16
|
+
@pressed = true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/wtf_chord/scale.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'wtf_chord/note'
|
2
4
|
require 'wtf_chord/pitch'
|
3
5
|
require 'wtf_chord/fretboard'
|
@@ -30,9 +32,9 @@ module WTFChord
|
|
30
32
|
|
31
33
|
private_constant :ScaleArray
|
32
34
|
|
33
|
-
FLAT ||= "b"
|
34
|
-
SHARP ||= "#"
|
35
|
-
SIGNS ||= { FLAT => "\u266D"
|
35
|
+
FLAT ||= "b"
|
36
|
+
SHARP ||= "#"
|
37
|
+
SIGNS ||= { FLAT => "\u266D", SHARP => "\u266F" }.freeze
|
36
38
|
|
37
39
|
DIATONIC = {
|
38
40
|
"C" => "Do",
|
@@ -43,7 +45,7 @@ module WTFChord
|
|
43
45
|
"A" => "La",
|
44
46
|
"B" => "Si",
|
45
47
|
"H" => "Si"
|
46
|
-
}
|
48
|
+
}
|
47
49
|
|
48
50
|
SCALE ||= begin
|
49
51
|
chromatic_scale = %W(
|
@@ -60,7 +62,7 @@ module WTFChord
|
|
60
62
|
Bb|A#
|
61
63
|
B|H
|
62
64
|
)
|
63
|
-
ScaleArray.build(*chromatic_scale)
|
65
|
+
ScaleArray.build(*chromatic_scale)
|
64
66
|
end
|
65
67
|
|
66
68
|
def self.guitar=(guitar)
|
@@ -70,4 +72,12 @@ module WTFChord
|
|
70
72
|
def self.guitar
|
71
73
|
Thread.current[:wtf_guitar] ||= Fretboard.new(*%w(E2 A2 D3 G3 B3 E4))
|
72
74
|
end
|
75
|
+
|
76
|
+
def self.piano=(piano)
|
77
|
+
Thread.current[:wtf_piano] = piano
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.piano
|
81
|
+
Thread.current[:wtf_piano] ||= Keyboard.new
|
82
|
+
end
|
73
83
|
end
|
data/lib/wtf_chord/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: wtf_chord
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Anton
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-03-
|
11
|
+
date: 2021-03-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: methadone
|
@@ -94,19 +94,26 @@ files:
|
|
94
94
|
- lib/wtf_chord.yml
|
95
95
|
- lib/wtf_chord/chord.rb
|
96
96
|
- lib/wtf_chord/cli.rb
|
97
|
+
- lib/wtf_chord/collectors.rb
|
98
|
+
- lib/wtf_chord/collectors/generic.rb
|
99
|
+
- lib/wtf_chord/collectors/guitar.rb
|
100
|
+
- lib/wtf_chord/collectors/piano.rb
|
97
101
|
- lib/wtf_chord/complexity_counter.rb
|
98
102
|
- lib/wtf_chord/fingering.rb
|
99
|
-
- lib/wtf_chord/fingerings_generator.rb
|
100
103
|
- lib/wtf_chord/formatter.rb
|
101
104
|
- lib/wtf_chord/formatters/base.rb
|
102
105
|
- lib/wtf_chord/formatters/default.rb
|
103
106
|
- lib/wtf_chord/formatters/piano.rb
|
107
|
+
- lib/wtf_chord/formatters/piano1.rb
|
104
108
|
- lib/wtf_chord/formatters/simple.rb
|
105
109
|
- lib/wtf_chord/fretboard.rb
|
106
110
|
- lib/wtf_chord/guitar_string.rb
|
107
111
|
- lib/wtf_chord/helpers/roman_numbers_helper.rb
|
112
|
+
- lib/wtf_chord/instrument_pitch.rb
|
108
113
|
- lib/wtf_chord/keyboard.rb
|
114
|
+
- lib/wtf_chord/keyboard_presentation.rb
|
109
115
|
- lib/wtf_chord/note.rb
|
116
|
+
- lib/wtf_chord/piano_key.rb
|
110
117
|
- lib/wtf_chord/pitch.rb
|
111
118
|
- lib/wtf_chord/rules.rb
|
112
119
|
- lib/wtf_chord/scale.rb
|
@@ -1,120 +0,0 @@
|
|
1
|
-
module WTFChord
|
2
|
-
class Chord
|
3
|
-
end
|
4
|
-
|
5
|
-
class FingeringsGenerator < DelegateClass(Chord)
|
6
|
-
MAX_DIST = 5
|
7
|
-
MAX_FRET = 7
|
8
|
-
|
9
|
-
def fingerings
|
10
|
-
@fingerings ||= []
|
11
|
-
end
|
12
|
-
|
13
|
-
def call
|
14
|
-
fingerings.clear
|
15
|
-
|
16
|
-
(0...MAX_FRET).each do |from|
|
17
|
-
to = from + MAX_DIST
|
18
|
-
generate(from...to) do |variant|
|
19
|
-
fingerings << set_bass(variant) if filter_variant(variant)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
fingerings.uniq!
|
24
|
-
fingerings.sort_by!(&:complexity)
|
25
|
-
end
|
26
|
-
|
27
|
-
private
|
28
|
-
|
29
|
-
def generate(fret_range)
|
30
|
-
combinations = WTFChord.guitar.strings.map.with_index do |s, index|
|
31
|
-
fret_range.
|
32
|
-
select { |dist| pitch = s.original + dist; has_note?(pitch) }.
|
33
|
-
tap { |frets| frets << nil if frets.size == 0 }
|
34
|
-
end
|
35
|
-
|
36
|
-
combinations.inject(&:product).each do |fingers|
|
37
|
-
fingers.flatten!
|
38
|
-
|
39
|
-
Fingering.new(WTFChord.guitar, fingers) do |variant|
|
40
|
-
adjust(variant)
|
41
|
-
yield(variant)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def filter_variant(variant)
|
47
|
-
used_strings = variant.used_strings
|
48
|
-
used_notes = used_strings.map(&:note)
|
49
|
-
tones_count = notes.map { |n| used_notes.count(n) }
|
50
|
-
|
51
|
-
!fingerings.include?(variant) &&
|
52
|
-
basetone?(used_strings[0]) &&
|
53
|
-
(
|
54
|
-
(third?(used_strings[1]) || third?(used_strings[2])) ||
|
55
|
-
(last?(used_strings[1]) || last?(used_strings[2]))
|
56
|
-
) &&
|
57
|
-
all_notes?(used_notes) &&
|
58
|
-
used_notes.each_cons(2).none? { |(l, r)| l == r } &&
|
59
|
-
tones_count.all? { |n| tones_count[0] >= n || tones_count[notes.index(third_tone)] >= n } &&
|
60
|
-
variant.complexity <= 2.25
|
61
|
-
end
|
62
|
-
|
63
|
-
def adjust(fingering)
|
64
|
-
while (string = fingering.used_strings[0]) && !basetone?(string)
|
65
|
-
string.dead if !string.dead?
|
66
|
-
break if fingering.used_strings.size == 4
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def set_bass(variant)
|
71
|
-
if bass?
|
72
|
-
variant.find_used_string_for(original_bass) do |idx, bass_string|
|
73
|
-
try_set_bass_on(variant, idx) or begin
|
74
|
-
4.times do |i|
|
75
|
-
next if i == idx
|
76
|
-
if try_set_bass_on(variant, i)
|
77
|
-
bass_string.dead
|
78
|
-
break
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
variant
|
86
|
-
end
|
87
|
-
|
88
|
-
def try_set_bass_on(variant, idx)
|
89
|
-
bass_string = variant.strings[idx].dup
|
90
|
-
distance = bass_string.distance_to(bass)
|
91
|
-
|
92
|
-
if distance >= 0 && distance < 5 && (distance - variant.min_fret) > -3
|
93
|
-
variant.extra_complexity += 0.5 if distance < variant.min_fret
|
94
|
-
variant.strings[idx].hold_on(distance)
|
95
|
-
true
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
def all_notes?(used_notes)
|
100
|
-
notes.all? { |n| used_notes.include?(n) }
|
101
|
-
end
|
102
|
-
|
103
|
-
def has_note?(value)
|
104
|
-
value = value.note if !value.is_a?(Note) && value.respond_to?(:note)
|
105
|
-
notes.include?(value)
|
106
|
-
end
|
107
|
-
|
108
|
-
def basetone?(string)
|
109
|
-
string && string.note == notes[0]
|
110
|
-
end
|
111
|
-
|
112
|
-
def third?(string)
|
113
|
-
string && (string.note == third_tone)
|
114
|
-
end
|
115
|
-
|
116
|
-
def last?(string)
|
117
|
-
string && (string.note == notes[-1])
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|