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
data/lib/diamond/midi.rb
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
module Diamond
|
2
|
+
|
3
|
+
# Enable the instrument to use MIDI
|
4
|
+
module MIDI
|
5
|
+
|
6
|
+
# Methods dealing with MIDI input
|
7
|
+
module Input
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.send(:extend, Forwardable)
|
11
|
+
base.send(:def_delegators, :@midi,
|
12
|
+
:input,
|
13
|
+
:inputs,
|
14
|
+
:omni_on,
|
15
|
+
:rx_channel,
|
16
|
+
:receive_channel,
|
17
|
+
:rx_channel=,
|
18
|
+
:receive_channel=)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Add MIDI input notes
|
22
|
+
# @param [Array<MIDIMessage>, MIDIMessage, *MIDIMessage] args
|
23
|
+
# @return [Array<MIDIMessage>]
|
24
|
+
def add(*args)
|
25
|
+
@midi.input << args
|
26
|
+
end
|
27
|
+
alias_method :<<, :add
|
28
|
+
|
29
|
+
# Add note offs to cancel input
|
30
|
+
# @param [Array<MIDIMessage>, MIDIMessage, *MIDIMessage] args
|
31
|
+
# @return [Array<MIDIMessage>]
|
32
|
+
def remove(*args)
|
33
|
+
messages = MIDIInstrument::Message.to_note_offs(*args)
|
34
|
+
@midi.input.add(messages.compact)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Initialize adding and removing MIDI notes from the sequence
|
38
|
+
# @param [Sequence] sequence
|
39
|
+
# @return [Boolean]
|
40
|
+
def enable_note_control(sequence)
|
41
|
+
@midi.input.receive(:class => MIDIMessage::NoteOn) do |event|
|
42
|
+
message = event[:message]
|
43
|
+
if @midi.input.channel.nil? || @midi.input.channel == message.channel
|
44
|
+
puts "[DEBUG] MIDI: add note from input #{message.name} channel: #{message.channel}" if @debug
|
45
|
+
sequence.add(message)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
@midi.input.receive(:class => MIDIMessage::NoteOff) do |event|
|
49
|
+
message = event[:message]
|
50
|
+
if @midi.input.channel.nil? || @midi.input.channel == message.channel
|
51
|
+
puts "[DEBUG] MIDI: remove note from input #{message.name} channel: #{message.channel}" if @debug
|
52
|
+
sequence.remove(message)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
true
|
56
|
+
end
|
57
|
+
|
58
|
+
# Initialize a user-defined map of control change messages
|
59
|
+
# @param [SequenceParameters] parameters
|
60
|
+
# @param [Array<Hash>] map
|
61
|
+
# @return [Boolean]
|
62
|
+
def enable_parameter_control(parameters, map)
|
63
|
+
from_range = 0..127
|
64
|
+
@midi.input.receive(:class => MIDIMessage::ControlChange) do |event|
|
65
|
+
message = event[:message]
|
66
|
+
if @midi.input.channel.nil? || @midi.input.channel == message.channel
|
67
|
+
index = message.index
|
68
|
+
mapping = map.find { |mapping| mapping[:index] == index }
|
69
|
+
property = mapping[:property]
|
70
|
+
to_range = SequenceParameters::RANGE[property]
|
71
|
+
value = message.value
|
72
|
+
value = Scale.transform(value).from(from_range).to(to_range)
|
73
|
+
puts "[DEBUG] MIDI: #{property}= #{value} channel: #{message.channel}" if @debug
|
74
|
+
parameters.send("#{property}=", value)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# @param [Array<UniMIDI::Input>] inputs
|
82
|
+
# @param [Hash] options
|
83
|
+
# @option options [Fixnum] :channel The receive channel (also: :rx_channel)
|
84
|
+
def initialize_input(inputs, options = {})
|
85
|
+
@midi.input.devices.concat(inputs)
|
86
|
+
@midi.input.channel = options[:receive_channel]
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
# Methods dealing with MIDI output
|
92
|
+
module Output
|
93
|
+
|
94
|
+
def self.included(base)
|
95
|
+
base.send(:extend, Forwardable)
|
96
|
+
base.send(:def_delegators, :@midi,
|
97
|
+
:mute,
|
98
|
+
:mute=,
|
99
|
+
:output,
|
100
|
+
:outputs,
|
101
|
+
:toggle_mute,
|
102
|
+
:tx_channel,
|
103
|
+
:transmit_channel,
|
104
|
+
:tx_channel=,
|
105
|
+
:transmit_channel=)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Initialize MIDI output, enabling the sequencer to emit notes
|
109
|
+
# @param [Sequencer::Core] sequencer
|
110
|
+
# @return [Boolean]
|
111
|
+
def enable_output(sequencer)
|
112
|
+
sequencer.event.perform << proc do |bucket|
|
113
|
+
unless bucket.empty?
|
114
|
+
if @debug
|
115
|
+
bucket.each do |message|
|
116
|
+
puts "[DEBUG] MIDI: output #{message.name} channel: #{message.channel}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
@midi.output.puts(bucket)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
sequencer.event.stop << proc { emit_pending_note_offs }
|
123
|
+
true
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
# Initialize MIDI output
|
129
|
+
# @param [Array<UniMIDI::Output>] outputs
|
130
|
+
# @param [Hash] options
|
131
|
+
# @option options [Fixnum] :tx_channel The transmit channel
|
132
|
+
def initialize_output(outputs, options = {})
|
133
|
+
@midi.output.devices.concat(outputs)
|
134
|
+
@midi.output.channel = options[:transmit_channel]
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
# An access point for dealing with all MIDI functionality for the instrument
|
140
|
+
class Node
|
141
|
+
|
142
|
+
include Input
|
143
|
+
include Output
|
144
|
+
|
145
|
+
# Initialize MIDI input and output
|
146
|
+
# @param [Hash] devices
|
147
|
+
# @param [Hash] options
|
148
|
+
# @option options [Fixnum] :channel The receive channel (also: :rx_channel)
|
149
|
+
# @option options [Fixnum] :tx_channel The transmit channel
|
150
|
+
def initialize(devices, options = {})
|
151
|
+
@debug = options.fetch(:debug, false)
|
152
|
+
@midi = MIDIInstrument::Node.new
|
153
|
+
initialize_input(devices[:input], options)
|
154
|
+
initialize_output(devices[:output], options)
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
|
159
|
+
# Shortcut to Diamond::MIDI::Node.new
|
160
|
+
def self.new(*args)
|
161
|
+
Node.new(*args)
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
end
|
data/lib/diamond/osc.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
module Diamond
|
2
|
+
|
3
|
+
# Enable the instrument to use OSC
|
4
|
+
module OSC
|
5
|
+
|
6
|
+
# An access point for dealing with all OSC functionality for the instrument
|
7
|
+
class Node
|
8
|
+
|
9
|
+
# @param [Hash] options
|
10
|
+
# @option options [Boolean] :debug Whether to send debug output
|
11
|
+
# @option options [Fixnum] :server_port The port to listen on (default: 8000)
|
12
|
+
def initialize(options = {})
|
13
|
+
@debug = options.fetch(:debug, false)
|
14
|
+
port = options.fetch(:server_port, 8000)
|
15
|
+
@server = ::OSC::EMServer.new(port)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Enable controlling the instrument via OSC
|
19
|
+
# @param [Object] subject The object to operate on when messages are received
|
20
|
+
# @param [Array<Hash>] map
|
21
|
+
# @return [Boolean]
|
22
|
+
def enable_parameter_control(subject, map)
|
23
|
+
start_server
|
24
|
+
maps = map.map do |item|
|
25
|
+
property = item[:property]
|
26
|
+
from_range = item[:value] || (0..1.0)
|
27
|
+
to_range = SequenceParameters::RANGE[property]
|
28
|
+
@server.add_method(item[:address]) do |message|
|
29
|
+
value = message.to_a[0]
|
30
|
+
value = Scale.transform(value).from(from_range).to(to_range)
|
31
|
+
puts "[DEBUG]: OSC: #{property}= #{value}" if @debug
|
32
|
+
subject.send("#{property}=", value)
|
33
|
+
true
|
34
|
+
end
|
35
|
+
true
|
36
|
+
end
|
37
|
+
maps.any?
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Start the server
|
43
|
+
# @return [Thread]
|
44
|
+
def start_server
|
45
|
+
@thread = Thread.new do
|
46
|
+
begin
|
47
|
+
EM.epoll
|
48
|
+
EM.run { @server.run }
|
49
|
+
rescue Exception => exception
|
50
|
+
Thread.main.raise(exception)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
@thread.abort_on_exception = true
|
54
|
+
@thread
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
# Shortcut to Diamond::OSC::Node.new
|
60
|
+
def self.new(*args)
|
61
|
+
Node.new(*args)
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Patch the OSC module
|
68
|
+
#
|
69
|
+
module OSC
|
70
|
+
class EMServer
|
71
|
+
|
72
|
+
def run
|
73
|
+
open
|
74
|
+
end
|
75
|
+
|
76
|
+
def open
|
77
|
+
EM::open_datagram_socket("0.0.0.0", @port, Connection)
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
module EventMachine
|
84
|
+
module WebSocket
|
85
|
+
def self.start(options, &blk)
|
86
|
+
#EM.epoll
|
87
|
+
#EM.run {
|
88
|
+
trap("TERM") { stop }
|
89
|
+
trap("INT") { stop }
|
90
|
+
|
91
|
+
run(options, &blk)
|
92
|
+
#}
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.run(options)
|
96
|
+
host, port = options.values_at(:host, :port)
|
97
|
+
EM.start_server(host, port, Connection, options) do |c|
|
98
|
+
yield c
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Diamond
|
2
|
+
|
3
|
+
# Pattern that the sequence is derived from given the parameters and input
|
4
|
+
class Pattern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
|
8
|
+
# All patterns
|
9
|
+
# @return [Array<Pattern>]
|
10
|
+
def all
|
11
|
+
@patterns ||= []
|
12
|
+
end
|
13
|
+
|
14
|
+
# Find a pattern by its name (case insensitive)
|
15
|
+
# @param [String, Symbol] name
|
16
|
+
# @return [Pattern]
|
17
|
+
def find(name)
|
18
|
+
all.find { |pattern| pattern.name.to_s.downcase == name.to_s.downcase }
|
19
|
+
end
|
20
|
+
|
21
|
+
# Construct and add a pattern
|
22
|
+
# @param [Symbol, String] name
|
23
|
+
# @param [Proc] block
|
24
|
+
# @return [Array<Pattern>]
|
25
|
+
def add(*args, &block)
|
26
|
+
all << new(*args, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Add a pattern
|
30
|
+
# @param [Pattern] pattern
|
31
|
+
# @return [Array<Pattern>]
|
32
|
+
def <<(pattern)
|
33
|
+
all << pattern
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Pattern]
|
37
|
+
def first
|
38
|
+
all.first
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Pattern]
|
42
|
+
def last
|
43
|
+
all.last
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
extend ClassMethods
|
49
|
+
|
50
|
+
attr_reader :name
|
51
|
+
|
52
|
+
# @param [String, Symbol] name A name to identify the pattern by eg "up/down"
|
53
|
+
# @param [Proc] block The pattern procedure, which should return an array of scale degree numbers.
|
54
|
+
# For example, given the arguments (3, 7) the "Up" pattern will produce [0, 7, 14, 21]
|
55
|
+
def initialize(name, &block)
|
56
|
+
@name = name
|
57
|
+
@proc = block
|
58
|
+
end
|
59
|
+
|
60
|
+
# Compute scale degrees using the pattern with the given range and interval
|
61
|
+
# @param [Fixnum] range
|
62
|
+
# @param [Interval] interval
|
63
|
+
# @return [Array<Fixnum>]
|
64
|
+
def compute(range, interval)
|
65
|
+
@proc.call(range, interval)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Standard preset patterns
|
69
|
+
module Presets
|
70
|
+
|
71
|
+
Pattern << Pattern.new("Up") do |range, interval|
|
72
|
+
0.upto(range).map { |num| num * interval }
|
73
|
+
end
|
74
|
+
|
75
|
+
Pattern << Pattern.new("Down") do |range, interval|
|
76
|
+
range.downto(0).map { |num| num * interval }
|
77
|
+
end
|
78
|
+
|
79
|
+
Pattern << Pattern.new("UpDown") do |range, interval|
|
80
|
+
up = 0.upto(range).map { |num| num * interval }
|
81
|
+
down = [(range - 1), 0].max.downto(0).map { |num| num * interval }
|
82
|
+
up + down
|
83
|
+
end
|
84
|
+
|
85
|
+
Pattern << Pattern.new("DownUp") do |range, interval|
|
86
|
+
down = range.downto(0).map { |num| num * interval }
|
87
|
+
up = 1.upto(range).map { |num| num * interval }
|
88
|
+
down + up
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
module Diamond
|
2
|
+
|
3
|
+
# The note event sequence from where the arpeggiator output is derived
|
4
|
+
class Sequence
|
5
|
+
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def_delegators :@sequence, :each, :first, :last, :length
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@parameter = nil
|
12
|
+
# realtime
|
13
|
+
@changed = false
|
14
|
+
@input_queue = []
|
15
|
+
@queue = []
|
16
|
+
end
|
17
|
+
|
18
|
+
# The bucket of messages for the given pointer
|
19
|
+
# @param [Fixnum] pointer
|
20
|
+
# @return [Array<MIDIMessage>]
|
21
|
+
def at(pointer)
|
22
|
+
if changed? && (pointer % @parameter.rate == 0)
|
23
|
+
update
|
24
|
+
@changed = false
|
25
|
+
end
|
26
|
+
enqueue_next(pointer)
|
27
|
+
messages = @queue.shift || []
|
28
|
+
messages
|
29
|
+
end
|
30
|
+
|
31
|
+
# Has the sequence changed since the last update?
|
32
|
+
# @return [Boolean]
|
33
|
+
def changed?
|
34
|
+
@changed
|
35
|
+
end
|
36
|
+
|
37
|
+
# Add inputted note_messages
|
38
|
+
# @param [Array<MIDIMessage::NoteOn>, MIDIMessage::NoteOn, *MIDIMessage::NoteOn] note_messages
|
39
|
+
# @return [Boolean]
|
40
|
+
def add(*note_messages)
|
41
|
+
messages = [note_messages].flatten.compact
|
42
|
+
@input_queue.concat(messages)
|
43
|
+
mark_changed
|
44
|
+
true
|
45
|
+
end
|
46
|
+
|
47
|
+
# Remove input note messages with the same note value
|
48
|
+
# @param [Array<MIDIMessage::NoteOn, MIDIMessage::NoteOff>, MIDIMessage::NoteOff, MIDIMessage::NoteOn, *MIDIMessage::NoteOff, *MIDIMessage::NoteOn] note_messages
|
49
|
+
# @return [Boolean]
|
50
|
+
def remove(*note_messages)
|
51
|
+
messages = [note_messages].flatten
|
52
|
+
deletion_queue = messages.map(&:note)
|
53
|
+
@input_queue.delete_if { |message| deletion_queue.include?(message.note) }
|
54
|
+
mark_changed
|
55
|
+
true
|
56
|
+
end
|
57
|
+
|
58
|
+
# Remove all input note messages
|
59
|
+
# @return [Boolean]
|
60
|
+
def remove_all
|
61
|
+
@input_queue.clear
|
62
|
+
mark_changed
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
# All NoteOff messages in the queue
|
67
|
+
# @return [Array<MIDIMessage::NoteOff>]
|
68
|
+
def pending_note_offs
|
69
|
+
messages = @queue.map do |bucket|
|
70
|
+
unless bucket.nil?
|
71
|
+
bucket.select { |m| m.class == MIDIMessage::NoteOff }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
messages.flatten.compact
|
75
|
+
end
|
76
|
+
|
77
|
+
# Mark the sequence as changed
|
78
|
+
# @return [Boolean]
|
79
|
+
def mark_changed
|
80
|
+
@changed = true
|
81
|
+
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
|
85
|
+
# Apply the given parameters object
|
86
|
+
# @param [SequenceParameters] parameters
|
87
|
+
# @return [SequenceParameters]
|
88
|
+
def use_parameters(parameters)
|
89
|
+
@parameter = parameters
|
90
|
+
update
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
# Enqueue next bucket for the given pointer
|
96
|
+
# @param [Fixnum] pointer
|
97
|
+
# @return [Array<NoteEvent>]
|
98
|
+
def enqueue_next(pointer)
|
99
|
+
bucket = @sequence[pointer]
|
100
|
+
enqueue(bucket) unless bucket.nil?
|
101
|
+
bucket
|
102
|
+
end
|
103
|
+
|
104
|
+
# Prepare the given event bucket for performance, moving note messages to the queue
|
105
|
+
# @param [Array<NoteEvent>] bucket
|
106
|
+
# @return [Array<NoteEvent>]
|
107
|
+
def enqueue(bucket)
|
108
|
+
bucket.map do |event|
|
109
|
+
@queue[0] ||= []
|
110
|
+
@queue[0] << event.start
|
111
|
+
float_length = (event.length.to_f / 100) * @parameter.duration.to_f
|
112
|
+
length = float_length.to_i
|
113
|
+
@queue[length] ||= []
|
114
|
+
@queue[length] << event.finish
|
115
|
+
event
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Commit changes to the sequence
|
120
|
+
# @return [ArpeggiatorSequence]
|
121
|
+
def update
|
122
|
+
notes = get_note_sequence
|
123
|
+
initialize_sequence(notes.length)
|
124
|
+
populate_sequence(notes) unless notes.empty?
|
125
|
+
@sequence
|
126
|
+
end
|
127
|
+
|
128
|
+
# (Re)initialize the sequence with the given length
|
129
|
+
# @param [Fixnum] length
|
130
|
+
# @return [Array]
|
131
|
+
def initialize_sequence(length)
|
132
|
+
sequence_length_in_ticks = length * @parameter.duration
|
133
|
+
@sequence = Array.new(sequence_length_in_ticks, [])
|
134
|
+
end
|
135
|
+
|
136
|
+
# Populate the sequence with the given notes
|
137
|
+
# @param [Array<MIDIMessage::NoteOn>] notes
|
138
|
+
# @return [Array<Array<NoteEvent>>]
|
139
|
+
def populate_sequence(notes)
|
140
|
+
@parameter.pattern_offset.times { notes.push(notes.shift) }
|
141
|
+
notes.each_with_index do |note, i|
|
142
|
+
index = i * @parameter.duration
|
143
|
+
populate_bucket(index, note) unless @sequence[index].nil?
|
144
|
+
end
|
145
|
+
@sequence
|
146
|
+
end
|
147
|
+
|
148
|
+
# Populate the bucket for index with the given note message
|
149
|
+
# @param [Fixnum] index
|
150
|
+
# @param [MIDIMessage::NoteOn] note_message
|
151
|
+
# @return [Array<NoteEvent>]
|
152
|
+
def populate_bucket(index, note_message)
|
153
|
+
@sequence[index] = create_bucket(note_message)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Create a bucket/note event for the given note message
|
157
|
+
# @param [MIDIMessage::NoteOn] note_message
|
158
|
+
# @return [Array<NoteEvent>]
|
159
|
+
def create_bucket(note_message)
|
160
|
+
event = MIDIInstrument::NoteEvent.new(note_message, @parameter.gate)
|
161
|
+
[event]
|
162
|
+
end
|
163
|
+
|
164
|
+
# The input queue as note messages
|
165
|
+
# @return [Array<MIDIMessage::NoteOn>]
|
166
|
+
def get_note_sequence
|
167
|
+
notes = @parameter.computed_pattern.map do |degree|
|
168
|
+
@input_queue.map do |message|
|
169
|
+
note = message.note + degree + @parameter.transpose
|
170
|
+
MIDIMessage::NoteOn.new(message.channel, note, message.velocity)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
notes.flatten.compact
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|