wtf_chord 0.3 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 0ffe99b5205f9a4cbde1ebdd815a41dc9f8f0836
4
- data.tar.gz: d08a21cf109465b729d171e88e7a3f3631ca95df
2
+ SHA256:
3
+ metadata.gz: 8d764a30038a11df351e6d3d9dfce09084aa89af352d92b36677ed5eac31e425
4
+ data.tar.gz: 0c713ee0f4e3b36720014f56c1c55ca6686981b49e228402d8c2f826463affce
5
5
  SHA512:
6
- metadata.gz: e250bfa2ee065d20208dfca8831b33c216b2a4c2c450fecae75bef2a9b8c69189c2d6fd244dff31c25c00c38f9549d579f42b7ba6fca557509fb5a88460a2290
7
- data.tar.gz: 25a18c48cecc08247ccfa78c6c396d2edd9c4481b0c86f39cf23636fb1b7bf51ba038d15b5e2fd0db8cd89c904aaf5d939cacb1a78233dcd0e7108495e1fd4cc
6
+ metadata.gz: e83f9de2c3913c4206d7d4f424f46f1082fcd16a50f8f19d4d78f2d83f3a66f610d23852ac66b890190f444db14e74d3fc9249514e6ece64626685c4c7434de2
7
+ data.tar.gz: 3ac9226adb773858a51c474c0f1b299e3bf2601ce567d9967c96b297ba4b0895da08c6b50e404221ba636be01d83740fea20b3f5684ebb24a2053b4b81235425
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ ## 0.5.0 / 2016-08-17
2
+
3
+ * 2 major features
4
+ * Support chords with bass string mods (like Am/G)
5
+ * Add extra_complexity modifier
6
+
7
+ * 1 bug fixes:
8
+ * Fix fingering equality
9
+
10
+ * Add CHANGELOG.md
data/README.md CHANGED
@@ -31,13 +31,13 @@ Or install it yourself as:
31
31
 
32
32
  Finds fingerings of the requested chord.
33
33
 
34
- v0.2.1
34
+ v0.3.1
35
35
 
36
36
  Options:
37
37
  -h, --help Show command line help
38
38
  --version Show help/version info
39
39
  -n, --amount N Amount of fingering variants to output.
40
- (default: 3)
40
+ (default: 5)
41
41
  -o, --output FORMAT Output format.
42
42
  (default: default)
43
43
  -R, --[no-]rates Output fingering complexity rates
@@ -57,25 +57,25 @@ For example, to print two fingering variants of the `Dm` chord, just run:
57
57
  And you'll get the visual presentation of chords' fingerings:
58
58
 
59
59
  [ × × 0 2 3 1 ]
60
- ––––––––––––––––––
61
- | | | | |
62
- | | | | |
63
- | | | | |
60
+ ——————————————————
61
+ | | | | |
62
+ | | | | |
63
+ | | | | |
64
64
  | | | | | |
65
65
  | | | | | |
66
66
  | | | | | |
67
- ––––––––––––––––––
67
+ ——————————————————
68
68
  D A D F
69
69
 
70
70
  [ × 5 7 7 6 5 ]
71
- ––––––––––––––––––
72
- | | | | V
73
- | | | | |
74
- | | | |
71
+ ——————————————————
72
+ | | | | ◉ ← V
73
+ | | | | |
74
+ | | | |
75
75
  | | | | | |
76
76
  | | | | | |
77
77
  | | | | | |
78
- ––––––––––––––––––
78
+ ——————————————————
79
79
  D A D F A
80
80
 
81
81
  You can get simpler output, using the `--output` option:
@@ -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
@@ -1,28 +1,62 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'wtf_chord/fingering'
2
- require 'wtf_chord/fingerings_generator'
4
+ require 'wtf_chord/collectors'
3
5
 
4
6
  module WTFChord
5
7
  class Chord
6
- attr_reader :pitch, :steps, :notes, :name
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}".freeze
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 inspect
16
- "#{name} (#{@notes.map(&:key) * ' - '})"
23
+ def fingerings(limit = nil, collector: Collectors::Guitar)
24
+ collector.new(self).call[0, limit || 5]
17
25
  end
18
26
 
19
- def fingerings(limit = nil)
20
- limit ||= 5
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.index(7) || -1]
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.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 = WTFChord.chord(name)
11
- fingerings = chord.fingerings(options['amount'])
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
- puts chord.inspect, nil
17
- formatter.(*fingerings)
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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "wtf_chord/collectors/generic"
4
+
5
+ module WTFChord
6
+ module Collectors
7
+ autoload :Guitar, "wtf_chord/collectors/guitar"
8
+ autoload :Piano, "wtf_chord/collectors/piano"
9
+ end
10
+ end
@@ -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
@@ -28,7 +28,7 @@ module WTFChord
28
28
  end
29
29
  end
30
30
 
31
- base_rate
31
+ base_rate + @fingering.extra_complexity
32
32
  end
33
33
 
34
34
  private
@@ -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
- @code ||= strings.map(&:code).pack("c*")
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
@@ -2,6 +2,8 @@ require 'wtf_chord/formatters/base'
2
2
 
3
3
  module WTFChord
4
4
  class Formatter
5
+ attr_reader :name
6
+
5
7
  def initialize(name, with_rates = false)
6
8
  @name = name
7
9
  @with_rates = with_rates
@@ -9,19 +11,21 @@ module WTFChord
9
11
  end
10
12
 
11
13
  def call(*fingerings)
12
- formatter.with_rates(@with_rates) do |f|
13
- puts (fingerings.map(&f) * f.separator)
14
- end
14
+ formatter.with_rates(@with_rates) { |f| fingerings.map(&f) }
15
15
  end
16
16
 
17
17
  alias :[] :call
18
18
 
19
+ def separator
20
+ formatter.separator
21
+ end
22
+
19
23
  def formatter
20
- WTFChord::Formatters.const_get(formatter_name)
24
+ Formatters.const_get(formatter_name)
21
25
  end
22
26
 
23
27
  def formatter?
24
- WTFChord::Formatters.const_defined?(formatter_name)
28
+ Formatters.const_defined?(formatter_name)
25
29
  end
26
30
 
27
31
  def formatter_name
@@ -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".freeze
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
@@ -1,25 +1,28 @@
1
+ require 'wtf_chord/helpers/roman_numbers_helper'
2
+
1
3
  module WTFChord
2
4
  module Formatters
3
5
  class Default < Base
4
6
  OPEN = "|".freeze
5
- HORIZ = "".freeze
7
+ HORIZ = "".freeze
6
8
  SPACE = " ".freeze
7
- BULL = "\u2022".freeze
8
- LATIN = %w(0 I II III IV V VI VII VIII IX X XII XIII).freeze
9
+ BULL = "\u25C9".freeze
9
10
  NEWLINE = "\n".freeze
11
+ COMPLEXITY_FORMAT = " (complexity: %.2f)".freeze
12
+ EMPTY_STRING = Array.new(FRETS, OPEN).freeze
10
13
 
11
- def self.separator
12
- "\n\n".freeze
13
- end
14
+ include RomanNumbersHelper
14
15
 
15
16
  def draw
16
- [
17
- "[ #{head} ]#{capo}",
18
- " #{border}",
19
- *fret_rows,
20
- " #{border}",
21
- " #{string_keys}#{rate}"
22
- ] * NEWLINE
17
+ <<-EOF.gsub(/^\s+\|/, '')
18
+ |[ #{head} ] #{capo}
19
+ | #{border}
20
+ #{fret_rows.map.with_index { |row, index|
21
+ " | #{row} #{roman_min_fret if index == 0}"
22
+ }.join("\n")}
23
+ | #{border}
24
+ | #{string_keys}#{rate}
25
+ EOF
23
26
  end
24
27
 
25
28
  private
@@ -29,11 +32,15 @@ module WTFChord
29
32
  end
30
33
 
31
34
  def capo
32
- " (capo #{to_latin @fret.capo})" if @fret.capo > 0
35
+ "(capo #{romanize(@fret.capo)})" if @fret.capo > 0
36
+ end
37
+
38
+ def roman_min_fret
39
+ " ← #{romanize(min_fret)}" if min_fret > 1
33
40
  end
34
41
 
35
42
  def rate
36
- " (complexity: %.2f)" % @fret.complexity if with_rates?
43
+ COMPLEXITY_FORMAT % @fret.complexity if with_rates?
37
44
  end
38
45
 
39
46
  def border
@@ -41,27 +48,21 @@ module WTFChord
41
48
  end
42
49
 
43
50
  def string_rows
44
- strings.map { |string| draw_string(string.fret) }
51
+ strings.map { |string| draw_string(string) }
45
52
  end
46
53
 
47
54
  def fret_rows
48
- string_rows.transpose.map! { |row| " #{row * SPACE} " }.tap do |rows|
49
- rows[0] << " #{to_latin min_fret}" if min_fret > 1
50
- end
55
+ return to_enum(__method__) unless block_given?
56
+ string_rows.transpose.each { |row| yield(row * SPACE) }
51
57
  end
52
58
 
53
59
  def string_keys
54
60
  strings.map { |s| s.dead? ? SPACE : "%-2s" % s.key } * " "
55
61
  end
56
62
 
57
- def to_latin(num)
58
- LATIN[num]
59
- end
60
-
61
- def draw_string(fret)
62
- Array.new(FRETS, OPEN).tap do |rows|
63
- rows[(fret - min_fret.pred).pred] = BULL if fret.to_i > 0
64
- end
63
+ def draw_string(string)
64
+ EMPTY_STRING.dup.
65
+ tap { |rows| rows[string.fret - min_fret] = BULL if string.holded? }
65
66
  end
66
67
  end
67
68
  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
@@ -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 = "×".freeze
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! { |string| GuitarString.new(string, @capo) }
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
- require 'wtf_chord/pitch'
1
+ # frozen_string_literal: true
2
+
3
+ require 'wtf_chord/instrument_pitch'
2
4
 
3
5
  module WTFChord
4
- class GuitarString < DelegateClass(WTFChord::Pitch)
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(@original + capo)
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 then @fret <=> other
55
- when GuitarString then @fret <=> other.fret
66
+ when Integer then @fret <=> other
67
+ when GuitarString then @fret <=> other.fret
56
68
  end
57
69
  end
58
70
 
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WTFChord
4
+ module RomanNumbersHelper
5
+ ROMAN_SYMBOLS ||= ("\u2160".."\u216B").to_a.unshift(nil).freeze
6
+
7
+ def romanize(number)
8
+ ROMAN_SYMBOLS[number]
9
+ end
10
+ end
11
+ end
@@ -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
@@ -25,7 +25,7 @@ module WTFChord
25
25
 
26
26
  def == other
27
27
  case other
28
- when String then other.casecmp(@key).zero?
28
+ when String then other.casecmp(@key).zero? || aliases.any? { |a| other.casecmp(a.key).zero? }
29
29
  when Integer then other == @position
30
30
  when Note then other.position == @position
31
31
  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
@@ -46,6 +46,10 @@ module WTFChord
46
46
  move -amount
47
47
  end
48
48
 
49
+ def -@
50
+ -to_i
51
+ end
52
+
49
53
  def to_i
50
54
  (@octave * 12) + @note.position
51
55
  end
@@ -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".freeze
34
- SHARP ||= "#".freeze
35
- SIGNS ||= { FLAT => "\u266D".freeze, SHARP => "\u266F".freeze }.freeze
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
- }.freeze
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).freeze
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
- GUITAR ||= Fretboard.new(*%w(E2 A2 D3 G3 B3 E4))
80
+ def self.piano
81
+ Thread.current[:wtf_piano] ||= Keyboard.new
82
+ end
67
83
  end
@@ -1,3 +1,3 @@
1
1
  module WTFChord
2
- VERSION = "0.3"
2
+ VERSION = "0.7.0"
3
3
  end
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", ">= 1.11", "< 2"
26
- spec.add_development_dependency "rake", ">= 10.0", "< 12"
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.3'
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: 2016-04-03 00:00:00.000000000 Z
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: '1.11'
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: '1.11'
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: '10.0'
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: '10.0'
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,16 +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
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
117
115
  - lib/wtf_chord/note.rb
116
+ - lib/wtf_chord/piano_key.rb
118
117
  - lib/wtf_chord/pitch.rb
119
118
  - lib/wtf_chord/rules.rb
120
119
  - lib/wtf_chord/scale.rb
@@ -138,10 +137,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
138
137
  - !ruby/object:Gem::Version
139
138
  version: '0'
140
139
  requirements: []
141
- rubyforge_project:
142
- rubygems_version: 2.5.1
140
+ rubygems_version: 3.0.3
143
141
  signing_key:
144
142
  specification_version: 4
145
143
  summary: "‘WTF Chord?’ is the Ruby guitar chords generator library."
146
144
  test_files: []
147
- has_rdoc:
@@ -1,85 +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
- (third?(used_strings[1]) || third?(used_strings[2])) &&
55
- all_notes?(used_notes) &&
56
- used_notes.each_cons(2).none? { |(l, r)| l == r } &&
57
- tones_count.all? { |n| tones_count[0] >= n || tones_count[notes.index(third_tone)] >= n } &&
58
- variant.complexity <= 2.25
59
- end
60
-
61
- def adjust(fingering)
62
- while (string = fingering.used_strings[0]) && !basetone?(string)
63
- string.dead if !string.dead?
64
- break if fingering.used_strings.size == 4
65
- end
66
- end
67
-
68
- def all_notes?(used_notes)
69
- notes.all? { |n| used_notes.include?(n) }
70
- end
71
-
72
- def has_note?(value)
73
- value = value.note if !value.is_a?(Note) && value.respond_to?(:note)
74
- notes.include?(value)
75
- end
76
-
77
- def basetone?(string)
78
- string && string.note == notes[0]
79
- end
80
-
81
- def third?(string)
82
- string && (string.note == third_tone)
83
- end
84
- end
85
- end