coltrane 2.0.0 → 2.1.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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -1
  3. data/Gemfile +1 -0
  4. data/Gemfile.lock +21 -3
  5. data/README.md +0 -7
  6. data/Rakefile +5 -0
  7. data/bin/console +2 -0
  8. data/bin/opal +29 -0
  9. data/bin/opal-build +29 -0
  10. data/bin/opal-mspec +29 -0
  11. data/bin/opal-repl +29 -0
  12. data/bin/rackup +12 -0
  13. data/bin/sprockets +12 -0
  14. data/bin/tilt +12 -0
  15. data/coltrane.gemspec +1 -1
  16. data/config.ru +10 -0
  17. data/exe/coltrane +44 -13
  18. data/lib/cli.rb +7 -1
  19. data/lib/cli/bass_guitar.rb +1 -1
  20. data/lib/cli/chord.rb +11 -4
  21. data/lib/cli/config.rb +38 -0
  22. data/lib/cli/guitar.rb +8 -10
  23. data/lib/cli/guitar_chords.rb +122 -0
  24. data/lib/cli/notes.rb +3 -4
  25. data/lib/cli/piano.rb +1 -1
  26. data/lib/cli/representation.rb +6 -12
  27. data/lib/cli/scale.rb +4 -4
  28. data/lib/cli/text.rb +1 -1
  29. data/lib/cli/ukulele.rb +1 -1
  30. data/lib/coltrane.rb +35 -32
  31. data/lib/coltrane/chord_quality.rb +4 -3
  32. data/lib/coltrane/circle_of_fifths.rb +29 -0
  33. data/lib/coltrane/classic_scales.rb +36 -43
  34. data/lib/coltrane/diatonic_scale.rb +34 -0
  35. data/lib/coltrane/frequency.rb +1 -1
  36. data/lib/coltrane/interval.rb +14 -0
  37. data/lib/coltrane/interval_class.rb +6 -0
  38. data/lib/coltrane/interval_sequence.rb +0 -1
  39. data/lib/coltrane/key.rb +14 -0
  40. data/lib/coltrane/note.rb +37 -2
  41. data/lib/coltrane/note_set.rb +23 -2
  42. data/lib/coltrane/pitch.rb +65 -31
  43. data/lib/coltrane/pitch_class.rb +25 -12
  44. data/lib/coltrane/progression.rb +2 -2
  45. data/lib/coltrane/qualities.rb +257 -0
  46. data/lib/coltrane/roman_chord.rb +10 -10
  47. data/lib/coltrane/scale.rb +9 -3
  48. data/lib/coltrane/version.rb +1 -1
  49. data/lib/coltrane/voicing.rb +38 -0
  50. data/lib/coltrane_game/question.rb +6 -0
  51. data/lib/coltrane_instruments.rb +3 -0
  52. data/lib/coltrane_instruments/guitar.rb +5 -2
  53. data/lib/coltrane_instruments/guitar/base.rb +18 -3
  54. data/lib/coltrane_instruments/guitar/chord.rb +139 -12
  55. data/lib/coltrane_instruments/guitar/note.rb +16 -0
  56. data/lib/coltrane_instruments/guitar/string.rb +21 -0
  57. data/lib/coltrane_synth.rb +7 -0
  58. data/lib/coltrane_synth/base.rb +47 -0
  59. data/lib/coltrane_synth/synth.rb +24 -0
  60. data/lib/core_ext.rb +25 -7
  61. data/lib/os.rb +19 -0
  62. metadata +24 -9
  63. data/dist/coltrane +0 -0
  64. data/lib/coltrane/piano_representation.rb +0 -0
@@ -0,0 +1,38 @@
1
+ module Coltrane
2
+
3
+ # This class describes an actual implementation of a Chord, being aware
4
+ # of exact octaves of each pitch and even repeating pitches across octaves.
5
+ class Voicing
6
+ attr_reader :pitches
7
+
8
+ def initialize(*pitch_strings, pitches: nil)
9
+ @pitches = if pitch_strings.any?
10
+ pitch_strings.map { |s| Pitch[s] }
11
+ elsif pitches
12
+ pitches
13
+ else
14
+ raise WrongArgumentsError
15
+ end
16
+ end
17
+
18
+ def self.[](*args)
19
+ new(*args)
20
+ end
21
+
22
+ def pitch_classes
23
+ NoteSet[*pitches.map(&:pitch_class).uniq]
24
+ end
25
+
26
+ alias notes pitch_classes
27
+
28
+ def chord
29
+ @chord ||= Chord.new(notes: notes)
30
+ rescue ChordNotFoundError
31
+ return false
32
+ end
33
+
34
+ def frequencies
35
+ pitches.map(&:frequency)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,6 @@
1
+ module ColtraneGame
2
+ class Question
3
+ def generate
4
+ end
5
+ end
6
+ end
@@ -1,4 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'coltrane'
4
+
3
5
  module ColtraneInstruments
6
+ autoload :Guitar, 'coltrane_instruments/guitar'
4
7
  end
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ColtraneInstruments
4
- # This module represents a guitar
5
4
  module Guitar
5
+ autoload :Base, 'coltrane_instruments/guitar/base'
6
+ autoload :Chord, 'coltrane_instruments/guitar/chord'
7
+ autoload :Note, 'coltrane_instruments/guitar/note'
8
+ autoload :String, 'coltrane_instruments/guitar/string'
6
9
  end
7
- end
10
+ end
@@ -2,12 +2,27 @@
2
2
 
3
3
  module ColtraneInstruments
4
4
  module Guitar
5
+ DEFAULT_TUNING = %w[E2 A2 D3 G3 B3 E4]
6
+ DEFAULT_FRETS = 23
5
7
  # A base class for operations involving Guitars
6
8
  class Base
7
- def initialize; end
9
+ attr_reader :strings, :frets
8
10
 
9
- def find_chord(target_chord)
10
- Chord.new(target_chord, guitar: self).fetch_descendant_chords
11
+ def self.find_chords(target_chord)
12
+ unless target_chord.is_a?(Coltrane::Chord)
13
+ target_chord = Coltrane::Chord.new(name: target_chord)
14
+ end
15
+
16
+ ColtraneInstruments::Guitar::Chord.new(target_chord, guitar: new)
17
+ .fetch_descendant_chords
18
+ end
19
+
20
+ def initialize(tuning = DEFAULT_TUNING, frets = DEFAULT_FRETS)
21
+ @strings = tuning.map do |p|
22
+ String.new(Coltrane::Pitch[p], guitar: self)
23
+ end
24
+
25
+ @frets = frets
11
26
  end
12
27
  end
13
28
  end
@@ -4,14 +4,62 @@ module ColtraneInstruments
4
4
  module Guitar
5
5
  # This class represents a group of guitar notes, strummed at the same time
6
6
  class Chord
7
- def initialize(target_chord, guitar_notes: [],
7
+ include Comparable
8
+ attr_reader :guitar_notes, :guitar, :free_fingers, :target_chord, :barre
9
+
10
+ MAX_FRET_SPAN = 3
11
+
12
+ def self.find(chord)
13
+ new(chord).fetch_descendant_chords
14
+ end
15
+
16
+ def initialize(target_chord,
17
+ guitar_notes: [],
8
18
  free_fingers: 4,
9
19
  barre: nil,
10
20
  guitar:)
21
+
11
22
  @target_chord = target_chord
12
23
  @guitar_notes = guitar_notes
13
24
  @free_fingers = free_fingers
14
- @barre = barre
25
+ @guitar = guitar
26
+ @barre = barre
27
+ end
28
+
29
+ def <=>(other)
30
+ rank <=> other.rank
31
+ end
32
+
33
+ def rank
34
+ +completeness * 10000 +
35
+ +fullness * 1000 +
36
+ -spreadness * 100 +
37
+ +easyness * 1
38
+ end
39
+
40
+ def analysis
41
+ {
42
+ completeness: completeness,
43
+ fullness: fullness,
44
+ easyness: easyness,
45
+ spreadness: spreadness
46
+ }
47
+ end
48
+
49
+ def spreadness
50
+ fret_range.size.to_f / MAX_FRET_SPAN
51
+ end
52
+
53
+ def completeness
54
+ (target_chord.notes.size.to_f - notes_left.size) / target_chord.notes.size
55
+ end
56
+
57
+ def easyness
58
+ frets.count(0).to_f / guitar_notes.size
59
+ end
60
+
61
+ def fullness
62
+ (guitar.strings.size.to_f - frets.count(nil)) / guitar.strings.size
15
63
  end
16
64
 
17
65
  def barre?
@@ -19,23 +67,102 @@ module ColtraneInstruments
19
67
  end
20
68
 
21
69
  def fetch_descendant_chords
22
- return self if guitar_notes.size < guitar.strings
23
- possible_new_notes.reduce([]) do |memo, n|
24
- fingers_change = n.fret == @barre ? 0 : 1
25
- return self unless (@free_fingers - fingers_change).negative?
26
- guitar.create_chord target_chord,
27
- guitar_notes: @guitar_notes + n,
28
- free_fingers: @free_fingers - fingers_change,
29
- barre: @barre
30
- .fetch_descendant_chords + memo
70
+ return [self] if guitar_notes.size >= guitar.strings.size
71
+ possible_new_notes(notes_available.positive?).reduce([]) do |memo, n|
72
+ barre = n.fret if guitar_notes.last == n.fret
73
+ fingers_change = (n.fret == barre || n.fret.zero?) ? 0 : 1
74
+ next memo if (free_fingers - fingers_change).negative?
75
+ Chord.new(target_chord, guitar_notes: guitar_notes + [n],
76
+ free_fingers: free_fingers - fingers_change,
77
+ guitar: guitar,
78
+ barre: barre)
79
+ .fetch_descendant_chords + memo
31
80
  end
32
81
  end
33
82
 
34
- def possible_new_notes; end
83
+ def notes_available
84
+ strings_available - notes_left.size
85
+ end
86
+
87
+ def strings_available
88
+ guitar.strings.size - guitar_notes.size
89
+ end
90
+
91
+ def notes_left
92
+ @notes_left ||= (target_chord.notes - Coltrane::NoteSet[
93
+ *guitar_notes.map do |n|
94
+ next if n.pitch.nil?
95
+ n.pitch.pitch_class
96
+ end
97
+ ])
98
+ end
99
+
100
+ def target_notes
101
+ notes_left.any? ? notes_left : target_chord.notes
102
+ end
103
+
104
+ def notes
105
+ guitar_notes.map(&:pitch)
106
+ end
107
+
108
+ def frets
109
+ @frets ||= guitar_notes.map(&:fret)
110
+ end
111
+
112
+ def non_zero_frets
113
+ frets.reject {|f| f.nil? || f.zero? }
114
+ end
115
+
116
+ def lowest_fret
117
+ non_zero_frets.any? ? non_zero_frets.min : 0
118
+ end
119
+
120
+ def highest_fret
121
+ non_zero_frets.max || 0
122
+ end
123
+
124
+ def fret_range
125
+ (lowest_fret..highest_fret)
126
+ end
127
+
128
+ def lowest_possible_fret
129
+ lowest_fret.zero? ? 0 : [(lowest_fret - possible_span), 0].max
130
+ end
131
+
132
+ def highest_possible_fret
133
+ [(possible_span + (highest_fret == 0 ? guitar.frets : highest_fret)), guitar.frets].min
134
+ end
135
+
136
+ def possible_span
137
+ MAX_FRET_SPAN - fret_range.size
138
+ end
139
+
140
+ def fret_expansion_range
141
+ (lowest_possible_fret..highest_possible_fret).to_a +
142
+ [(0 unless barre?)].compact
143
+ end
144
+
145
+ def to_s
146
+ guitar_notes.map { |n| n.fret.nil? ? 'x' : n.fret }.join('-')
147
+ end
148
+
149
+ def voicing
150
+ Coltrane::Voicing.new(pitches: guitar_notes.map(&:pitch).compact)
151
+ end
35
152
 
36
153
  private
37
154
 
38
155
  attr_writer :barre
156
+
157
+ def next_string
158
+ guitar.strings[guitar_notes.size]
159
+ end
160
+
161
+ def possible_new_notes(include_mute_note = false)
162
+ target_notes.notes.map do |note|
163
+ next_string.find(note, possible_frets: fret_expansion_range)
164
+ end.flatten + [(Guitar::Note.new(next_string, nil) if include_mute_note)].compact
165
+ end
39
166
  end
40
167
  end
41
168
  end
@@ -3,6 +3,22 @@
3
3
  module ColtraneInstruments
4
4
  module Guitar
5
5
  class Note
6
+ attr_reader :string, :fret
7
+
8
+ def initialize(string, fret = nil)
9
+ @string = string
10
+ @fret = fret
11
+ end
12
+
13
+ def pitch
14
+ string + fret unless fret.nil?
15
+ end
16
+
17
+ def pitch_class
18
+ pitch.pitch_class unless fret.nil?
19
+ end
20
+
21
+ alias note pitch_class
6
22
  end
7
23
  end
8
24
  end
@@ -3,6 +3,27 @@
3
3
  module ColtraneInstruments
4
4
  module Guitar
5
5
  class String
6
+ attr_reader :pitch, :guitar
7
+
8
+ def initialize(pitch, guitar:)
9
+ @guitar = guitar
10
+ @pitch = pitch
11
+ end
12
+
13
+ def find(pitch_class, possible_frets: (0..guitar.frets).to_a)
14
+ output = []
15
+ n = 0
16
+ loop do
17
+ f = (pitch_class.integer - pitch.integer) % 12 + 12 * n
18
+ possible_frets.include?(f) ? output << Note.new(self, f) : break
19
+ n += 1
20
+ end
21
+ output
22
+ end
23
+
24
+ def +(fret)
25
+ pitch + fret
26
+ end
6
27
  end
7
28
  end
8
29
  end
@@ -0,0 +1,7 @@
1
+ require 'coltrane'
2
+ require 'coreaudio'
3
+
4
+ module ColtraneSynth
5
+ autoload :Base, 'coltrane_synth/base'
6
+ autoload :Synth, 'coltrane_synth/synth'
7
+ end
@@ -0,0 +1,47 @@
1
+ module ColtraneSynth
2
+ class Base
3
+ attr_reader :device, :buffer
4
+
5
+ SAMPLE_RATE = 44000
6
+
7
+ def self.play(something, duration=1.0)
8
+ new.play(*extract_frequencies(something), duration: duration)
9
+ end
10
+
11
+ def initialize
12
+ @device = CoreAudio.default_output_device
13
+ end
14
+
15
+ def play(*freqs, duration: 1.0)
16
+ @device = CoreAudio.default_output_device
17
+ buffer = @device.output_loop(SAMPLE_RATE)
18
+
19
+ SAMPLE_RATE.times do |i|
20
+ sound = freqs.reduce(0) { |sum, freq| Math.sin(phase(freq)*i) + sum }
21
+ buffer[i] = (sound * (0x6AFF/freqs.size)).round
22
+ end
23
+
24
+ buffer.start
25
+ sleep duration
26
+ buffer.stop
27
+ end
28
+
29
+ private
30
+
31
+ def self.extract_frequencies(arg)
32
+ case arg
33
+ when Coltrane::Chord then arg.notes.map {|n| n.frequency.octave(4).to_f }
34
+ when Coltrane::PitchClass then arg.frequency.octave(4).to_f
35
+ when Coltrane::Voicing then arg.frequencies.map(&:to_f)
36
+ end
37
+ end
38
+
39
+ def sine_wave(freq, i)
40
+ Math.sin(phase(freq)*i)
41
+ end
42
+
43
+ def phase(freq)
44
+ (Math::PI * 2.0 * freq) / SAMPLE_RATE.to_f
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,24 @@
1
+ module ColtraneSynth
2
+ class Synth
3
+ attr_reader :freq, :nominal_rate
4
+
5
+ def initialize(buffer, freq, nominal_rate)
6
+ @freq = freq.to_f
7
+ @nominal_rate = nominal_rate
8
+
9
+ i = -1
10
+ wav = NArray.sint(1024)
11
+
12
+ while i += 1
13
+ 1024.times do |j|
14
+ wav[j] = (0.4 * Math.sin(phase(freq) * (i * 1024 + j)) * 0x7FFF).round
15
+ end
16
+ buffer << wav
17
+ end
18
+ end
19
+
20
+ def phase(freq)
21
+ Math::PI * 2.0 * freq / nominal_rate
22
+ end
23
+ end
24
+ end
data/lib/core_ext.rb CHANGED
@@ -2,15 +2,24 @@
2
2
 
3
3
  # Stolen from Active Support and changed a bit
4
4
  # may in the future be substituted by facets version
5
+ class Regexp
6
+ def match?(*args)
7
+ !!match(*args)
8
+ end
9
+ end
10
+
5
11
  class String
6
12
  def underscore
7
13
  return self unless /[A-Z-]|::/.match?(self)
8
- word = to_s.gsub('::', '/')
9
- word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
10
- word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
11
- word.tr!('- ', '_')
12
- word.downcase!
13
- word
14
+ to_s.gsub('::', '/')
15
+ .gsub(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
16
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
17
+ .tr('- ', '_')
18
+ .downcase
19
+ end
20
+
21
+ def match?(*args)
22
+ !!match(*args)
14
23
  end
15
24
 
16
25
  def interval_quality
@@ -22,10 +31,14 @@ class String
22
31
  'd' => 'Diminished'
23
32
  }[self]
24
33
  end
34
+
35
+ def symbolize
36
+ self.underscore.to_sym
37
+ end
25
38
  end
26
39
 
27
40
  # Here we add some syntax sugar to make the code more understandable later
28
- class Integer
41
+ class Numeric
29
42
  def interval_name
30
43
  {
31
44
  1 => 'Unison',
@@ -49,11 +62,16 @@ end
49
62
 
50
63
  # Here we add some methods better work with Tries
51
64
  class Hash
65
+ def dig(*args)
66
+ args.size > 1 ? self[args.shift].dig(*args) : self[args[0]]
67
+ end
68
+
52
69
  def clone_values(from_keys: nil,
53
70
  to_keys: nil,
54
71
  suffix: nil,
55
72
  branch_a: nil,
56
73
  branch_b: nil)
74
+
57
75
  branch_a ||= dig(*from_keys)
58
76
  if branch_b.nil?
59
77
  create_branch!(*to_keys)