wtf_chord 0.3.1 → 0.7.1
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 +5 -5
- data/CHANGELOG.md +10 -0
- data/README.md +22 -0
- data/lib/wtf_chord.rb +5 -0
- data/lib/wtf_chord/chord.rb +43 -9
- data/lib/wtf_chord/cli.rb +13 -5
- 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 +39 -0
- data/lib/wtf_chord/complexity_counter.rb +1 -1
- data/lib/wtf_chord/fingering.rb +24 -1
- data/lib/wtf_chord/formatter.rb +2 -0
- data/lib/wtf_chord/formatters/base.rb +7 -1
- data/lib/wtf_chord/formatters/piano.rb +21 -0
- data/lib/wtf_chord/formatters/piano1.rb +46 -0
- data/lib/wtf_chord/fretboard.rb +12 -2
- data/lib/wtf_chord/guitar_string.rb +18 -6
- data/lib/wtf_chord/helpers/roman_numbers_helper.rb +3 -1
- data/lib/wtf_chord/instrument_pitch.rb +53 -0
- data/lib/wtf_chord/keyboard.rb +21 -0
- data/lib/wtf_chord/keyboard_presentation.rb +90 -0
- data/lib/wtf_chord/piano_key.rb +19 -0
- data/lib/wtf_chord/pitch.rb +4 -0
- data/lib/wtf_chord/scale.rb +22 -6
- data/lib/wtf_chord/version.rb +1 -1
- data/wtf_chord.gemspec +2 -2
- metadata +18 -22
- data/lib/wtf_chord/fingerings_generator.rb +0 -92
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d79d3b4f0a6ef8088987819387a46e7e1b4fd4e03614f131dba94ea7cc19e6c8
|
4
|
+
data.tar.gz: e7122f195b69586a37c6a570bc0e3b64cf5f4ca8f1e357fc2504be86dfc0d0bc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d2defd3bdae6c97b0eeb1e899edc1204a53c61a124b6832ac5748c1083fae7714cf322ba53e0000fc36fd217bbd85c0393833fada192cc02df76a2aadd4661b1
|
7
|
+
data.tar.gz: 18d8cf19045668207068d5854108292cc045f35da1e9063b7a3d869c78d2d78a98cad034730b8f950c465223128acb30d754a2b84136926a3358484f8575beeb
|
data/CHANGELOG.md
ADDED
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,28 +1,62 @@
|
|
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
|
6
|
-
|
8
|
+
BASS_MATCH = /(?<=[\\\/])[A-H][b#]?(?=$)/
|
9
|
+
|
10
|
+
attr_reader :pitch, :steps, :notes, :name, :bass
|
7
11
|
|
8
12
|
def initialize(note, name)
|
9
13
|
@pitch = note.is_a?(Pitch) ? note : WTFChord.note(note)
|
10
|
-
@name = "#{@pitch.key}#{name}"
|
14
|
+
@name = "#{@pitch.key}#{name}"
|
11
15
|
@steps = Array(WTFChord.rules[name])
|
12
16
|
@notes = @steps.map { |dist| (@pitch + dist).note }.tap(&:uniq!)
|
17
|
+
|
18
|
+
BASS_MATCH.match(@name) do |m|
|
19
|
+
@bass = WTFChord.note(m[0]).note
|
20
|
+
end
|
13
21
|
end
|
14
22
|
|
15
|
-
def
|
16
|
-
|
23
|
+
def fingerings(limit = nil, collector: Collectors::Guitar)
|
24
|
+
collector.new(self).call[0, limit || 5]
|
17
25
|
end
|
18
26
|
|
19
|
-
def
|
20
|
-
|
21
|
-
FingeringsGenerator.new(self).call[0, limit]
|
27
|
+
def tone
|
28
|
+
bass || original_bass
|
22
29
|
end
|
23
30
|
|
24
31
|
def third_tone
|
25
|
-
@third_tone ||= @notes[@steps.
|
32
|
+
@third_tone ||= @notes[@steps.size > 3 ? 2 : -1]
|
33
|
+
end
|
34
|
+
|
35
|
+
def bass?
|
36
|
+
!!@bass
|
37
|
+
end
|
38
|
+
|
39
|
+
def size
|
40
|
+
bass? ? [*@notes, bass].uniq.size : @notes.size
|
41
|
+
end
|
42
|
+
|
43
|
+
def original_bass
|
44
|
+
@notes[0]
|
45
|
+
end
|
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
|
+
|
58
|
+
def inspect
|
59
|
+
"#{name} (#{@notes.map(&:key) * ' - '})"
|
26
60
|
end
|
27
61
|
end
|
28
62
|
end
|
data/lib/wtf_chord/cli.rb
CHANGED
@@ -7,14 +7,22 @@ module WTFChord
|
|
7
7
|
include Methadone::CLILogging
|
8
8
|
|
9
9
|
main do |name|
|
10
|
-
chord
|
11
|
-
|
12
|
-
formatter = WTFChord::Formatter.new(options['output'], options['rates'])
|
10
|
+
chord = WTFChord.chord(name)
|
11
|
+
formatter = WTFChord::Formatter.new(options['output'], options['rates'])
|
13
12
|
|
14
13
|
debug { "Output using formatter: #{formatter.formatter.to_s}\n" }
|
15
14
|
|
16
|
-
|
17
|
-
|
15
|
+
options['amount'] = 1 if options['output'] == 'piano1'
|
16
|
+
|
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
|
18
26
|
end
|
19
27
|
|
20
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,39 @@
|
|
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
|
+
Sorting = -> (tone, notes) {
|
20
|
+
notes = notes.sort
|
21
|
+
mod = (notes[0].note.position - tone.position).abs.next
|
22
|
+
score = Distances[notes[1..-1]].sum
|
23
|
+
mod + score
|
24
|
+
}.curry(2).freeze
|
25
|
+
|
26
|
+
def collect!
|
27
|
+
@keyboard = Keyboard.new(1..MAX_OCTAVE)
|
28
|
+
|
29
|
+
@fingerings = @keyboard.keys.
|
30
|
+
grep(__getobj__).
|
31
|
+
combination(size).
|
32
|
+
filter(&FilterProc[size])
|
33
|
+
@fingerings.uniq! { |x| x.map(&:key) }
|
34
|
+
@fingerings.sort_by!(&Sorting[tone])
|
35
|
+
@fingerings
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/wtf_chord/fingering.rb
CHANGED
@@ -4,15 +4,18 @@ require 'wtf_chord/formatter'
|
|
4
4
|
|
5
5
|
module WTFChord
|
6
6
|
class Fingering < Fretboard
|
7
|
+
attr_accessor :extra_complexity
|
8
|
+
|
7
9
|
def initialize(guitar, fingers = nil)
|
8
10
|
@capo = guitar.capo
|
9
11
|
@strings = guitar.strings.map(&:dup)
|
12
|
+
@extra_complexity = 0.0
|
10
13
|
set_fingers(fingers) if fingers.is_a?(Array)
|
11
14
|
yield(self) if block_given?
|
12
15
|
end
|
13
16
|
|
14
17
|
def code
|
15
|
-
|
18
|
+
strings.map(&:code).pack("c*")
|
16
19
|
end
|
17
20
|
|
18
21
|
def == other
|
@@ -23,6 +26,18 @@ module WTFChord
|
|
23
26
|
end
|
24
27
|
end
|
25
28
|
|
29
|
+
def eql?(other)
|
30
|
+
super || self == other
|
31
|
+
end
|
32
|
+
|
33
|
+
def hash
|
34
|
+
strings.map(&:code).hash
|
35
|
+
end
|
36
|
+
|
37
|
+
def used_keys
|
38
|
+
used_strings.map(&:note).map(&:key).uniq.join
|
39
|
+
end
|
40
|
+
|
26
41
|
def complexity
|
27
42
|
complexity_counter.rate
|
28
43
|
end
|
@@ -43,6 +58,14 @@ module WTFChord
|
|
43
58
|
@strings.reject(&:dead?)
|
44
59
|
end
|
45
60
|
|
61
|
+
def find_used_string_for(note)
|
62
|
+
used = used_strings
|
63
|
+
if idx = used.index(note)
|
64
|
+
string = used[idx]
|
65
|
+
yield(strings.index(string), string)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
46
69
|
def min_fret
|
47
70
|
holded_strings.min.fret
|
48
71
|
end
|
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,6 +33,10 @@ module WTFChord
|
|
31
33
|
def draw
|
32
34
|
end
|
33
35
|
|
36
|
+
def actual_strings
|
37
|
+
strings.reject(&:dead?)
|
38
|
+
end
|
39
|
+
|
34
40
|
def with_rates?
|
35
41
|
!!@with_rates
|
36
42
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "wtf_chord/formatters/piano1"
|
4
|
+
require "wtf_chord/keyboard"
|
5
|
+
|
6
|
+
module WTFChord
|
7
|
+
module Formatters
|
8
|
+
class Piano < Piano1
|
9
|
+
def draw
|
10
|
+
[
|
11
|
+
pressed_keys.map(&:to_s).join(" - "),
|
12
|
+
keyboard_presentation
|
13
|
+
].join("\n")
|
14
|
+
end
|
15
|
+
|
16
|
+
def keyboard_presentation
|
17
|
+
KeyboardPresentation.press(*pressed_keys)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
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
|
@@ -23,6 +25,14 @@ module WTFChord
|
|
23
25
|
self
|
24
26
|
end
|
25
27
|
|
28
|
+
def with_capo(capo)
|
29
|
+
capo_was = @capo
|
30
|
+
set_capo(capo)
|
31
|
+
yield
|
32
|
+
ensure
|
33
|
+
set_capo(capo_was)
|
34
|
+
end
|
35
|
+
|
26
36
|
def fingers
|
27
37
|
@strings.map { |string| string.dead? ? DEAD : string.fret }
|
28
38
|
end
|
@@ -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)
|
@@ -48,11 +53,18 @@ module WTFChord
|
|
48
53
|
dead? ? -1 : to_i
|
49
54
|
end
|
50
55
|
|
56
|
+
def distance_to(pitch)
|
57
|
+
pos = 0
|
58
|
+
opened = dup.open
|
59
|
+
pos += 1 while (opened + pos) != pitch
|
60
|
+
pos
|
61
|
+
end
|
62
|
+
|
51
63
|
def <=> other
|
52
64
|
return if dead?
|
53
65
|
case other
|
54
|
-
when Integer
|
55
|
-
when GuitarString
|
66
|
+
when Integer then @fret <=> other
|
67
|
+
when GuitarString then @fret <=> other.fret
|
56
68
|
end
|
57
69
|
end
|
58
70
|
|
@@ -1,6 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module WTFChord
|
2
4
|
module RomanNumbersHelper
|
3
|
-
ROMAN_SYMBOLS ||=
|
5
|
+
ROMAN_SYMBOLS ||= ("\u2160".."\u216B").to_a.unshift(nil).freeze
|
4
6
|
|
5
7
|
def romanize(number)
|
6
8
|
ROMAN_SYMBOLS[number]
|
@@ -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
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "wtf_chord/piano_key"
|
4
|
+
|
5
|
+
module WTFChord
|
6
|
+
class Keyboard
|
7
|
+
attr_reader :keys
|
8
|
+
|
9
|
+
KEYS = %w(C C# D D# E F F# G G# A Bb B)
|
10
|
+
|
11
|
+
def initialize(octaves = 1..5)
|
12
|
+
@keys = octaves.to_a.product(KEYS).map { |(o, k)| PianoKey.new("#{k}#{o}") }
|
13
|
+
end
|
14
|
+
|
15
|
+
def [] idx
|
16
|
+
case idx
|
17
|
+
when 1..@keys.size then @keys[~-idx]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
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/pitch.rb
CHANGED
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,8 +62,22 @@ module WTFChord
|
|
60
62
|
Bb|A#
|
61
63
|
B|H
|
62
64
|
)
|
63
|
-
ScaleArray.build(*chromatic_scale)
|
65
|
+
ScaleArray.build(*chromatic_scale)
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.guitar=(guitar)
|
69
|
+
Thread.current[:wtf_guitar] = guitar
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.guitar
|
73
|
+
Thread.current[:wtf_guitar] ||= Fretboard.new(*%w(E2 A2 D3 G3 B3 E4))
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.piano=(piano)
|
77
|
+
Thread.current[:wtf_piano] = piano
|
64
78
|
end
|
65
79
|
|
66
|
-
|
80
|
+
def self.piano
|
81
|
+
Thread.current[:wtf_piano] ||= Keyboard.new
|
82
|
+
end
|
67
83
|
end
|
data/lib/wtf_chord/version.rb
CHANGED
data/wtf_chord.gemspec
CHANGED
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
|
|
22
22
|
|
23
23
|
spec.add_runtime_dependency "methadone"
|
24
24
|
|
25
|
-
spec.add_development_dependency "bundler"
|
26
|
-
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency "bundler"
|
26
|
+
spec.add_development_dependency "rake"
|
27
27
|
spec.add_development_dependency "rspec", ">= 3.0", "< 4"
|
28
28
|
end
|
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.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Anton
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-03-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: methadone
|
@@ -30,40 +30,28 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
34
|
-
- - "<"
|
35
|
-
- !ruby/object:Gem::Version
|
36
|
-
version: '2'
|
33
|
+
version: '0'
|
37
34
|
type: :development
|
38
35
|
prerelease: false
|
39
36
|
version_requirements: !ruby/object:Gem::Requirement
|
40
37
|
requirements:
|
41
38
|
- - ">="
|
42
39
|
- !ruby/object:Gem::Version
|
43
|
-
version: '
|
44
|
-
- - "<"
|
45
|
-
- !ruby/object:Gem::Version
|
46
|
-
version: '2'
|
40
|
+
version: '0'
|
47
41
|
- !ruby/object:Gem::Dependency
|
48
42
|
name: rake
|
49
43
|
requirement: !ruby/object:Gem::Requirement
|
50
44
|
requirements:
|
51
45
|
- - ">="
|
52
46
|
- !ruby/object:Gem::Version
|
53
|
-
version: '
|
54
|
-
- - "<"
|
55
|
-
- !ruby/object:Gem::Version
|
56
|
-
version: '12'
|
47
|
+
version: '0'
|
57
48
|
type: :development
|
58
49
|
prerelease: false
|
59
50
|
version_requirements: !ruby/object:Gem::Requirement
|
60
51
|
requirements:
|
61
52
|
- - ">="
|
62
53
|
- !ruby/object:Gem::Version
|
63
|
-
version: '
|
64
|
-
- - "<"
|
65
|
-
- !ruby/object:Gem::Version
|
66
|
-
version: '12'
|
54
|
+
version: '0'
|
67
55
|
- !ruby/object:Gem::Dependency
|
68
56
|
name: rspec
|
69
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -95,6 +83,7 @@ files:
|
|
95
83
|
- ".gitignore"
|
96
84
|
- ".rspec"
|
97
85
|
- ".travis.yml"
|
86
|
+
- CHANGELOG.md
|
98
87
|
- Gemfile
|
99
88
|
- README.md
|
100
89
|
- Rakefile
|
@@ -105,17 +94,26 @@ files:
|
|
105
94
|
- lib/wtf_chord.yml
|
106
95
|
- lib/wtf_chord/chord.rb
|
107
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
|
108
101
|
- lib/wtf_chord/complexity_counter.rb
|
109
102
|
- lib/wtf_chord/fingering.rb
|
110
|
-
- lib/wtf_chord/fingerings_generator.rb
|
111
103
|
- lib/wtf_chord/formatter.rb
|
112
104
|
- lib/wtf_chord/formatters/base.rb
|
113
105
|
- lib/wtf_chord/formatters/default.rb
|
106
|
+
- lib/wtf_chord/formatters/piano.rb
|
107
|
+
- lib/wtf_chord/formatters/piano1.rb
|
114
108
|
- lib/wtf_chord/formatters/simple.rb
|
115
109
|
- lib/wtf_chord/fretboard.rb
|
116
110
|
- lib/wtf_chord/guitar_string.rb
|
117
111
|
- lib/wtf_chord/helpers/roman_numbers_helper.rb
|
112
|
+
- lib/wtf_chord/instrument_pitch.rb
|
113
|
+
- lib/wtf_chord/keyboard.rb
|
114
|
+
- lib/wtf_chord/keyboard_presentation.rb
|
118
115
|
- lib/wtf_chord/note.rb
|
116
|
+
- lib/wtf_chord/piano_key.rb
|
119
117
|
- lib/wtf_chord/pitch.rb
|
120
118
|
- lib/wtf_chord/rules.rb
|
121
119
|
- lib/wtf_chord/scale.rb
|
@@ -139,10 +137,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
139
137
|
- !ruby/object:Gem::Version
|
140
138
|
version: '0'
|
141
139
|
requirements: []
|
142
|
-
|
143
|
-
rubygems_version: 2.6.2
|
140
|
+
rubygems_version: 3.0.3
|
144
141
|
signing_key:
|
145
142
|
specification_version: 4
|
146
143
|
summary: "‘WTF Chord?’ is the Ruby guitar chords generator library."
|
147
144
|
test_files: []
|
148
|
-
has_rdoc:
|
@@ -1,92 +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 << variant if filter_variant(variant)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
fingerings.sort_by!(&:complexity)
|
24
|
-
end
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
def generate(fret_range)
|
29
|
-
combinations = GUITAR.strings.map.with_index do |s, index|
|
30
|
-
fret_range.select do |dist|
|
31
|
-
pitch = s.original + dist
|
32
|
-
has_note?(pitch)
|
33
|
-
end.tap do |frets|
|
34
|
-
frets << nil if frets.size == 0
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
combinations.inject(&:product).each do |fingers|
|
39
|
-
fingers.flatten!
|
40
|
-
Fingering.new(GUITAR, fingers) do |variant|
|
41
|
-
adjust(variant)
|
42
|
-
yield(variant)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
def filter_variant(variant)
|
48
|
-
used_strings = variant.used_strings
|
49
|
-
used_notes = used_strings.map(&:note)
|
50
|
-
tones_count = notes.map { |n| used_notes.count(n) }
|
51
|
-
|
52
|
-
!fingerings.include?(variant) &&
|
53
|
-
basetone?(used_strings[0]) &&
|
54
|
-
(
|
55
|
-
(third?(used_strings[1]) || third?(used_strings[2])) ||
|
56
|
-
(last?(used_strings[1]) || last?(used_strings[2]))
|
57
|
-
) &&
|
58
|
-
all_notes?(used_notes) &&
|
59
|
-
used_notes.each_cons(2).none? { |(l, r)| l == r } &&
|
60
|
-
tones_count.all? { |n| tones_count[0] >= n || tones_count[notes.index(third_tone)] >= n } &&
|
61
|
-
variant.complexity <= 2.25
|
62
|
-
end
|
63
|
-
|
64
|
-
def adjust(fingering)
|
65
|
-
while (string = fingering.used_strings[0]) && !basetone?(string)
|
66
|
-
string.dead if !string.dead?
|
67
|
-
break if fingering.used_strings.size == 4
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def all_notes?(used_notes)
|
72
|
-
notes.all? { |n| used_notes.include?(n) }
|
73
|
-
end
|
74
|
-
|
75
|
-
def has_note?(value)
|
76
|
-
value = value.note if !value.is_a?(Note) && value.respond_to?(:note)
|
77
|
-
notes.include?(value)
|
78
|
-
end
|
79
|
-
|
80
|
-
def basetone?(string)
|
81
|
-
string && string.note == notes[0]
|
82
|
-
end
|
83
|
-
|
84
|
-
def third?(string)
|
85
|
-
string && (string.note == third_tone)
|
86
|
-
end
|
87
|
-
|
88
|
-
def last?(string)
|
89
|
-
string && (string.note == notes[-1])
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|