diamond 0.5.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 +7 -0
- data/LICENSE +13 -0
- data/README.md +126 -0
- data/lib/diamond.rb +35 -0
- data/lib/diamond/api.rb +102 -0
- data/lib/diamond/arpeggiator.rb +81 -0
- data/lib/diamond/clock.rb +88 -0
- data/lib/diamond/midi.rb +165 -0
- data/lib/diamond/osc.rb +102 -0
- data/lib/diamond/pattern.rb +95 -0
- data/lib/diamond/sequence.rb +178 -0
- data/lib/diamond/sequence_parameters.rb +164 -0
- data/test/api_test.rb +152 -0
- data/test/arpeggiator_test.rb +34 -0
- data/test/helper.rb +23 -0
- data/test/osc_test.rb +37 -0
- data/test/pattern_test.rb +63 -0
- data/test/sequence_parameters_test.rb +96 -0
- data/test/sequence_test.rb +36 -0
- metadata +222 -0
@@ -0,0 +1,164 @@
|
|
1
|
+
module Diamond
|
2
|
+
|
3
|
+
# User-controller parameters that are used to formulate the note event sequence
|
4
|
+
class SequenceParameters
|
5
|
+
|
6
|
+
RANGE = {
|
7
|
+
:gate => 1..500,
|
8
|
+
:interval => -48..48,
|
9
|
+
:pattern_offset => -16..16,
|
10
|
+
:range => 0..10,
|
11
|
+
:rate => 0..64,
|
12
|
+
:transpose => -64..64
|
13
|
+
}
|
14
|
+
|
15
|
+
attr_reader :gate,
|
16
|
+
:interval,
|
17
|
+
:pattern,
|
18
|
+
:pattern_offset,
|
19
|
+
:range,
|
20
|
+
:rate,
|
21
|
+
:resolution
|
22
|
+
|
23
|
+
# @param [Sequence] sequence
|
24
|
+
# @param [Fixnum] resolution
|
25
|
+
# @param [Hash] options
|
26
|
+
# @option options [Fixnum] :gate Duration of the arpeggiated notes. The value is a percentage based on the rate. If the rate is 4, then a gate of 100 is equal to a quarter note. (default: 75). must be 1..500
|
27
|
+
# @option options [Fixnum] :interval Increment (pattern) over (interval) scale degrees (range) times. May be positive or negative. (default: 12)
|
28
|
+
# @option options [Fixnum] :pattern_offset Begin on the nth note of the sequence (but not omit any notes). (default: 0)
|
29
|
+
# @option options [String, Pattern] :pattern Computes the contour of the arpeggiated melody. Can be the name of a pattern or a pattern object.
|
30
|
+
# @option options [Fixnum] :range Increment the (pattern) over (interval) scale degrees (range) times. Must be positive (abs will be used). (default: 3)
|
31
|
+
# @option options [Fixnum] :rate How fast the arpeggios will be played. Must be positive (abs will be used). (default: 8, eighth note.) must be 0..resolution
|
32
|
+
# @param [Proc] callback
|
33
|
+
def initialize(sequence, resolution, options = {}, &callback)
|
34
|
+
@transpose = 0
|
35
|
+
@resolution = resolution
|
36
|
+
@callback = callback
|
37
|
+
apply_options(options)
|
38
|
+
sequence.send(:use_parameters, self)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Set the gate property
|
42
|
+
# @param [Fixnum] num
|
43
|
+
# @return [Fixnum]
|
44
|
+
def gate=(num)
|
45
|
+
@gate = constrain(num, :range => RANGE[:gate])
|
46
|
+
mark_changed
|
47
|
+
@gate
|
48
|
+
end
|
49
|
+
|
50
|
+
# Set the interval property
|
51
|
+
# @param [Fixnum] num
|
52
|
+
# @param [Fixnum]
|
53
|
+
def interval=(num)
|
54
|
+
@interval = num
|
55
|
+
mark_changed
|
56
|
+
@interval
|
57
|
+
end
|
58
|
+
|
59
|
+
# Set the pattern offset property
|
60
|
+
# @param [Fixnum] num
|
61
|
+
# @return [Fixnum]
|
62
|
+
def pattern_offset=(num)
|
63
|
+
@pattern_offset = num
|
64
|
+
mark_changed
|
65
|
+
@pattern_offset
|
66
|
+
end
|
67
|
+
|
68
|
+
# Set the range property
|
69
|
+
# @param [Fixnum] range
|
70
|
+
# @return [Fixnum]
|
71
|
+
def range=(num)
|
72
|
+
@range = constrain(num, :range => RANGE[:range])
|
73
|
+
mark_changed
|
74
|
+
@range
|
75
|
+
end
|
76
|
+
|
77
|
+
# Set the rate property
|
78
|
+
# @param [Fixnum] num
|
79
|
+
# @return [Fixnum]
|
80
|
+
def rate=(num)
|
81
|
+
@rate = constrain(num, :range => RANGE[:rate].begin..@resolution)
|
82
|
+
mark_changed
|
83
|
+
@rate
|
84
|
+
end
|
85
|
+
|
86
|
+
# Set the pattern property
|
87
|
+
# @param [Pattern] pattern
|
88
|
+
# @return [Pattern]
|
89
|
+
def pattern=(pattern)
|
90
|
+
@pattern = pattern
|
91
|
+
mark_changed
|
92
|
+
@pattern
|
93
|
+
end
|
94
|
+
|
95
|
+
# Transpose everything by the given number of scale degrees. Can be used as a getter
|
96
|
+
# @param [Fixnum, nil] num
|
97
|
+
# @return [Fixnum, nil]
|
98
|
+
def transpose(num = nil)
|
99
|
+
@transpose = num unless num.nil?
|
100
|
+
mark_changed
|
101
|
+
@transpose
|
102
|
+
end
|
103
|
+
alias_method :transpose=, :transpose
|
104
|
+
|
105
|
+
# The computed pattern given the sequence options
|
106
|
+
# @return [Array<Fixnum>]
|
107
|
+
def computed_pattern
|
108
|
+
@pattern.compute(@range, @interval)
|
109
|
+
end
|
110
|
+
|
111
|
+
# The note duration given the sequence options
|
112
|
+
# @return [Numeric]
|
113
|
+
def duration
|
114
|
+
@resolution / @rate
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
# Mark that there's been a change in the sequence
|
120
|
+
def mark_changed
|
121
|
+
@callback.call
|
122
|
+
end
|
123
|
+
|
124
|
+
# @param [Hash] options
|
125
|
+
# @return [ArpeggiatorSequence::Parameters]
|
126
|
+
def apply_options(options)
|
127
|
+
@interval = constrain((options[:interval] || 12), :range => RANGE[:interval])
|
128
|
+
@range = constrain((options[:range] || 3), :range => RANGE[:range])
|
129
|
+
@pattern_offset = constrain((options[:pattern_offset] || 0),:range => RANGE[:pattern_offset])
|
130
|
+
@rate = constrain((options[:rate] || 8), :range => 0..@resolution)
|
131
|
+
@gate = constrain((options[:gate] || 75), :range => RANGE[:gate])
|
132
|
+
@pattern = get_pattern(options[:pattern])
|
133
|
+
self
|
134
|
+
end
|
135
|
+
|
136
|
+
# Derive a pattern from the options or using the default
|
137
|
+
# @param [Pattern, String, nil] option
|
138
|
+
# @return [Pattern, nil]
|
139
|
+
def get_pattern(option)
|
140
|
+
@pattern = case option
|
141
|
+
when Pattern then option
|
142
|
+
when String then Pattern.find(option)
|
143
|
+
end
|
144
|
+
@pattern ||= Pattern.first
|
145
|
+
end
|
146
|
+
|
147
|
+
# Constrain the given value based on the sequence options
|
148
|
+
# @param [Numeric] value
|
149
|
+
# @param [Hash] options
|
150
|
+
# @option options [Numeric] :min
|
151
|
+
# @option options [Numeric] :max
|
152
|
+
# @option options [Range] :range
|
153
|
+
# @return [Numeric]
|
154
|
+
def constrain(value, options = {})
|
155
|
+
min = options[:range].nil? ? options[:min] : options[:range].begin
|
156
|
+
max = options[:range].nil? ? options[:max] : options[:range].end
|
157
|
+
new_value = value
|
158
|
+
new_value = min.nil? ? new_value : [new_value, min].max
|
159
|
+
new_value = max.nil? ? new_value : [new_value, max].min
|
160
|
+
new_value
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
end
|
data/test/api_test.rb
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
class Diamond::APITest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
context "API" do
|
6
|
+
|
7
|
+
context "MIDI" do
|
8
|
+
|
9
|
+
context "#omni_on" do
|
10
|
+
|
11
|
+
setup do
|
12
|
+
@messages = [
|
13
|
+
MIDIMessage::NoteOn["C4"].new(10, 100),
|
14
|
+
MIDIMessage::NoteOn["C4"].new(3, 100),
|
15
|
+
MIDIMessage::NoteOn["C4"].new(2, 100)
|
16
|
+
]
|
17
|
+
@arpeggiator = Diamond::Arpeggiator.new(:rx_channel => 3)
|
18
|
+
end
|
19
|
+
|
20
|
+
should "not acknowledge message with wrong rx channel" do
|
21
|
+
@arpeggiator.add(@messages[0])
|
22
|
+
assert_empty @arpeggiator.sequence.instance_variable_get("@input_queue")
|
23
|
+
end
|
24
|
+
|
25
|
+
should "acknowledge message with rx channel" do
|
26
|
+
@arpeggiator.add(@messages[1])
|
27
|
+
@arpeggiator.sequence.instance_variable_get("@input_queue").expects(:concat).once.with([@messages[1]])
|
28
|
+
end
|
29
|
+
|
30
|
+
should "with omni on, acknowledge any rx channel" do
|
31
|
+
@arpeggiator.omni_on
|
32
|
+
@arpeggiator.add(@messages[2])
|
33
|
+
@arpeggiator.sequence.instance_variable_get("@input_queue").expects(:concat).once.with([@messages[2]])
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
context "#add_midi_source" do
|
39
|
+
|
40
|
+
setup do
|
41
|
+
@input = $test_device[:input]
|
42
|
+
@arpeggiator = Diamond::Arpeggiator.new
|
43
|
+
refute @arpeggiator.midi_sources.include?(@input)
|
44
|
+
end
|
45
|
+
|
46
|
+
should "add a midi source" do
|
47
|
+
@arpeggiator.add_midi_source(@input)
|
48
|
+
assert_not_empty @arpeggiator.midi_sources
|
49
|
+
assert @arpeggiator.midi_sources.include?(@input)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
context "#remove_midi_source" do
|
55
|
+
|
56
|
+
setup do
|
57
|
+
@input = $test_device[:input]
|
58
|
+
@arpeggiator = Diamond::Arpeggiator.new
|
59
|
+
@arpeggiator.add_midi_source(@input)
|
60
|
+
assert_not_empty @arpeggiator.midi_sources
|
61
|
+
assert @arpeggiator.midi_sources.include?(@input)
|
62
|
+
end
|
63
|
+
|
64
|
+
should "remove a midi source" do
|
65
|
+
@arpeggiator.remove_midi_source(@input)
|
66
|
+
refute @arpeggiator.midi_sources.include?(@input)
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
context "#mute" do
|
72
|
+
|
73
|
+
setup do
|
74
|
+
@arpeggiator = Diamond::Arpeggiator.new
|
75
|
+
end
|
76
|
+
|
77
|
+
should "mute the arpeggiator" do
|
78
|
+
refute @arpeggiator.muted?
|
79
|
+
@arpeggiator.mute = true
|
80
|
+
assert @arpeggiator.muted?
|
81
|
+
@arpeggiator.mute = false
|
82
|
+
refute @arpeggiator.muted?
|
83
|
+
@arpeggiator.toggle_mute
|
84
|
+
assert @arpeggiator.muted?
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
context "SequenceParameters" do
|
92
|
+
|
93
|
+
setup do
|
94
|
+
@arpeggiator = Diamond::Arpeggiator.new
|
95
|
+
end
|
96
|
+
|
97
|
+
context "#rate=" do
|
98
|
+
|
99
|
+
should "set the rate" do
|
100
|
+
assert_not_equal 16, @arpeggiator.rate
|
101
|
+
@arpeggiator.rate = 16
|
102
|
+
assert_equal 16, @arpeggiator.rate
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
context "#range=" do
|
108
|
+
|
109
|
+
should "set the range" do
|
110
|
+
@arpeggiator.range = 4
|
111
|
+
assert_equal 4, @arpeggiator.range
|
112
|
+
@arpeggiator.range += 1
|
113
|
+
assert_equal 5, @arpeggiator.range
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
context "#interval=" do
|
119
|
+
|
120
|
+
should "set the interval" do
|
121
|
+
assert_not_equal 7, @arpeggiator.interval
|
122
|
+
@arpeggiator.interval = 7
|
123
|
+
assert_equal 7, @arpeggiator.interval
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
context "#gate=" do
|
129
|
+
|
130
|
+
should "set the gate" do
|
131
|
+
assert_not_equal 125, @arpeggiator.gate
|
132
|
+
@arpeggiator.gate = 125
|
133
|
+
assert_equal 125, @arpeggiator.gate
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
context "#pattern_offset=" do
|
139
|
+
|
140
|
+
should "set the offset" do
|
141
|
+
assert_not_equal 5, @arpeggiator.pattern_offset
|
142
|
+
@arpeggiator.pattern_offset = 5
|
143
|
+
assert_equal 5, @arpeggiator.pattern_offset
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
class Diamond::ApeggiatorTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
context "Arpeggiator" do
|
6
|
+
|
7
|
+
context "#initialize" do
|
8
|
+
|
9
|
+
should "have defaults" do
|
10
|
+
@arpeggiator = Diamond::Arpeggiator.new
|
11
|
+
assert_equal 8, @arpeggiator.rate
|
12
|
+
assert_equal 3, @arpeggiator.range
|
13
|
+
assert_equal 12, @arpeggiator.interval
|
14
|
+
end
|
15
|
+
|
16
|
+
should "allow setting params" do
|
17
|
+
@arpeggiator = Diamond::Arpeggiator.new(:interval => 7, :range => 4, :rate => 16)
|
18
|
+
assert_equal 16, @arpeggiator.rate
|
19
|
+
assert_equal 4, @arpeggiator.range
|
20
|
+
assert_equal 7, @arpeggiator.interval
|
21
|
+
end
|
22
|
+
|
23
|
+
should "allow passing in input" do
|
24
|
+
@input = $test_device[:input]
|
25
|
+
@arpeggiator = Diamond::Arpeggiator.new(:midi => @input)
|
26
|
+
assert_not_empty @arpeggiator.midi_sources
|
27
|
+
assert @arpeggiator.midi_sources.include?(@input)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
dir = File.dirname(File.expand_path(__FILE__))
|
2
|
+
$LOAD_PATH.unshift dir + "/../lib"
|
3
|
+
|
4
|
+
require "test/unit"
|
5
|
+
require "mocha/test_unit"
|
6
|
+
require "shoulda-context"
|
7
|
+
require "diamond"
|
8
|
+
|
9
|
+
module TestHelper
|
10
|
+
|
11
|
+
def self.select_devices
|
12
|
+
$test_device ||= {}
|
13
|
+
{ :input => UniMIDI::Input, :output => UniMIDI::Output }.each do |type, klass|
|
14
|
+
$test_device[type] = klass.gets
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
TestHelper.select_devices
|
21
|
+
|
22
|
+
# Stub out OSC networking
|
23
|
+
::EM.stubs(:run).returns(:true)
|
data/test/osc_test.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
class Diamond::OSCTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
context "OSC" do
|
6
|
+
|
7
|
+
context "#enable_parameter_control" do
|
8
|
+
|
9
|
+
setup do
|
10
|
+
@map = [
|
11
|
+
{ :property => :interval, :address => "/1/rotaryA", :value => (0..1.0) },
|
12
|
+
{ :property => :transpose, :address => "/1/rotaryB" }
|
13
|
+
]
|
14
|
+
@addresses = @map.map { |mapping| mapping[:address] }
|
15
|
+
@osc = Diamond::OSC.new(:server_port => 8000)
|
16
|
+
end
|
17
|
+
|
18
|
+
should "start server" do
|
19
|
+
::OSC::EMServer.any_instance.expects(:run).once
|
20
|
+
@osc.enable_parameter_control(Object.new, @map)
|
21
|
+
::OSC::EMServer.any_instance.unstub(:run)
|
22
|
+
end
|
23
|
+
|
24
|
+
should "assign map" do
|
25
|
+
::OSC::EMServer.any_instance.expects(:add_method).times(@map.size).with do |arg|
|
26
|
+
assert @addresses.include?(arg)
|
27
|
+
end
|
28
|
+
@osc.enable_parameter_control(Object.new, @map)
|
29
|
+
::OSC::EMServer.any_instance.unstub(:add_method)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
class PatternTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
context "Pattern" do
|
6
|
+
|
7
|
+
setup do
|
8
|
+
::OSC::EMServer.any_instance.stubs(:run).returns(:true)
|
9
|
+
end
|
10
|
+
|
11
|
+
context "#initialize" do
|
12
|
+
|
13
|
+
should "create usable pattern" do
|
14
|
+
pattern = Diamond::Pattern.new("test") do |range, interval|
|
15
|
+
0.upto(range).map { |num| num * interval }
|
16
|
+
end
|
17
|
+
result = pattern.compute(2, 12)
|
18
|
+
assert_equal [0, 12, 24], result
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
context "#compute" do
|
24
|
+
|
25
|
+
should "reflect range and interval" do
|
26
|
+
pattern = Diamond::Pattern.find("Up")
|
27
|
+
result = pattern.compute(3, 7)
|
28
|
+
assert_equal 4, result.length
|
29
|
+
assert_equal [0, 7, 14, 21], result
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
context "Presets" do
|
34
|
+
|
35
|
+
should "populate Up" do
|
36
|
+
pattern = Diamond::Pattern.find("Up")
|
37
|
+
result = pattern.compute(2, 12)
|
38
|
+
assert_equal [0, 12, 24], result
|
39
|
+
end
|
40
|
+
|
41
|
+
should "populate Down" do
|
42
|
+
pattern = Diamond::Pattern.find("Down")
|
43
|
+
result = pattern.compute(2, 12)
|
44
|
+
assert_equal [24, 12, 0], result
|
45
|
+
end
|
46
|
+
|
47
|
+
should "populate UpDown" do
|
48
|
+
pattern = Diamond::Pattern.find("UpDown")
|
49
|
+
result = pattern.compute(3, 12)
|
50
|
+
assert_equal [0, 12, 24, 36, 24, 12, 0], result
|
51
|
+
end
|
52
|
+
|
53
|
+
should "populate DownUp" do
|
54
|
+
pattern = Diamond::Pattern.find("DownUp")
|
55
|
+
result = pattern.compute(3, 12)
|
56
|
+
assert_equal [36, 24, 12, 0, 12, 24, 36], result
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|