midi 0.0.7
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/LICENSE +13 -0
- data/README.rdoc +103 -0
- data/TODO +8 -0
- data/lib/micromidi.rb +67 -0
- data/lib/micromidi/context.rb +57 -0
- data/lib/micromidi/instructions/composite.rb +35 -0
- data/lib/micromidi/instructions/input.rb +114 -0
- data/lib/micromidi/instructions/message.rb +100 -0
- data/lib/micromidi/instructions/output.rb +28 -0
- data/lib/micromidi/instructions/process.rb +57 -0
- data/lib/micromidi/instructions/shorthand.rb +79 -0
- data/lib/micromidi/instructions/sticky.rb +59 -0
- data/lib/micromidi/instructions/sysex.rb +43 -0
- data/lib/micromidi/state.rb +79 -0
- data/lib/midi.rb +15 -0
- data/test/helper.rb +32 -0
- data/test/test_composite.rb +36 -0
- data/test/test_context.rb +33 -0
- data/test/test_effect.rb +135 -0
- data/test/test_input.rb +23 -0
- data/test/test_message.rb +123 -0
- data/test/test_output.rb +21 -0
- data/test/test_state.rb +32 -0
- data/test/test_sticky.rb +134 -0
- data/test/test_sysex.rb +60 -0
- metadata +103 -0
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2011 Ari Russo
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.rdoc
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
= micromidi
|
2
|
+
|
3
|
+
A Ruby DSL for MIDI
|
4
|
+
|
5
|
+
{pic}[http://images.treetrouble.net/images/midi.png]
|
6
|
+
|
7
|
+
== Features
|
8
|
+
|
9
|
+
* Cross-platform compatible using MRI or JRuby.
|
10
|
+
* Simplified MIDI and Sysex message output
|
11
|
+
* MIDI Thru, processing and custom input events
|
12
|
+
* Optional shorthand for {live coding}[http://en.wikipedia.org/wiki/Live_coding]
|
13
|
+
|
14
|
+
== Installation
|
15
|
+
|
16
|
+
gem install micromidi
|
17
|
+
|
18
|
+
== Requirements
|
19
|
+
|
20
|
+
Ruby 1.9.2+ or JRuby in 1.9 mode
|
21
|
+
|
22
|
+
Requires {midi-eye}[http://github.com/arirusso/midi-eye], {midi-message}[http://github.com/arirusso/midi-message] and {unimidi}[http://github.com/arirusso/unimidi]. These should install automatically with the gem.
|
23
|
+
|
24
|
+
== Usage
|
25
|
+
|
26
|
+
The following are basic examples that use {unimidi}[http://github.com/arirusso/unimidi] inputs and outputs. ({see an example here that explains selecting an output...}[http://github.com/arirusso/unimidi/blob/master/examples/select_a_device.rb])
|
27
|
+
|
28
|
+
require "midi"
|
29
|
+
|
30
|
+
@i = UniMIDI::Input.use(:first)
|
31
|
+
@o = UniMIDI::Output.use(:first)
|
32
|
+
|
33
|
+
This example plays some arpeggios
|
34
|
+
|
35
|
+
MIDI.using($o) do
|
36
|
+
|
37
|
+
5.times do |oct|
|
38
|
+
octave oct
|
39
|
+
%w{C E G B}.each { |n| play n 0.5 }
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
While running, this next example sends all input directly to the output except for notes; notes that are received are only sent to the output if their octave is between 1 and 3. Output is also printed to the console by passing in <em>$stdout</em>.
|
45
|
+
|
46
|
+
MIDI.using(@i, @o, $stdout) do
|
47
|
+
|
48
|
+
thru_except :note { |msg| only(msg, :octave, (1..3)) }
|
49
|
+
|
50
|
+
join
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
This is the same example redone using shorthand aliases
|
55
|
+
|
56
|
+
M(@i, @o) do
|
57
|
+
|
58
|
+
te :n { |m| only(m, :oct, (1..3)) }
|
59
|
+
|
60
|
+
j
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
Finally, here is an example that maps some MIDI Control Change messages to SysEx
|
65
|
+
|
66
|
+
MIDI.using(@i, @o) do
|
67
|
+
|
68
|
+
*@the_map =
|
69
|
+
[0x40, 0x7F, 0x00],
|
70
|
+
[0x41, 0x7F, 0x00],
|
71
|
+
[0x42, 0x7F, 0x00]
|
72
|
+
|
73
|
+
node :roland, :model_id => 0x42, :device_id => 0x10
|
74
|
+
|
75
|
+
receive :cc do |message|
|
76
|
+
|
77
|
+
command @the_map[message.index - 1], message.value
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
I've written up a few posts explaining each of the concepts used here in greater detail:
|
84
|
+
|
85
|
+
* {Output}[http://tx81z.blogspot.com/2011/08/micromidi-midi-messages-and-output.html]
|
86
|
+
* {MIDI Thru and Processing}[http://tx81z.blogspot.com/2011/08/micromidi-midi-thru-and-midi-processing.html]
|
87
|
+
* {Binding Custom Input Events}[http://tx81z.blogspot.com/2011/08/micromidi-custom-events.html]
|
88
|
+
* {Shorthand}[http://tx81z.blogspot.com/2011/08/micromidi-shorthand.html]
|
89
|
+
* {Sysex}[http://tx81z.blogspot.com/2011/09/generating-sysex-messages-with.html]
|
90
|
+
|
91
|
+
== Documentation
|
92
|
+
|
93
|
+
* {rdoc}[http://rubydoc.info/github/arirusso/micromidi]
|
94
|
+
|
95
|
+
== Author
|
96
|
+
|
97
|
+
* {Ari Russo}[http://github.com/arirusso] <ari.russo at gmail.com>
|
98
|
+
|
99
|
+
== License
|
100
|
+
|
101
|
+
Apache 2.0, See the file LICENSE
|
102
|
+
|
103
|
+
Copyright (c) 2011 Ari Russo
|
data/TODO
ADDED
data/lib/micromidi.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# micromidi
|
4
|
+
# A Ruby DSL for MIDI
|
5
|
+
#
|
6
|
+
# (c)2011 Ari Russo
|
7
|
+
# licensed under the Apache 2.0 License
|
8
|
+
#
|
9
|
+
|
10
|
+
# libs
|
11
|
+
require 'midi-eye'
|
12
|
+
require 'midi-message'
|
13
|
+
require 'unimidi'
|
14
|
+
|
15
|
+
module MicroMIDI
|
16
|
+
|
17
|
+
VERSION = "0.0.7"
|
18
|
+
|
19
|
+
module Instructions
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.new(*a, &block)
|
23
|
+
message(*a, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.message(*args, &block)
|
27
|
+
ins, outs = *process_devices(args)
|
28
|
+
MicroMIDI::Context.new(ins, outs, &block)
|
29
|
+
end
|
30
|
+
class << self
|
31
|
+
alias_method :using, :message
|
32
|
+
end
|
33
|
+
|
34
|
+
module IO
|
35
|
+
|
36
|
+
def self.new(*args)
|
37
|
+
MicroMIDI.message(*args)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def self.process_devices(args)
|
45
|
+
ins = args.find_all { |d| d.respond_to?(:type) && d.type == :input && d.respond_to?(:gets) }
|
46
|
+
outs = args.find_all { |d| d.respond_to?(:puts) }
|
47
|
+
[ins, outs]
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
MIDI = MicroMIDI
|
52
|
+
|
53
|
+
# modules
|
54
|
+
require 'micromidi/instructions/composite'
|
55
|
+
|
56
|
+
# classes
|
57
|
+
require 'micromidi/context'
|
58
|
+
require 'micromidi/state'
|
59
|
+
require 'micromidi/instructions/process'
|
60
|
+
require 'micromidi/instructions/input'
|
61
|
+
require 'micromidi/instructions/message'
|
62
|
+
require 'micromidi/instructions/output'
|
63
|
+
require 'micromidi/instructions/sticky'
|
64
|
+
require 'micromidi/instructions/sysex'
|
65
|
+
|
66
|
+
# re-open
|
67
|
+
require 'micromidi/instructions/shorthand'
|
@@ -0,0 +1,57 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
|
4
|
+
module MicroMIDI
|
5
|
+
|
6
|
+
class Context
|
7
|
+
|
8
|
+
include Instructions::Composite
|
9
|
+
|
10
|
+
attr_reader :state
|
11
|
+
|
12
|
+
def initialize(ins, outs, &block)
|
13
|
+
|
14
|
+
@state = State.new(ins, outs)
|
15
|
+
|
16
|
+
@instructions = {
|
17
|
+
:process => Instructions::Process.new(@state),
|
18
|
+
:input => Instructions::Input.new(@state),
|
19
|
+
:message => Instructions::Message.new(@state),
|
20
|
+
:output => Instructions::Output.new(@state),
|
21
|
+
:sticky => Instructions::Sticky.new(@state),
|
22
|
+
:sysex => Instructions::SysEx.new(@state)
|
23
|
+
}
|
24
|
+
|
25
|
+
instance_eval(&block) unless block.nil?
|
26
|
+
end
|
27
|
+
|
28
|
+
def repeat
|
29
|
+
self.send(@state.last_command[:method], *@state.last_command[:args]) unless @state.last_command.nil?
|
30
|
+
end
|
31
|
+
|
32
|
+
def method_missing(m, *a, &b)
|
33
|
+
delegated = false
|
34
|
+
outp = nil
|
35
|
+
options = a.last.kind_of?(Hash) ? a.last : {}
|
36
|
+
do_output = options[:output] || true
|
37
|
+
[@instructions[:sysex], @instructions[:message], @instructions[:process]].each do |dsl|
|
38
|
+
if dsl.respond_to?(m)
|
39
|
+
msg = dsl.send(m, *a, &b)
|
40
|
+
outp = @state.auto_output && do_output ? @instructions[:output].output(msg) : msg
|
41
|
+
delegated = true
|
42
|
+
end
|
43
|
+
end
|
44
|
+
unless delegated
|
45
|
+
[@instructions[:input], @instructions[:output], @instructions[:sticky]].each do |dsl|
|
46
|
+
if dsl.respond_to?(m)
|
47
|
+
outp = dsl.send(m, *a, &b)
|
48
|
+
delegated = true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
@state.record(m, a, b, outp)
|
53
|
+
delegated ? outp : super
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
module MicroMIDI
|
4
|
+
|
5
|
+
module Instructions
|
6
|
+
|
7
|
+
module Composite
|
8
|
+
|
9
|
+
# play note n, wait <em>duration</em> seconds, then do note off for note n
|
10
|
+
def play(n, duration)
|
11
|
+
msg = case n
|
12
|
+
when Numeric, String then note(n)
|
13
|
+
when MIDIMessage then n
|
14
|
+
end
|
15
|
+
sleep(duration)
|
16
|
+
off
|
17
|
+
msg
|
18
|
+
end
|
19
|
+
|
20
|
+
# sends a note off message for every note on every channel
|
21
|
+
def all_off
|
22
|
+
(0..15).each do |channel|
|
23
|
+
(0..127).each do |note_num|
|
24
|
+
note_off(note_num, :channel => channel)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
true
|
28
|
+
end
|
29
|
+
alias_method :quiet!, :all_off
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
module MicroMIDI
|
4
|
+
|
5
|
+
module Instructions
|
6
|
+
|
7
|
+
class Input
|
8
|
+
|
9
|
+
include MIDIMessage
|
10
|
+
|
11
|
+
def initialize(state)
|
12
|
+
@state = state
|
13
|
+
end
|
14
|
+
|
15
|
+
# bind an event that will be called every time a message is received
|
16
|
+
def receive(*a, &block)
|
17
|
+
options = a.last.kind_of?(Hash) ? a.pop : {}
|
18
|
+
match = a.empty? ? nil : { :class => msg_classes(a) }
|
19
|
+
listener(match, options) { |event| yield(event[:message], event[:timestamp]) }
|
20
|
+
end
|
21
|
+
alias_method :gets, :receive
|
22
|
+
alias_method :handle, :receive
|
23
|
+
alias_method :listen, :receive
|
24
|
+
alias_method :listen_for, :receive
|
25
|
+
alias_method :when_receive, :receive
|
26
|
+
|
27
|
+
def receive_unless(*a, &block)
|
28
|
+
options = a.last.kind_of?(Hash) ? a.pop : {}
|
29
|
+
match = { :class => msg_classes(a) }
|
30
|
+
listener(nil, options) { |event| yield(event[:message], event[:timestamp]) unless match.include?(event[:message].class) }
|
31
|
+
end
|
32
|
+
alias_method :handle_unless, :receive_unless
|
33
|
+
alias_method :listen_unless, :receive_unless
|
34
|
+
alias_method :listen_for_unless, :receive_unless
|
35
|
+
alias_method :unless_receive, :receive_unless
|
36
|
+
|
37
|
+
# send input messages thru to the outputs
|
38
|
+
def thru
|
39
|
+
thru_if
|
40
|
+
end
|
41
|
+
|
42
|
+
# send input messages thru to the outputs if it has a specific class
|
43
|
+
def thru_if(*a)
|
44
|
+
a.last.kind_of?(Hash) ? a.last[:thru] = true : a.push({ :thru => true })
|
45
|
+
receive(*a) { |message, timestamp| output(message) }
|
46
|
+
end
|
47
|
+
|
48
|
+
# send input messages thru to the outputs unless of a specific class
|
49
|
+
def thru_unless(*a)
|
50
|
+
a.last.kind_of?(Hash) ? a.last[:thru] = true : a.push({ :thru => true })
|
51
|
+
receive_unless(*a) { |message, timestamp| output(message) }
|
52
|
+
end
|
53
|
+
|
54
|
+
# like <em>thru_unless</em> except a block can be passed that will be called when
|
55
|
+
# notes specified as the <em>unless</em> arrive
|
56
|
+
def thru_except(*a, &block)
|
57
|
+
thru_unless(*a)
|
58
|
+
receive(*a, &block)
|
59
|
+
end
|
60
|
+
|
61
|
+
# wait for input on the last input passed in
|
62
|
+
# can pass the option :from => [an input] to specify which one to wait on
|
63
|
+
def wait_for_input(options = {})
|
64
|
+
listener = options[:from] || @state.listeners.last || @state.thru_listeners.last
|
65
|
+
listener.join
|
66
|
+
end
|
67
|
+
|
68
|
+
def join
|
69
|
+
loop { wait_for_input }
|
70
|
+
end
|
71
|
+
|
72
|
+
protected
|
73
|
+
|
74
|
+
def listener(match = {}, options = {}, &block)
|
75
|
+
inputs = options[:from] || @state.inputs
|
76
|
+
thru = options[:thru] || false
|
77
|
+
match ||= {}
|
78
|
+
inputs.each do |input|
|
79
|
+
listener = MIDIEye::Listener.new(input)
|
80
|
+
listener.listen_for(match, &block)
|
81
|
+
if thru
|
82
|
+
@state.thru_listeners.each { |l| l.stop }
|
83
|
+
@state.thru_listeners.clear
|
84
|
+
@state.thru_listeners << listener
|
85
|
+
else
|
86
|
+
@state.listeners << listener
|
87
|
+
end
|
88
|
+
listener.start(:background => true) unless !options[:start].nil? && !options[:start]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def msg_classes(list)
|
95
|
+
list.map do |type|
|
96
|
+
case type
|
97
|
+
when :aftertouch, :pressure, :aft then [ChannelAftertouch, PolyphonicAftertouch]
|
98
|
+
when :channel_aftertouch, :channel_pressure, :ca, :cp then ChannelAftertouch
|
99
|
+
when :control_change, :cc, :c then ControlChange
|
100
|
+
when :note, :n then [NoteOn, NoteOff]
|
101
|
+
when :note_on, :nn then NoteOn
|
102
|
+
when :note_off, :no, :off then NoteOff
|
103
|
+
when :pitch_bend, :pb then PitchBend
|
104
|
+
when :polyphonic_aftertouch, :poly_aftertouch, :poly_pressure, :polyphonic_pressure, :pa, :pp then PolyphonicAftertouch
|
105
|
+
when :program_change, :pc, :p then ProgramChange
|
106
|
+
end
|
107
|
+
end.flatten.compact
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
module MicroMIDI
|
4
|
+
|
5
|
+
module Instructions
|
6
|
+
|
7
|
+
class Message
|
8
|
+
|
9
|
+
include MIDIMessage
|
10
|
+
|
11
|
+
def initialize(state)
|
12
|
+
@state = state
|
13
|
+
end
|
14
|
+
|
15
|
+
# create a control change message
|
16
|
+
def control_change(id, value, opts = {})
|
17
|
+
props = @state.message_properties(opts, :channel)
|
18
|
+
id.kind_of?(Numeric) ? ControlChange.new(props[:channel], id, value) : ControlChange[id].new(props[:channel], value)
|
19
|
+
end
|
20
|
+
|
21
|
+
# create a note message
|
22
|
+
def note(id, opts = {})
|
23
|
+
props = @state.message_properties(opts, :channel, :velocity)
|
24
|
+
note = if id.kind_of?(Numeric)
|
25
|
+
NoteOn.new(props[:channel], id, props[:velocity])
|
26
|
+
elsif id.kind_of?(String) || id.kind_of?(Symbol)
|
27
|
+
note_string = parse_note_name(id)
|
28
|
+
NoteOn[note_string].new(props[:channel], props[:velocity])
|
29
|
+
end
|
30
|
+
@state.last_note = note
|
31
|
+
note
|
32
|
+
end
|
33
|
+
|
34
|
+
# create a note off message
|
35
|
+
def note_off(id, opts = {})
|
36
|
+
props = @state.message_properties(opts, :channel, :velocity)
|
37
|
+
if id.kind_of?(Numeric)
|
38
|
+
NoteOff.new(props[:channel], id, props[:velocity])
|
39
|
+
elsif id.kind_of?(String) || id.kind_of?(Symbol)
|
40
|
+
note_string = parse_note_name(id)
|
41
|
+
NoteOff[parse_note_name(id)].new(props[:channel], props[:velocity])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# create a MIDI message from a byte string, array of bytes, or list of bytes
|
46
|
+
def parse(message)
|
47
|
+
MIDIMessage.parse(message)
|
48
|
+
end
|
49
|
+
|
50
|
+
# create a program change message
|
51
|
+
def program_change(program, opts = {})
|
52
|
+
props = @state.message_properties(opts, :channel)
|
53
|
+
MIDIMessage::ProgramChange.new(props[:channel], program)
|
54
|
+
end
|
55
|
+
|
56
|
+
# create a note-off message from the last note-on message
|
57
|
+
def off
|
58
|
+
o = @state.last_note.to_note_off unless @state.last_note.nil?
|
59
|
+
@state.last_note = nil
|
60
|
+
o
|
61
|
+
end
|
62
|
+
|
63
|
+
# create a channel pressure message
|
64
|
+
def channel_aftertouch(value, opts = {})
|
65
|
+
props = @state.message_properties(opts, :channel)
|
66
|
+
MIDIMessage::ChannelAftertouch.new(props[:channel], value)
|
67
|
+
end
|
68
|
+
alias_method :channel_pressure, :channel_aftertouch
|
69
|
+
|
70
|
+
# create a poly pressure message
|
71
|
+
def polyphonic_aftertouch(note, value, opts = {})
|
72
|
+
props = @state.message_properties(opts, :channel)
|
73
|
+
MIDIMessage::PolyphonicAftertouch.new(props[:channel], note, value)
|
74
|
+
end
|
75
|
+
alias_method :poly_aftertouch, :polyphonic_aftertouch
|
76
|
+
alias_method :polyphonic_pressure, :polyphonic_aftertouch
|
77
|
+
alias_method :poly_pressure, :polyphonic_aftertouch
|
78
|
+
|
79
|
+
def pitch_bend(low, high, opts = {})
|
80
|
+
props = @state.message_properties(opts, :channel)
|
81
|
+
MIDIMessage::PitchBend.new(props[:channel], low, high)
|
82
|
+
end
|
83
|
+
alias_method :bend, :pitch_bend
|
84
|
+
alias_method :pitchbend, :pitch_bend
|
85
|
+
|
86
|
+
protected
|
87
|
+
|
88
|
+
def parse_note_name(name)
|
89
|
+
name = name.to_s #ensure that name is a string
|
90
|
+
string_opts = { :octave => name.scan(/-?\d\z/).first }
|
91
|
+
note = name.split(/-?\d\z/).first
|
92
|
+
string_props = @state.message_properties(string_opts, :octave)
|
93
|
+
"#{note}#{string_props[:octave].to_s}"
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|