mtk 0.0.1 → 0.0.2
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.
- data/.yardopts +9 -0
- data/INTRO.md +73 -0
- data/LICENSE.txt +27 -0
- data/README.md +93 -18
- data/Rakefile +13 -1
- data/examples/crescendo.rb +20 -0
- data/examples/dynamic_pattern.rb +39 -0
- data/examples/play_midi.rb +19 -0
- data/examples/print_midi.rb +13 -0
- data/examples/random_tone_row.rb +18 -0
- data/examples/tone_row_melody.rb +21 -0
- data/lib/mtk/_constants/durations.rb +80 -0
- data/lib/mtk/_constants/intensities.rb +81 -0
- data/lib/mtk/{constants → _constants}/intervals.rb +10 -1
- data/lib/mtk/_constants/pitch_classes.rb +35 -0
- data/lib/mtk/_constants/pitches.rb +49 -0
- data/lib/mtk/{numeric_extensions.rb → _numeric_extensions.rb} +0 -0
- data/lib/mtk/event.rb +14 -5
- data/lib/mtk/helper/collection.rb +114 -0
- data/lib/mtk/helper/event_builder.rb +85 -0
- data/lib/mtk/{constants → helper}/pseudo_constants.rb +7 -6
- data/lib/mtk/lang/grammar.rb +17 -0
- data/lib/mtk/lang/mtk_grammar.citrus +60 -0
- data/lib/mtk/midi/file.rb +10 -15
- data/lib/mtk/midi/jsound_input.rb +68 -0
- data/lib/mtk/midi/jsound_output.rb +80 -0
- data/lib/mtk/note.rb +22 -3
- data/lib/mtk/pattern/abstract_pattern.rb +132 -0
- data/lib/mtk/pattern/choice.rb +25 -9
- data/lib/mtk/pattern/cycle.rb +51 -0
- data/lib/mtk/pattern/enumerator.rb +26 -0
- data/lib/mtk/pattern/function.rb +46 -0
- data/lib/mtk/pattern/lines.rb +60 -0
- data/lib/mtk/pattern/palindrome.rb +42 -0
- data/lib/mtk/pattern/sequence.rb +15 -50
- data/lib/mtk/pitch.rb +45 -6
- data/lib/mtk/pitch_class.rb +36 -35
- data/lib/mtk/pitch_class_set.rb +46 -14
- data/lib/mtk/pitch_set.rb +20 -31
- data/lib/mtk/sequencer/abstract_sequencer.rb +85 -0
- data/lib/mtk/sequencer/rhythmic_sequencer.rb +29 -0
- data/lib/mtk/sequencer/step_sequencer.rb +26 -0
- data/lib/mtk/timeline.rb +75 -22
- data/lib/mtk/transform/invertible.rb +15 -0
- data/lib/mtk/{util → transform}/mappable.rb +6 -2
- data/lib/mtk/transform/set_theory_operations.rb +34 -0
- data/lib/mtk/transform/transposable.rb +14 -0
- data/lib/mtk.rb +56 -22
- data/spec/mtk/_constants/durations_spec.rb +118 -0
- data/spec/mtk/{constants/dynamics_spec.rb → _constants/intensities_spec.rb} +48 -17
- data/spec/mtk/{constants → _constants}/intervals_spec.rb +21 -0
- data/spec/mtk/_constants/pitch_classes_spec.rb +58 -0
- data/spec/mtk/_constants/pitches_spec.rb +52 -0
- data/spec/mtk/{numeric_extensions_spec.rb → _numeric_extensions_spec.rb} +0 -0
- data/spec/mtk/event_spec.rb +19 -0
- data/spec/mtk/helper/collection_spec.rb +291 -0
- data/spec/mtk/helper/event_builder_spec.rb +92 -0
- data/spec/mtk/helper/pseudo_constants_spec.rb +20 -0
- data/spec/mtk/lang/grammar_spec.rb +100 -0
- data/spec/mtk/midi/file_spec.rb +41 -6
- data/spec/mtk/note_spec.rb +53 -3
- data/spec/mtk/pattern/abstract_pattern_spec.rb +45 -0
- data/spec/mtk/pattern/choice_spec.rb +89 -3
- data/spec/mtk/pattern/cycle_spec.rb +133 -0
- data/spec/mtk/pattern/function_spec.rb +133 -0
- data/spec/mtk/pattern/lines_spec.rb +93 -0
- data/spec/mtk/pattern/note_cycle_spec.rb.bak +116 -0
- data/spec/mtk/pattern/palindrome_spec.rb +124 -0
- data/spec/mtk/pattern/pitch_cycle_spec.rb.bak +47 -0
- data/spec/mtk/pattern/pitch_sequence_spec.rb.bak +37 -0
- data/spec/mtk/pattern/sequence_spec.rb +128 -31
- data/spec/mtk/pitch_class_set_spec.rb +240 -7
- data/spec/mtk/pitch_class_spec.rb +84 -18
- data/spec/mtk/pitch_set_spec.rb +45 -10
- data/spec/mtk/pitch_spec.rb +59 -0
- data/spec/mtk/sequencer/abstract_sequencer_spec.rb +159 -0
- data/spec/mtk/sequencer/rhythmic_sequencer_spec.rb +49 -0
- data/spec/mtk/sequencer/step_sequencer_spec.rb +71 -0
- data/spec/mtk/timeline_spec.rb +118 -15
- data/spec/spec_helper.rb +4 -3
- metadata +59 -22
- data/lib/mtk/chord.rb +0 -47
- data/lib/mtk/constants/dynamics.rb +0 -56
- data/lib/mtk/constants/pitch_classes.rb +0 -18
- data/lib/mtk/constants/pitches.rb +0 -24
- data/lib/mtk/pattern/note_sequence.rb +0 -60
- data/lib/mtk/pattern/pitch_sequence.rb +0 -22
- data/lib/mtk/patterns.rb +0 -4
- data/spec/mtk/chord_spec.rb +0 -74
- data/spec/mtk/constants/pitch_classes_spec.rb +0 -35
- data/spec/mtk/constants/pitches_spec.rb +0 -23
- data/spec/mtk/pattern/note_sequence_spec.rb +0 -121
- data/spec/mtk/pattern/pitch_sequence_spec.rb +0 -47
@@ -0,0 +1,132 @@
|
|
1
|
+
module MTK
|
2
|
+
module Pattern
|
3
|
+
|
4
|
+
# A pattern of elements that can be emitted one element at a time via calls to {#next}.
|
5
|
+
#
|
6
|
+
# Patterns can be reset to the beginning via {#rewind}.
|
7
|
+
#
|
8
|
+
# @abstract Subclass and override {#advance!} and {#current} to implement a Pattern
|
9
|
+
#
|
10
|
+
class AbstractPattern
|
11
|
+
include Helper::Collection
|
12
|
+
include Enumerator
|
13
|
+
|
14
|
+
# The elements in the pattern
|
15
|
+
attr_reader :elements
|
16
|
+
|
17
|
+
attr_reader :options
|
18
|
+
|
19
|
+
# The type of elements in the pattern, such as :pitch, :intensity, or :duration
|
20
|
+
#
|
21
|
+
# This is often needed by {Sequencer} classes to interpret the pattern elements.
|
22
|
+
attr_reader :type
|
23
|
+
|
24
|
+
# The number of elements emitted since the last {#rewind}
|
25
|
+
attr_reader :element_count
|
26
|
+
|
27
|
+
# The maximum number of elements this Pattern will emit before a StopIteration exception
|
28
|
+
attr_reader :max_elements
|
29
|
+
|
30
|
+
# @param elements [Enumerable, #to_a] the list of elements in the pattern
|
31
|
+
# @param options [Hash] the pattern options
|
32
|
+
# @option options [String] :type the pattern {#type}
|
33
|
+
# @option options [Fixnum] :max_elements the {#max_elements}
|
34
|
+
def initialize(elements, options={})
|
35
|
+
elements = elements.to_a if elements.respond_to? :to_a and not elements.is_a? Proc # Proc check prevents warnings in Ruby 1.8
|
36
|
+
@elements = elements
|
37
|
+
@options = options
|
38
|
+
@type = options[:type]
|
39
|
+
@max_elements = options[:max_elements]
|
40
|
+
rewind
|
41
|
+
end
|
42
|
+
|
43
|
+
# Construct a pattern from an Array.
|
44
|
+
# @param (see #initialize)
|
45
|
+
# @option (see #initialize)
|
46
|
+
# @see #initialize
|
47
|
+
def self.from_a(elements, options={})
|
48
|
+
new(elements, options)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Reset the pattern to the beginning
|
52
|
+
def rewind
|
53
|
+
@current = nil
|
54
|
+
@element_count = 0
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
# Emit the next element in the pattern
|
59
|
+
# @raise StopIteration when the pattern has emitted all values, or has hit the {#max_elements} limit.
|
60
|
+
def next
|
61
|
+
if @current.is_a? Enumerator
|
62
|
+
begin
|
63
|
+
subpattern_next = @current.next
|
64
|
+
subpattern_has_next = true
|
65
|
+
rescue StopIteration
|
66
|
+
subpattern_has_next = false
|
67
|
+
end
|
68
|
+
|
69
|
+
return emit subpattern_next if subpattern_has_next
|
70
|
+
# else fall through and continue with normal behavior
|
71
|
+
end
|
72
|
+
|
73
|
+
begin
|
74
|
+
advance!
|
75
|
+
rescue StopIteration
|
76
|
+
@current = nil
|
77
|
+
raise
|
78
|
+
end
|
79
|
+
|
80
|
+
@current = current
|
81
|
+
if @current.is_a? Enumerator
|
82
|
+
@current.rewind # start over, in case we already enumerated this element and then did a rewind
|
83
|
+
return self.next
|
84
|
+
end
|
85
|
+
|
86
|
+
emit @current
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
##################
|
91
|
+
protected
|
92
|
+
|
93
|
+
# Update internal state (index, etc) so that {#current} will refer to the next element.
|
94
|
+
# @note Override this method in a subclass to define a custom Pattern.
|
95
|
+
# @raise StopIteration if there are no more elements
|
96
|
+
def advance!
|
97
|
+
raise StopIteration if @elements.nil? or @elements.empty?
|
98
|
+
end
|
99
|
+
|
100
|
+
# The current element in the pattern, which will be returned by {#next} (after a call to {#advance!}).
|
101
|
+
# @note Override this method in a subclass to define a custom Pattern.
|
102
|
+
def current
|
103
|
+
@elements[0]
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
##################
|
108
|
+
private
|
109
|
+
|
110
|
+
def emit element
|
111
|
+
@element_count += 1
|
112
|
+
raise StopIteration if @max_elements and @element_count > @max_elements
|
113
|
+
element
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Build any "TypedPattern" (like PitchCycle or DurationPalindrome) or even just Pattern
|
118
|
+
def method_missing(method, *args, &block)
|
119
|
+
# Assuming we get something like PitchCycle, split into 'Pitch' and 'Cycle'
|
120
|
+
camel_case_words = method.to_s.gsub(/([a-z])([A-Z])/,'\1 \2').split(' ')
|
121
|
+
pattern = MTK::Pattern.const_get camel_case_words.last
|
122
|
+
if camel_case_words.length > 1
|
123
|
+
type = camel_case_words.first.downcase.to_sym
|
124
|
+
pattern.new(args, :type => type)
|
125
|
+
else
|
126
|
+
pattern.new(args)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
module_function :method_missing
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
data/lib/mtk/pattern/choice.rb
CHANGED
@@ -1,18 +1,34 @@
|
|
1
1
|
module MTK
|
2
2
|
module Pattern
|
3
3
|
|
4
|
-
#
|
5
|
-
|
4
|
+
# Randomly choose from a list of elements.
|
5
|
+
#
|
6
|
+
# Supports giving different weights to different choices.
|
7
|
+
# Default is to weight all choices equally.
|
8
|
+
#
|
9
|
+
class Choice < AbstractPattern
|
6
10
|
|
7
|
-
#
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
11
|
+
# @param (see AbstractPattern#initialize)
|
12
|
+
# @option (see AbstractPattern#initialize)
|
13
|
+
# @option options [Array] :weights a list of chances that each corresponding element will be selected (normalized against the total weight)
|
14
|
+
# @example choose the second element twice as often as the first or third:
|
15
|
+
# MTK::Pattern::Choice.new [:first,:second,:third], :weights => [1,2,1]
|
16
|
+
def initialize(elements, options={})
|
17
|
+
super
|
18
|
+
@weights = options.fetch :weights, Array.new(@elements.length, 1)
|
19
|
+
@total_weight = @weights.inject(:+).to_f
|
12
20
|
end
|
13
21
|
|
14
|
-
|
15
|
-
|
22
|
+
#####################
|
23
|
+
protected
|
24
|
+
|
25
|
+
# (see AbstractPattern#current)
|
26
|
+
def current
|
27
|
+
target = rand * @total_weight
|
28
|
+
@weights.each_with_index do |weight,index|
|
29
|
+
return @elements[index] if target < weight
|
30
|
+
target -= weight
|
31
|
+
end
|
16
32
|
end
|
17
33
|
|
18
34
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module MTK
|
2
|
+
module Pattern
|
3
|
+
|
4
|
+
# An endless enumerator that outputs an element one at a time from a list of elements,
|
5
|
+
# looping back to the beginning when elements run out.
|
6
|
+
class Cycle < AbstractPattern
|
7
|
+
|
8
|
+
# The number of cycles emitted (1 cycle == all elements emitted) since the last {#rewind}
|
9
|
+
attr_reader :cycle_count
|
10
|
+
|
11
|
+
# The maximum number of cycles this Pattern will emit before a StopIteration exception
|
12
|
+
attr_reader :max_cycles
|
13
|
+
|
14
|
+
def initialize(elements, options={})
|
15
|
+
super
|
16
|
+
@max_cycles = options[:max_cycles]
|
17
|
+
end
|
18
|
+
|
19
|
+
# Reset the sequence to the beginning
|
20
|
+
def rewind
|
21
|
+
@index = -1
|
22
|
+
@cycle_count = 0
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
###################
|
27
|
+
protected
|
28
|
+
|
29
|
+
# (see AbstractPattern#advance!)
|
30
|
+
def advance!
|
31
|
+
super # base advance!() implementation prevents infinite loops with empty patterns
|
32
|
+
@index += 1
|
33
|
+
if @index >= @elements.length
|
34
|
+
@cycle_count += 1
|
35
|
+
if @max_cycles and @cycle_count >= @max_cycles
|
36
|
+
raise StopIteration
|
37
|
+
end
|
38
|
+
@index = -1
|
39
|
+
advance!
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# (see AbstractPattern#current)
|
44
|
+
def current
|
45
|
+
@elements[@index]
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module MTK
|
2
|
+
module Pattern
|
3
|
+
|
4
|
+
# The core interface for all Patterns.
|
5
|
+
#
|
6
|
+
# This module doesn't provide any useful default functionality.
|
7
|
+
# It only indicates that a class is compatible with MTK's pattern enumerator interface.
|
8
|
+
#
|
9
|
+
module Enumerator
|
10
|
+
|
11
|
+
# Return the next element in the enumerator
|
12
|
+
# @raise StopIteration when no more elements are available
|
13
|
+
def next
|
14
|
+
raise StopIteration
|
15
|
+
end
|
16
|
+
|
17
|
+
# Reset the enumerator to the beginning
|
18
|
+
# @return self
|
19
|
+
def rewind
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module MTK
|
2
|
+
module Pattern
|
3
|
+
|
4
|
+
# An arbitrary function that dynamically generates elements.
|
5
|
+
#
|
6
|
+
class Function < AbstractPattern
|
7
|
+
|
8
|
+
attr_reader :function
|
9
|
+
|
10
|
+
def initialize(elements, options={})
|
11
|
+
super
|
12
|
+
@function = @elements
|
13
|
+
# unpack from the varargs Array that may be passed in from the "convenience constructor methods" defined in MTK::Pattern \
|
14
|
+
@function = @function.first if @function.is_a? Enumerable
|
15
|
+
end
|
16
|
+
|
17
|
+
# Reset the sequence to the beginning
|
18
|
+
def rewind
|
19
|
+
@prev = nil
|
20
|
+
@function_call_count = -1
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
###################
|
25
|
+
protected
|
26
|
+
|
27
|
+
# (see AbstractPattern#advance!)
|
28
|
+
def advance!
|
29
|
+
raise StopIteration if @elements.nil?
|
30
|
+
end
|
31
|
+
|
32
|
+
# (see AbstractPattern#current)
|
33
|
+
def current
|
34
|
+
@function_call_count += 1
|
35
|
+
@prev = case @function.arity
|
36
|
+
when 0 then @function.call
|
37
|
+
when 1 then @function.call(@prev)
|
38
|
+
when 2 then @function.call(@prev, @function_call_count)
|
39
|
+
else @function.call(@prev, @function_call_count, @element_count)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module MTK
|
2
|
+
module Pattern
|
3
|
+
|
4
|
+
# A piecewise linear function (see {http://en.wikipedia.org/wiki/File:PiecewiseLinear.png}) defined in terms
|
5
|
+
# of [value, steps_to_reach_value] pairs.
|
6
|
+
#
|
7
|
+
# The "steps_to_reach_value" for the first element is ignored and may be omitted, since it takes 0 steps to start.
|
8
|
+
class Lines < AbstractPattern
|
9
|
+
|
10
|
+
# Reset the sequence to the beginning
|
11
|
+
def rewind
|
12
|
+
@index = -1
|
13
|
+
@steps = -1
|
14
|
+
@step_count = -1
|
15
|
+
@prev = nil
|
16
|
+
@next = nil
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
###################
|
21
|
+
protected
|
22
|
+
|
23
|
+
# (see AbstractPattern#advance!)
|
24
|
+
def advance!
|
25
|
+
super
|
26
|
+
|
27
|
+
while @step_count >= @steps
|
28
|
+
@step_count = 0
|
29
|
+
|
30
|
+
@index += 1
|
31
|
+
raise StopIteration if @index >= @elements.length
|
32
|
+
|
33
|
+
@prev = @next
|
34
|
+
next_elem = @elements[@index]
|
35
|
+
if next_elem.is_a? Array
|
36
|
+
@next = next_elem.first
|
37
|
+
@steps = next_elem.last.to_f
|
38
|
+
else
|
39
|
+
@next = next_elem
|
40
|
+
@steps = 1.0
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
@step_count += 1
|
45
|
+
end
|
46
|
+
|
47
|
+
# (see AbstractPattern#current)
|
48
|
+
def current
|
49
|
+
if @prev and @next
|
50
|
+
# linear interpolation
|
51
|
+
@prev + (@next - @prev)*@step_count/@steps
|
52
|
+
else
|
53
|
+
@next
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module MTK
|
2
|
+
module Pattern
|
3
|
+
|
4
|
+
# An endless enumerator that outputs an element one at a time from a list of elements,
|
5
|
+
# looping back to the beginning when elements run out.
|
6
|
+
class Palindrome < Cycle
|
7
|
+
|
8
|
+
def rewind
|
9
|
+
@direction = 1
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
# true if the first/last element are repeated when the ends are reached, else false
|
14
|
+
def repeat_ends?
|
15
|
+
@repeat_ends ||= @options.fetch :repeat_ends, false
|
16
|
+
end
|
17
|
+
|
18
|
+
##############
|
19
|
+
protected
|
20
|
+
|
21
|
+
# (see AbstractPattern#advance!)
|
22
|
+
def advance!
|
23
|
+
raise StopIteration if @elements.nil? or @elements.empty? # prevent infinite loops
|
24
|
+
|
25
|
+
@index += @direction
|
26
|
+
|
27
|
+
if @index >= @elements.length
|
28
|
+
@direction = -1
|
29
|
+
@index = @elements.length - 1
|
30
|
+
@index -= 1 unless repeat_ends? or @elements.length == 1
|
31
|
+
|
32
|
+
elsif @index < 0
|
33
|
+
@direction = 1
|
34
|
+
@index = 0
|
35
|
+
@index += 1 unless repeat_ends? or @elements.length == 1
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
data/lib/mtk/pattern/sequence.rb
CHANGED
@@ -1,64 +1,29 @@
|
|
1
1
|
module MTK
|
2
2
|
module Pattern
|
3
3
|
|
4
|
-
#
|
5
|
-
|
6
|
-
class Sequence
|
4
|
+
# A finite list of elements, which can be enumerated one at a time.
|
5
|
+
class Sequence < AbstractPattern
|
7
6
|
|
8
|
-
#
|
9
|
-
|
10
|
-
|
11
|
-
# @param elements [Array] the list of {#elements}
|
12
|
-
# @param default [Object] the default value returned by {#next} in place of nil
|
13
|
-
def initialize(elements)
|
14
|
-
if elements.respond_to? :elements
|
15
|
-
@elements = elements.elements
|
16
|
-
else
|
17
|
-
@elements = elements.to_a
|
18
|
-
end
|
19
|
-
reset
|
20
|
-
end
|
21
|
-
|
22
|
-
# reset the Sequence to its initial state
|
23
|
-
def reset
|
7
|
+
# Reset the sequence to the beginning
|
8
|
+
def rewind
|
24
9
|
@index = -1
|
25
|
-
|
26
|
-
@value = nil
|
10
|
+
super
|
27
11
|
end
|
28
12
|
|
29
|
-
|
30
|
-
# The sequence goes back to the first element if there are no more elements.
|
31
|
-
#
|
32
|
-
# @return if an element is a Proc, it is called (depending on the arity of the Proc: with either no arguments,
|
33
|
-
# the last value of #next, or the last value and last element) and its return value is returned.
|
34
|
-
# @return if an element is nil, previous value of #next is returned. If there is no previous value the @default is returned.
|
35
|
-
# @return otherwise the element itself is returned
|
36
|
-
#
|
37
|
-
def next
|
38
|
-
if @elements and not @elements.empty?
|
39
|
-
@index = (@index + 1) % @elements.length
|
40
|
-
element = @elements[@index]
|
41
|
-
value = value_of(element)
|
42
|
-
@element, @value = element, value
|
43
|
-
end
|
44
|
-
@value
|
45
|
-
end
|
46
|
-
|
47
|
-
####################
|
13
|
+
###################
|
48
14
|
protected
|
49
15
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
when 1 then element.call(@value)
|
56
|
-
else element.call(@value, @element)
|
57
|
-
end
|
58
|
-
else element
|
59
|
-
end
|
16
|
+
# (see AbstractPattern#advance!)
|
17
|
+
def advance!
|
18
|
+
super
|
19
|
+
@index += 1
|
20
|
+
raise StopIteration if @index >= @elements.length
|
60
21
|
end
|
61
22
|
|
23
|
+
# (see AbstractPattern#current)
|
24
|
+
def current
|
25
|
+
@elements[@index]
|
26
|
+
end
|
62
27
|
end
|
63
28
|
|
64
29
|
end
|
data/lib/mtk/pitch.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
module MTK
|
2
2
|
|
3
3
|
# A frequency represented by a {PitchClass}, an integer octave, and an offset in semitones.
|
4
|
-
|
5
4
|
class Pitch
|
6
5
|
|
7
6
|
include Comparable
|
@@ -15,28 +14,49 @@ module MTK
|
|
15
14
|
|
16
15
|
@flyweight = {}
|
17
16
|
|
17
|
+
# Return a pitch with no offset, only constructing a new instance when not already in the flyweight cache
|
18
18
|
def self.[](pitch_class, octave)
|
19
|
+
pitch_class = MTK.PitchClass(pitch_class)
|
19
20
|
@flyweight[[pitch_class,octave]] ||= new(pitch_class, octave)
|
20
21
|
end
|
21
|
-
|
22
|
+
|
23
|
+
# Lookup a pitch by name, which consists of any {PitchClass::VALID_NAMES} and an octave number.
|
24
|
+
# The name may also be optionally suffixed by +/-###cents (where ### is any number).
|
25
|
+
# @example get the Pitch for middle C :
|
26
|
+
# Pitch.from_s('C4')
|
27
|
+
# @example get the Pitch for middle C + 50 cents:
|
28
|
+
# Pitch.from_s('C4+50cents')
|
22
29
|
def self.from_s( s )
|
23
|
-
|
30
|
+
s = s.to_s
|
24
31
|
s = s[0..0].upcase + s[1..-1].downcase # normalize name
|
25
|
-
if s =~ /^([A-G](#|##|b|bb)?)(-?\d+)
|
32
|
+
if s =~ /^([A-G](#|##|b|bb)?)(-?\d+)(\+(\d+(\.\d+)?)cents)?$/
|
26
33
|
pitch_class = PitchClass.from_s($1)
|
27
34
|
if pitch_class
|
28
35
|
octave = $3.to_i
|
29
|
-
|
36
|
+
offset_in_cents = $5.to_f
|
37
|
+
if offset_in_cents.nil? or offset_in_cents.zero?
|
38
|
+
self[pitch_class, octave]
|
39
|
+
else
|
40
|
+
new( pitch_class, octave, offset_in_cents/100.0 )
|
41
|
+
end
|
30
42
|
end
|
31
43
|
end
|
32
44
|
end
|
45
|
+
|
46
|
+
class << self
|
47
|
+
alias :from_name :from_s
|
48
|
+
end
|
33
49
|
|
34
50
|
# Convert a Numeric semitones value into a Pitch
|
35
51
|
def self.from_f( f )
|
36
52
|
i, offset = f.floor, f%1 # split into int and fractional part
|
37
53
|
pitch_class = PitchClass.from_i(i)
|
38
54
|
octave = i/12 - 1
|
39
|
-
|
55
|
+
if offset == 0
|
56
|
+
self[pitch_class, octave]
|
57
|
+
else
|
58
|
+
new( pitch_class, octave, offset )
|
59
|
+
end
|
40
60
|
end
|
41
61
|
|
42
62
|
def self.from_hash(hash)
|
@@ -86,6 +106,7 @@ module MTK
|
|
86
106
|
def + interval_in_semitones
|
87
107
|
self.class.from_f( @value + interval_in_semitones.to_f )
|
88
108
|
end
|
109
|
+
alias transpose +
|
89
110
|
|
90
111
|
def - interval_in_semitones
|
91
112
|
self.class.from_f( @value - interval_in_semitones.to_f )
|
@@ -109,4 +130,22 @@ module MTK
|
|
109
130
|
|
110
131
|
end
|
111
132
|
|
133
|
+
# Construct a {Pitch} from any supported type
|
134
|
+
def Pitch(*anything)
|
135
|
+
anything = anything.first if anything.length == 1
|
136
|
+
case anything
|
137
|
+
when Numeric then Pitch.from_f(anything)
|
138
|
+
when String, Symbol then Pitch.from_s(anything)
|
139
|
+
when Pitch then anything
|
140
|
+
when Array
|
141
|
+
if anything.length == 2
|
142
|
+
Pitch[*anything]
|
143
|
+
else
|
144
|
+
Pitch.new(*anything)
|
145
|
+
end
|
146
|
+
else raise "Pitch doesn't understand #{anything.class}"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
module_function :Pitch
|
150
|
+
|
112
151
|
end
|
data/lib/mtk/pitch_class.rb
CHANGED
@@ -6,9 +6,9 @@ module MTK
|
|
6
6
|
|
7
7
|
class PitchClass
|
8
8
|
|
9
|
-
NAMES = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']
|
9
|
+
NAMES = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'].freeze
|
10
10
|
|
11
|
-
|
11
|
+
VALID_NAMES_BY_VALUE = [
|
12
12
|
['B#', 'C', 'Dbb'],
|
13
13
|
['B##', 'C#', 'Db'],
|
14
14
|
['C##', 'D', 'Ebb'],
|
@@ -21,55 +21,40 @@ module MTK
|
|
21
21
|
['G##', 'A', 'Bbb'],
|
22
22
|
['A#', 'Bb', 'Cbb'],
|
23
23
|
['A##', 'B', 'Cb']
|
24
|
-
]
|
24
|
+
].freeze
|
25
25
|
|
26
|
-
|
26
|
+
VALID_NAMES = VALID_NAMES_BY_VALUE.flatten.freeze
|
27
27
|
|
28
|
-
|
29
|
-
private
|
28
|
+
attr_reader :name
|
30
29
|
|
31
30
|
def initialize(name, int_value)
|
32
31
|
@name, @int_value = name, int_value
|
33
32
|
end
|
34
|
-
|
35
33
|
private_class_method :new
|
36
34
|
|
37
35
|
@flyweight = {}
|
38
36
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
def self.from_s(s)
|
49
|
-
s = s.to_s
|
50
|
-
s = s[0..0].upcase + s[1..-1].downcase # normalize the name
|
51
|
-
VALID_NAMES.each_with_index do |names, index|
|
52
|
-
return get(s, index) if names.include? s
|
37
|
+
# Lookup a PitchClass by name.
|
38
|
+
# @param name [String, #to_s] one of the values in {VALID_NAMES}
|
39
|
+
def self.[](name)
|
40
|
+
name = name.to_s
|
41
|
+
name = name[0..0].upcase + name[1..-1].downcase # normalize the name
|
42
|
+
VALID_NAMES_BY_VALUE.each_with_index do |names, value|
|
43
|
+
if names.include? name
|
44
|
+
return @flyweight[name] ||= new(name, value)
|
45
|
+
end
|
53
46
|
end
|
54
47
|
nil
|
55
48
|
end
|
56
49
|
|
57
50
|
class << self
|
58
|
-
alias
|
51
|
+
alias :from_s :[]
|
52
|
+
alias :from_name :[]
|
59
53
|
end
|
60
54
|
|
61
55
|
def self.from_i(value)
|
62
|
-
|
63
|
-
name
|
64
|
-
get(name, value)
|
65
|
-
end
|
66
|
-
|
67
|
-
def self.[](name_or_value)
|
68
|
-
if name_or_value.kind_of? Numeric
|
69
|
-
from_i(name_or_value)
|
70
|
-
else
|
71
|
-
from_s(name_or_value)
|
72
|
-
end
|
56
|
+
name = NAMES[value.to_i % 12]
|
57
|
+
self[name]
|
73
58
|
end
|
74
59
|
|
75
60
|
def == other
|
@@ -88,14 +73,19 @@ module MTK
|
|
88
73
|
@int_value
|
89
74
|
end
|
90
75
|
|
91
|
-
def +
|
76
|
+
def + interval
|
92
77
|
self.class.from_i(to_i + interval.to_i)
|
93
78
|
end
|
79
|
+
alias transpose +
|
94
80
|
|
95
|
-
def -
|
81
|
+
def - interval
|
96
82
|
self.class.from_i(to_i - interval.to_i)
|
97
83
|
end
|
98
84
|
|
85
|
+
def invert(center_pitch_class)
|
86
|
+
self + 2*(center_pitch_class.to_i - to_i)
|
87
|
+
end
|
88
|
+
|
99
89
|
# the smallest interval in semitones that needs to be added to this PitchClass to reach the given PitchClass
|
100
90
|
def distance_to(pitch_class)
|
101
91
|
delta = (pitch_class.to_i - to_i) % 12
|
@@ -110,4 +100,15 @@ module MTK
|
|
110
100
|
|
111
101
|
end
|
112
102
|
|
103
|
+
# Construct a {PitchClass} from any supported type
|
104
|
+
def PitchClass(anything)
|
105
|
+
case anything
|
106
|
+
when Numeric then PitchClass.from_i(anything)
|
107
|
+
when String, Symbol then PitchClass.from_s(anything)
|
108
|
+
when PitchClass then anything
|
109
|
+
else raise "PitchClass doesn't understand #{anything.class}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
module_function :PitchClass
|
113
|
+
|
113
114
|
end
|