ffi-coremidi 0.1.7
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +13 -0
- data/README.rdoc +48 -0
- data/lib/coremidi.rb +24 -0
- data/lib/coremidi/destination.rb +154 -0
- data/lib/coremidi/device.rb +92 -0
- data/lib/coremidi/endpoint.rb +75 -0
- data/lib/coremidi/entity.rb +90 -0
- data/lib/coremidi/map.rb +141 -0
- data/lib/coremidi/source.rb +184 -0
- data/test/helper.rb +58 -0
- data/test/test_input_buffer.rb +45 -0
- data/test/test_io.rb +83 -0
- metadata +68 -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,48 @@
|
|
1
|
+
= ffi-coremidi
|
2
|
+
|
3
|
+
== Summary
|
4
|
+
|
5
|
+
Realtime MIDI IO with Ruby for OSX
|
6
|
+
|
7
|
+
Note that in the interest of allowing people on other platforms to utilize your code, you should consider using {unimidi}[http://github.com/arirusso/unimidi]. Unimidi is a platform independent wrapper that implements this library and has a similar API.
|
8
|
+
|
9
|
+
== Features
|
10
|
+
|
11
|
+
* Simplified API
|
12
|
+
* Input and output on multiple devices concurrently
|
13
|
+
* Agnostically handle different MIDI Message types (including SysEx)
|
14
|
+
* Timestamped input events
|
15
|
+
* Internally patch MIDI to other programs using IAC (and {MIDI Patch Bay}[http://notahat.com/midi_patchbay] if you're using OSX Snow Leopard or earlier)
|
16
|
+
== Requirements
|
17
|
+
|
18
|
+
* {ffi}[http://github.com/ffi/ffi] (gem install ffi)
|
19
|
+
|
20
|
+
== Install
|
21
|
+
|
22
|
+
gem install ffi-coremidi
|
23
|
+
|
24
|
+
== Documentation
|
25
|
+
|
26
|
+
* {rdoc}[http://rubydoc.info/github/arirusso/ffi-coremidi]
|
27
|
+
|
28
|
+
== Author
|
29
|
+
|
30
|
+
* {Ari Russo}[http://github.com/arirusso] <ari.russo at gmail.com>
|
31
|
+
|
32
|
+
== Credits
|
33
|
+
|
34
|
+
This library began with some coremidi/ffi binding code for MIDI output by
|
35
|
+
|
36
|
+
* Colin Harris -- http://github.com/aberant
|
37
|
+
|
38
|
+
contained in {his fork of MIDIator}[http://github.com/aberant/midiator] and a {blog post}[http://aberant.tumblr.com/post/694878119/sending-midi-sysex-with-core-midi-and-ruby-ffi]
|
39
|
+
|
40
|
+
{MIDIator}[http://github.com/bleything/midiator] is (c)2008 by Ben Bleything and Topher Cyll and released under the MIT license (see LICENSE.midiator and LICENSE.prp)
|
41
|
+
|
42
|
+
Also thank you to {Jeremy Voorhis}[http://github.com/jvoorhis] for some useful debugging
|
43
|
+
|
44
|
+
== License
|
45
|
+
|
46
|
+
Apache 2.0, See the file LICENSE
|
47
|
+
|
48
|
+
Copyright (c) 2011 Ari Russo
|
data/lib/coremidi.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# ffi-coremidi
|
4
|
+
# Realtime MIDI IO with Ruby for OSX
|
5
|
+
# (c)2011 Ari Russo
|
6
|
+
#
|
7
|
+
|
8
|
+
# libs
|
9
|
+
require 'ffi'
|
10
|
+
require 'forwardable'
|
11
|
+
|
12
|
+
# modules
|
13
|
+
require 'coremidi/endpoint'
|
14
|
+
require 'coremidi/map'
|
15
|
+
|
16
|
+
# classes
|
17
|
+
require 'coremidi/entity'
|
18
|
+
require 'coremidi/device'
|
19
|
+
require 'coremidi/source'
|
20
|
+
require 'coremidi/destination'
|
21
|
+
|
22
|
+
module CoreMIDI
|
23
|
+
VERSION = "0.1.7"
|
24
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
module CoreMIDI
|
4
|
+
|
5
|
+
#
|
6
|
+
# Output/Destination endpoint class
|
7
|
+
#
|
8
|
+
class Destination
|
9
|
+
|
10
|
+
include Endpoint
|
11
|
+
|
12
|
+
# close this output
|
13
|
+
def close
|
14
|
+
#error = Map.MIDIClientDispose(@handle)
|
15
|
+
#raise "MIDIClientDispose returned error code #{error}" unless error.zero?
|
16
|
+
#error = Map.MIDIPortDispose(@handle)
|
17
|
+
#raise "MIDIPortDispose returned error code #{error}" unless error.zero?
|
18
|
+
#error = Map.MIDIEndpointDispose(@resource)
|
19
|
+
#raise "MIDIEndpointDispose returned error code #{error}" unless error.zero?
|
20
|
+
@enabled = false
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
# sends a MIDI message comprised of a String of hex digits
|
25
|
+
def puts_s(data)
|
26
|
+
data = data.dup
|
27
|
+
output = []
|
28
|
+
until (str = data.slice!(0,2)).eql?("")
|
29
|
+
output << str.hex
|
30
|
+
end
|
31
|
+
puts_bytes(*output)
|
32
|
+
end
|
33
|
+
alias_method :puts_bytestr, :puts_s
|
34
|
+
alias_method :puts_hex, :puts_s
|
35
|
+
|
36
|
+
# sends a MIDI messages comprised of Numeric bytes
|
37
|
+
def puts_bytes(*data)
|
38
|
+
|
39
|
+
format = "C" * data.size
|
40
|
+
bytes = (FFI::MemoryPointer.new FFI.type_size(:char) * data.size)
|
41
|
+
bytes.write_string(data.pack(format))
|
42
|
+
|
43
|
+
if data.first.eql?(0xF0) && data.last.eql?(0xF7)
|
44
|
+
puts_sysex(bytes, data.size)
|
45
|
+
else
|
46
|
+
puts_small(bytes, data.size)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# send a MIDI message of an indeterminant type
|
51
|
+
def puts(*a)
|
52
|
+
case a.first
|
53
|
+
when Array then puts_bytes(*a.first)
|
54
|
+
when Numeric then puts_bytes(*a)
|
55
|
+
when String then puts_bytestr(*a)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
alias_method :write, :puts
|
59
|
+
|
60
|
+
# enable this device; also takes a block
|
61
|
+
def enable(options = {}, &block)
|
62
|
+
#connect
|
63
|
+
@enabled = true
|
64
|
+
unless block.nil?
|
65
|
+
begin
|
66
|
+
yield(self)
|
67
|
+
ensure
|
68
|
+
close
|
69
|
+
end
|
70
|
+
else
|
71
|
+
self
|
72
|
+
end
|
73
|
+
end
|
74
|
+
alias_method :open, :enable
|
75
|
+
alias_method :start, :enable
|
76
|
+
|
77
|
+
# shortcut to the first output endpoint available
|
78
|
+
def self.first
|
79
|
+
Endpoint.first(:destination)
|
80
|
+
end
|
81
|
+
|
82
|
+
# shortcut to the last output endpoint available
|
83
|
+
def self.last
|
84
|
+
Endpoint.last(:destination)
|
85
|
+
end
|
86
|
+
|
87
|
+
# all output endpoints
|
88
|
+
def self.all
|
89
|
+
Endpoint.all_by_type[:destination]
|
90
|
+
end
|
91
|
+
|
92
|
+
protected
|
93
|
+
|
94
|
+
# base initialization for this endpoint -- done whether or not the endpoint is enabled to
|
95
|
+
# check whether it is truly available for use
|
96
|
+
def connect
|
97
|
+
client_error = enable_client
|
98
|
+
port_error = initialize_port
|
99
|
+
|
100
|
+
@resource = Map.MIDIEntityGetDestination( @entity.resource, @resource_id )
|
101
|
+
!@resource.address.zero? && client_error.zero? && port_error.zero?
|
102
|
+
end
|
103
|
+
alias_method :connect?, :connect
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
# output a short MIDI message
|
108
|
+
def puts_small(bytes, size)
|
109
|
+
packet_list = FFI::MemoryPointer.new(256)
|
110
|
+
packet_ptr = Map.MIDIPacketListInit(packet_list)
|
111
|
+
|
112
|
+
if Map::SnowLeopard
|
113
|
+
packet_ptr = Map.MIDIPacketListAdd(packet_list, 256, packet_ptr, 0, size, bytes)
|
114
|
+
else
|
115
|
+
# Pass in two 32-bit 0s for the 64 bit time
|
116
|
+
packet_ptr = Map.MIDIPacketListAdd(packet_list, 256, packet_ptr, 0, 0, size, bytes)
|
117
|
+
end
|
118
|
+
|
119
|
+
Map.MIDISend( @handle, @resource, packet_list )
|
120
|
+
end
|
121
|
+
|
122
|
+
# output a System Exclusive MIDI message
|
123
|
+
def puts_sysex(bytes, size)
|
124
|
+
|
125
|
+
request = Map::MIDISysexSendRequest.new
|
126
|
+
request[:destination] = @resource
|
127
|
+
request[:data] = bytes
|
128
|
+
request[:bytes_to_send] = size
|
129
|
+
request[:complete] = 0
|
130
|
+
request[:completion_proc] = SysexCompletionCallback
|
131
|
+
request[:completion_ref_con] = request
|
132
|
+
|
133
|
+
Map.MIDISendSysex(request)
|
134
|
+
end
|
135
|
+
|
136
|
+
SysexCompletionCallback =
|
137
|
+
FFI::Function.new(:void, [:pointer]) do |sysex_request_ptr|
|
138
|
+
p 'hi'
|
139
|
+
# this isn't working for some reason
|
140
|
+
# as of now, we don't need it though
|
141
|
+
end
|
142
|
+
|
143
|
+
# initialize a coremidi port for this endpoint
|
144
|
+
def initialize_port
|
145
|
+
port_name = Map::CF.CFStringCreateWithCString(nil, "Port #{@resource_id}: #{name}", 0)
|
146
|
+
outport_ptr = FFI::MemoryPointer.new(:pointer)
|
147
|
+
error = Map.MIDIOutputPortCreate(@client, port_name, outport_ptr)
|
148
|
+
@handle = outport_ptr.read_pointer
|
149
|
+
error
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
module CoreMIDI
|
4
|
+
|
5
|
+
class Device
|
6
|
+
|
7
|
+
attr_reader :entities,
|
8
|
+
# unique Numeric id
|
9
|
+
:id,
|
10
|
+
# device name from coremidi
|
11
|
+
:name
|
12
|
+
|
13
|
+
def initialize(id, device_pointer, options = {})
|
14
|
+
include_if_offline = options[:include_offline] || false
|
15
|
+
@id = id
|
16
|
+
@resource = device_pointer
|
17
|
+
@entities = []
|
18
|
+
|
19
|
+
prop = Map::CF.CFStringCreateWithCString( nil, "name", 0 )
|
20
|
+
name = Map::CF.CFStringCreateWithCString( nil, id.to_s, 0 )
|
21
|
+
Map::MIDIObjectGetStringProperty(@resource, prop, name)
|
22
|
+
|
23
|
+
@name = Map::CF.CFStringGetCStringPtr(name.read_pointer, 0).read_string
|
24
|
+
populate_entities(:include_offline => include_if_offline)
|
25
|
+
end
|
26
|
+
|
27
|
+
# returns all devices which are cached in an instance variable @devices on the Device class
|
28
|
+
#
|
29
|
+
# options:
|
30
|
+
#
|
31
|
+
# * <b>cache</b>: if false, the device list will never be cached. this would be useful if one needs to alter the device list (e.g. plug in a USB MIDI interface) while their program is running.
|
32
|
+
# * <b>include_offline</b>: if true, devices marked offline by coremidi will be included in the list
|
33
|
+
#
|
34
|
+
def self.all(options = {})
|
35
|
+
use_cache = options[:cache] || true
|
36
|
+
include_offline = options[:include_offline] || false
|
37
|
+
if @devices.nil? || @devices.empty? || !use_cache
|
38
|
+
@devices = []
|
39
|
+
i = 0
|
40
|
+
while !(device_pointer = Map.MIDIGetDevice(i)).null?
|
41
|
+
device = new(i, device_pointer, :include_offline => include_offline)
|
42
|
+
@devices << device
|
43
|
+
i+=1
|
44
|
+
end
|
45
|
+
populate_endpoint_ids
|
46
|
+
end
|
47
|
+
@devices
|
48
|
+
end
|
49
|
+
|
50
|
+
# Refresh the Device cash. You'll need to do this if, for instance, you plug in
|
51
|
+
# a USB MIDI device while the program is running
|
52
|
+
def self.refresh
|
53
|
+
@devices.clear
|
54
|
+
end
|
55
|
+
|
56
|
+
# returns all of the Endpoints for this device
|
57
|
+
def endpoints
|
58
|
+
endpoints = { :source => [], :destination => [] }
|
59
|
+
endpoints.keys.each do |k|
|
60
|
+
endpoints[k] += entities.map { |entity| entity.endpoints[k] }.flatten
|
61
|
+
end
|
62
|
+
endpoints
|
63
|
+
end
|
64
|
+
|
65
|
+
# assign all of this Device's endpoints an consecutive local id
|
66
|
+
def populate_endpoint_ids(starting_id)
|
67
|
+
i = 0
|
68
|
+
entities.each_with_index { |entity| i += entity.populate_endpoint_ids(i + starting_id) }
|
69
|
+
i
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
# gives all of the endpoints for all devices a consecutive local id
|
75
|
+
def self.populate_endpoint_ids
|
76
|
+
i = 0
|
77
|
+
all.each { |device| i += device.populate_endpoint_ids(i) }
|
78
|
+
end
|
79
|
+
|
80
|
+
# populates the entities for this device. these are in turn used to gather the endpoints
|
81
|
+
def populate_entities(options = {})
|
82
|
+
include_if_offline = options[:include_offline] || false
|
83
|
+
i = 0
|
84
|
+
while !(entity_pointer = Map.MIDIDeviceGetEntity(@resource, i)).null?
|
85
|
+
@entities << Entity.new(entity_pointer, :include_offline => include_if_offline)
|
86
|
+
i += 1
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
module CoreMIDI
|
4
|
+
|
5
|
+
module Endpoint
|
6
|
+
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
# has the endpoint been initialized?
|
10
|
+
attr_reader :enabled,
|
11
|
+
:entity,
|
12
|
+
# unique local Numeric id of the endpoint
|
13
|
+
:id,
|
14
|
+
:resource_id,
|
15
|
+
# :input or :output
|
16
|
+
:type
|
17
|
+
|
18
|
+
def_delegators :entity, :manufacturer, :model, :name
|
19
|
+
|
20
|
+
alias_method :enabled?, :enabled
|
21
|
+
|
22
|
+
def initialize(resource_id, entity, options = {}, &block)
|
23
|
+
@entity = entity
|
24
|
+
@resource_id = resource_id
|
25
|
+
@type = self.class.name.split('::').last.downcase.to_sym
|
26
|
+
@enabled = false
|
27
|
+
end
|
28
|
+
|
29
|
+
# is this endpoint online?
|
30
|
+
def online?
|
31
|
+
@entity.online? && connect?
|
32
|
+
end
|
33
|
+
|
34
|
+
# sets the id for this endpoint (the id is immutable once its set)
|
35
|
+
def id=(val)
|
36
|
+
@id ||= val
|
37
|
+
end
|
38
|
+
|
39
|
+
# select the first endpoint of type <em>type</em>
|
40
|
+
def self.first(type)
|
41
|
+
all_by_type[type].first
|
42
|
+
end
|
43
|
+
|
44
|
+
# select the last endpoint of type <em>type</em>
|
45
|
+
def self.last(type)
|
46
|
+
all_by_type[type].last
|
47
|
+
end
|
48
|
+
|
49
|
+
# a Hash of :source and :destination endpoints
|
50
|
+
def self.all_by_type
|
51
|
+
{
|
52
|
+
:source => Device.all.map { |d| d.endpoints[:source] }.flatten,
|
53
|
+
:destination => Device.all.map { |d| d.endpoints[:destination] }.flatten
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
# all endpoints of both types
|
58
|
+
def self.all
|
59
|
+
Device.all.map { |d| d.endpoints }.flatten
|
60
|
+
end
|
61
|
+
|
62
|
+
protected
|
63
|
+
|
64
|
+
# enables the coremidi MIDI client that will go with this endpoint
|
65
|
+
def enable_client
|
66
|
+
client_name = Map::CF.CFStringCreateWithCString( nil, "Client #{@resource_id} #{name}", 0 )
|
67
|
+
client_ptr = FFI::MemoryPointer.new(:pointer)
|
68
|
+
error = Map.MIDIClientCreate(client_name, nil, nil, client_ptr)
|
69
|
+
@client = client_ptr.read_pointer
|
70
|
+
error
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
module CoreMIDI
|
4
|
+
|
5
|
+
class Entity
|
6
|
+
|
7
|
+
attr_reader :endpoints,
|
8
|
+
:is_online,
|
9
|
+
:manufacturer,
|
10
|
+
:model,
|
11
|
+
:name,
|
12
|
+
:resource
|
13
|
+
|
14
|
+
alias_method :online?, :is_online
|
15
|
+
|
16
|
+
def initialize(resource, options = {}, &block)
|
17
|
+
@endpoints = { :source => [], :destination => [] }
|
18
|
+
@resource = resource
|
19
|
+
@manufacturer = get_property(:manufacturer)
|
20
|
+
@model = get_property(:model)
|
21
|
+
@name = "#{@manufacturer} #{@model}"
|
22
|
+
@is_online = get_property(:offline, :type => :int) == 0
|
23
|
+
@endpoints.keys.each { |type| populate_endpoints(type) }
|
24
|
+
end
|
25
|
+
|
26
|
+
# assign all of this Entity's endpoints an consecutive local id
|
27
|
+
def populate_endpoint_ids(starting_id)
|
28
|
+
i = 0
|
29
|
+
@endpoints.values.flatten.each do |e|
|
30
|
+
e.id = (i + starting_id)
|
31
|
+
i += 1
|
32
|
+
end
|
33
|
+
i
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# populate endpoints of <em>type</em> for this entity
|
39
|
+
def populate_endpoints(type, options = {})
|
40
|
+
include_if_offline = options[:include_offline] || false
|
41
|
+
endpoint_class = case type
|
42
|
+
when :source then Source
|
43
|
+
when :destination then Destination
|
44
|
+
end
|
45
|
+
num_endpoints = number_of_endpoints(type)
|
46
|
+
(0..num_endpoints).each do |i|
|
47
|
+
ep = endpoint_class.new(i, self)
|
48
|
+
@endpoints[type] << ep if ep.online? || include_if_offline
|
49
|
+
end
|
50
|
+
@endpoints[type].size
|
51
|
+
end
|
52
|
+
|
53
|
+
# gets the number of endpoints for this entity
|
54
|
+
def number_of_endpoints(type)
|
55
|
+
case type
|
56
|
+
when :source then Map.MIDIEntityGetNumberOfSources(@resource)
|
57
|
+
when :destination then Map.MIDIEntityGetNumberOfDestinations(@resource)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# gets a CFString property
|
62
|
+
def get_string(name, pointer)
|
63
|
+
prop = Map::CF.CFStringCreateWithCString( nil, name.to_s, 0 )
|
64
|
+
val = Map::CF.CFStringCreateWithCString( nil, name.to_s, 0 ) # placeholder
|
65
|
+
Map::MIDIObjectGetStringProperty(pointer, prop, val)
|
66
|
+
Map::CF.CFStringGetCStringPtr(val.read_pointer, 0).read_string rescue nil
|
67
|
+
end
|
68
|
+
|
69
|
+
# gets an Integer property
|
70
|
+
def get_int(name, pointer)
|
71
|
+
prop = Map::CF.CFStringCreateWithCString( nil, name.to_s, 0 )
|
72
|
+
val = FFI::MemoryPointer.new(:pointer, 32)
|
73
|
+
Map::MIDIObjectGetIntegerProperty(pointer, prop, val)
|
74
|
+
val.read_int
|
75
|
+
end
|
76
|
+
|
77
|
+
# gets a CString or Integer property from this Endpoint's entity
|
78
|
+
def get_property(name, options = {})
|
79
|
+
from = options[:from] || @resource
|
80
|
+
type = options[:type] || :string
|
81
|
+
|
82
|
+
case type
|
83
|
+
when :string then get_string(name, from)
|
84
|
+
when :int then get_int(name, from)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
data/lib/coremidi/map.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
module CoreMIDI
|
4
|
+
|
5
|
+
#
|
6
|
+
# coremidi binding
|
7
|
+
#
|
8
|
+
#
|
9
|
+
module Map
|
10
|
+
|
11
|
+
extend FFI::Library
|
12
|
+
ffi_lib '/System/Library/Frameworks/CoreMIDI.framework/Versions/Current/CoreMIDI'
|
13
|
+
|
14
|
+
SnowLeopard = `uname -r`.scan(/\d*\.\d*/).first.to_f >= 10.6
|
15
|
+
|
16
|
+
typedef :pointer, :CFStringRef
|
17
|
+
typedef :int32, :ItemCount
|
18
|
+
typedef :pointer, :MIDIClientRef
|
19
|
+
typedef :pointer, :MIDIDeviceRef
|
20
|
+
typedef :pointer, :MIDIEndpointRef
|
21
|
+
typedef :pointer, :MIDIEntityRef
|
22
|
+
typedef :pointer, :MIDIObjectRef
|
23
|
+
typedef :pointer, :MIDIPortRef
|
24
|
+
#typedef :pointer, :MIDIReadProc
|
25
|
+
typedef :uint32, :MIDITimeStamp
|
26
|
+
typedef :int32, :OSStatus
|
27
|
+
|
28
|
+
class MIDISysexSendRequest < FFI::Struct
|
29
|
+
|
30
|
+
layout :destination, :MIDIEndpointRef,
|
31
|
+
:data, :pointer,
|
32
|
+
:bytes_to_send, :uint32,
|
33
|
+
:complete, :int,
|
34
|
+
:reserved, [:char, 3],
|
35
|
+
:completion_proc, :pointer,
|
36
|
+
:completion_ref_con, :pointer
|
37
|
+
end
|
38
|
+
|
39
|
+
class MIDIPacket < FFI::Struct
|
40
|
+
|
41
|
+
layout :timestamp, :MIDITimeStamp,
|
42
|
+
:nothing, :uint32, # no idea...
|
43
|
+
:length, :uint16,
|
44
|
+
:data, [:uint8, 256]
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
class MIDIPacketList < FFI::Struct
|
49
|
+
layout :numPackets, :uint32,
|
50
|
+
:packet, [MIDIPacket.by_value, 1]
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
callback :MIDIReadProc, [MIDIPacketList.by_ref, :pointer, :pointer], :pointer
|
55
|
+
|
56
|
+
attach_function :MIDIClientCreate, [:pointer, :pointer, :pointer, :pointer], :int
|
57
|
+
|
58
|
+
attach_function :MIDIClientDispose, [:pointer], :int
|
59
|
+
|
60
|
+
# MIDIEntityRef MIDIDeviceGetEntity (MIDIDeviceRef device, ItemCount entityIndex0);
|
61
|
+
attach_function :MIDIDeviceGetEntity, [:MIDIDeviceRef, :ItemCount], :MIDIEntityRef
|
62
|
+
|
63
|
+
attach_function :MIDIGetNumberOfDestinations, [], :ItemCount
|
64
|
+
|
65
|
+
attach_function :MIDIGetNumberOfDevices, [], :ItemCount
|
66
|
+
|
67
|
+
attach_function :MIDIGetDestination, [:int], :pointer
|
68
|
+
|
69
|
+
#extern OSStatus MIDIEndpointDispose( MIDIEndpointRef endpt );
|
70
|
+
attach_function :MIDIEndpointDispose, [:MIDIEndpointRef], :OSStatus
|
71
|
+
|
72
|
+
# MIDIEndpointRef MIDIEntityGetDestination( MIDIEntityRef entity, ItemCount destIndex0 );
|
73
|
+
attach_function :MIDIEntityGetDestination, [:MIDIEntityRef, :int], :MIDIEndpointRef
|
74
|
+
|
75
|
+
# ItemCount MIDIEntityGetNumberOfDestinations (MIDIEntityRef entity);
|
76
|
+
attach_function :MIDIEntityGetNumberOfDestinations, [:MIDIEntityRef], :ItemCount
|
77
|
+
|
78
|
+
# ItemCount MIDIEntityGetNumberOfSources (MIDIEntityRef entity);
|
79
|
+
attach_function :MIDIEntityGetNumberOfSources, [:MIDIEntityRef], :ItemCount
|
80
|
+
|
81
|
+
# MIDIEndpointRef MIDIEntityGetSource (MIDIEntityRef entity, ItemCount sourceIndex0);
|
82
|
+
attach_function :MIDIEntityGetSource, [:MIDIEntityRef, :ItemCount], :MIDIEndpointRef
|
83
|
+
|
84
|
+
attach_function :MIDIGetDevice, [:ItemCount], :MIDIDeviceRef
|
85
|
+
|
86
|
+
# extern OSStatus MIDIInputPortCreate( MIDIClientRef client, CFStringRef portName, MIDIReadProc readProc, void * refCon, MIDIPortRef * outPort );
|
87
|
+
attach_function :MIDIInputPortCreate, [:MIDIClientRef, :CFStringRef, :MIDIReadProc, :pointer, :MIDIPortRef], :OSStatus
|
88
|
+
|
89
|
+
# extern OSStatus MIDIObjectGetIntegerProperty( MIDIObjectRef obj, CFStringRef propertyID, SInt32 * outValue );
|
90
|
+
attach_function :MIDIObjectGetIntegerProperty, [:MIDIObjectRef, :CFStringRef, :pointer], :OSStatus
|
91
|
+
# OSStatus MIDIObjectGetStringProperty (MIDIObjectRef obj, CFStringRef propertyID, CFStringRef *str);
|
92
|
+
attach_function :MIDIObjectGetStringProperty, [:MIDIObjectRef, :CFStringRef, :pointer], :OSStatus
|
93
|
+
\
|
94
|
+
# extern OSStatus MIDIOutputPortCreate( MIDIClientRef client, CFStringRef portName, MIDIPortRef * outPort );
|
95
|
+
attach_function :MIDIOutputPortCreate, [:MIDIClientRef, :CFStringRef, :pointer], :int
|
96
|
+
|
97
|
+
attach_function :MIDIPacketListInit, [:pointer], :pointer
|
98
|
+
|
99
|
+
#extern OSStatus MIDIPortConnectSource( MIDIPortRef port, MIDIEndpointRef source, void * connRefCon )
|
100
|
+
attach_function :MIDIPortConnectSource, [:MIDIPortRef, :MIDIEndpointRef, :pointer], :OSStatus
|
101
|
+
|
102
|
+
#extern OSStatus MIDIPortDisconnectSource( MIDIPortRef port, MIDIEndpointRef source );
|
103
|
+
attach_function :MIDIPortDisconnectSource, [:MIDIPortRef, :MIDIEndpointRef], :OSStatus
|
104
|
+
|
105
|
+
#extern OSStatus MIDIPortDispose(MIDIPortRef port );
|
106
|
+
attach_function :MIDIPortDispose, [:MIDIPortRef], :OSStatus
|
107
|
+
|
108
|
+
#extern OSStatus MIDISend(MIDIPortRef port,MIDIEndpointRef dest,const MIDIPacketList *pktlist);
|
109
|
+
attach_function :MIDISend, [:MIDIPortRef, :MIDIEndpointRef, :pointer], :int
|
110
|
+
|
111
|
+
attach_function :MIDISendSysex, [:pointer], :int
|
112
|
+
|
113
|
+
if SnowLeopard
|
114
|
+
attach_function :MIDIPacketListAdd, [:pointer, :int, :pointer, :int, :int, :pointer], :pointer
|
115
|
+
else
|
116
|
+
# extern MIDIPacket * MIDIPacketListAdd( MIDIPacketList * pktlist, ByteCount listSize, MIDIPacket * curPacket, MIDITimeStamp time, ByteCount nData, const Byte * data)
|
117
|
+
attach_function :MIDIPacketListAdd, [:pointer, :int, :pointer, :int, :int, :int, :pointer], :pointer
|
118
|
+
end
|
119
|
+
|
120
|
+
module CF
|
121
|
+
|
122
|
+
extend FFI::Library
|
123
|
+
ffi_lib '/System/Library/Frameworks/CoreFoundation.framework/Versions/Current/CoreFoundation'
|
124
|
+
|
125
|
+
# CFString* CFStringCreateWithCString( ?, CString, encoding)
|
126
|
+
attach_function :CFStringCreateWithCString, [:pointer, :string, :int], :pointer
|
127
|
+
# CString* CFStringGetCStringPtr(CFString*, encoding)
|
128
|
+
attach_function :CFStringGetCStringPtr, [:pointer, :int], :pointer
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
module HostTime
|
133
|
+
extend FFI::Library
|
134
|
+
ffi_lib '/System/Library/Frameworks/CoreAudio.framework/Versions/Current/CoreAudio'
|
135
|
+
|
136
|
+
attach_function :AudioConvertHostTimeToNanos, [:uint64], :uint64
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
module CoreMIDI
|
4
|
+
|
5
|
+
#
|
6
|
+
# Input/Source endpoint class
|
7
|
+
#
|
8
|
+
class Source
|
9
|
+
|
10
|
+
include Endpoint
|
11
|
+
|
12
|
+
attr_reader :buffer
|
13
|
+
|
14
|
+
#
|
15
|
+
# returns an array of MIDI event hashes as such:
|
16
|
+
# [
|
17
|
+
# { :data => [144, 60, 100], :timestamp => 1024 },
|
18
|
+
# { :data => [128, 60, 100], :timestamp => 1100 },
|
19
|
+
# { :data => [144, 40, 120], :timestamp => 1200 }
|
20
|
+
# ]
|
21
|
+
#
|
22
|
+
# the data is an array of Numeric bytes
|
23
|
+
# the timestamp is the number of millis since this input was enabled
|
24
|
+
#
|
25
|
+
def gets
|
26
|
+
until queued_messages?
|
27
|
+
end
|
28
|
+
msgs = queued_messages
|
29
|
+
@pointer = @buffer.length
|
30
|
+
msgs
|
31
|
+
end
|
32
|
+
alias_method :read, :gets
|
33
|
+
|
34
|
+
# same as gets but returns message data as string of hex digits as such:
|
35
|
+
# [
|
36
|
+
# { :data => "904060", :timestamp => 904 },
|
37
|
+
# { :data => "804060", :timestamp => 1150 },
|
38
|
+
# { :data => "90447F", :timestamp => 1300 }
|
39
|
+
# ]
|
40
|
+
#
|
41
|
+
#
|
42
|
+
def gets_s
|
43
|
+
msgs = gets
|
44
|
+
msgs.each { |msg| msg[:data] = numeric_bytes_to_hex_string(msg[:data]) }
|
45
|
+
msgs
|
46
|
+
end
|
47
|
+
alias_method :gets_bytestr, :gets_s
|
48
|
+
|
49
|
+
# enable this the input for use; can be passed a block
|
50
|
+
def enable(options = {}, &block)
|
51
|
+
@enabled = true
|
52
|
+
|
53
|
+
unless block.nil?
|
54
|
+
begin
|
55
|
+
yield(self)
|
56
|
+
ensure
|
57
|
+
close
|
58
|
+
end
|
59
|
+
else
|
60
|
+
self
|
61
|
+
end
|
62
|
+
end
|
63
|
+
alias_method :open, :enable
|
64
|
+
alias_method :start, :enable
|
65
|
+
|
66
|
+
# close this input
|
67
|
+
def close
|
68
|
+
#error = Map.MIDIPortDisconnectSource( @handle, @resource )
|
69
|
+
#raise "MIDIPortDisconnectSource returned error code #{error}" unless error.zero?
|
70
|
+
#error = Map.MIDIClientDispose(@handle)
|
71
|
+
#raise "MIDIClientDispose returned error code #{error}" unless error.zero?
|
72
|
+
#error = Map.MIDIPortDispose(@handle)
|
73
|
+
#raise "MIDIPortDispose returned error code #{error}" unless error.zero?
|
74
|
+
#error = Map.MIDIEndpointDispose(@resource)
|
75
|
+
#raise "MIDIEndpointDispose returned error code #{error}" unless error.zero?
|
76
|
+
@enabled = false
|
77
|
+
end
|
78
|
+
|
79
|
+
# shortcut to the first available input endpoint
|
80
|
+
def self.first
|
81
|
+
Endpoint.first(:source)
|
82
|
+
end
|
83
|
+
|
84
|
+
# shortcut to the last available input endpoint
|
85
|
+
def self.last
|
86
|
+
Endpoint.last(:source)
|
87
|
+
end
|
88
|
+
|
89
|
+
# all input endpoints
|
90
|
+
def self.all
|
91
|
+
Endpoint.all_by_type[:source]
|
92
|
+
end
|
93
|
+
|
94
|
+
protected
|
95
|
+
|
96
|
+
# base initialization for this endpoint -- done whether or not the endpoint is enabled to
|
97
|
+
# check whether it is truly available for use
|
98
|
+
def connect
|
99
|
+
enable_client
|
100
|
+
initialize_port
|
101
|
+
@resource = Map.MIDIEntityGetSource(@entity.resource, @resource_id)
|
102
|
+
error = Map.MIDIPortConnectSource(@handle, @resource, nil )
|
103
|
+
initialize_buffer
|
104
|
+
@sysex_buffer = []
|
105
|
+
@start_time = Time.now.to_f
|
106
|
+
|
107
|
+
error.zero?
|
108
|
+
end
|
109
|
+
alias_method :connect?, :connect
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
# returns new MIDI messages from the queue
|
114
|
+
def queued_messages
|
115
|
+
@buffer.slice(@pointer, @buffer.length - @pointer)
|
116
|
+
end
|
117
|
+
|
118
|
+
# are there new MIDI messages in the queue?
|
119
|
+
def queued_messages?
|
120
|
+
@pointer < @buffer.length
|
121
|
+
end
|
122
|
+
|
123
|
+
# the callback which is called by coremidi when new MIDI messages are in the buffer
|
124
|
+
def get_event_callback
|
125
|
+
Proc.new do | new_packets, refCon_ptr, connRefCon_ptr |
|
126
|
+
time = Time.now.to_f
|
127
|
+
packet = new_packets[:packet][0]
|
128
|
+
len = packet[:length]
|
129
|
+
#p "packets received: #{new_packets[:numPackets]}"
|
130
|
+
#p "first packet length: #{len} data: #{packet[:data].to_a.to_s}"
|
131
|
+
if len > 0
|
132
|
+
bytes = packet[:data].to_a[0, len]
|
133
|
+
if bytes.first.eql?(0xF0) || !@sysex_buffer.empty?
|
134
|
+
@sysex_buffer += bytes
|
135
|
+
if bytes.last.eql?(0xF7)
|
136
|
+
bytes = @sysex_buffer.dup
|
137
|
+
@sysex_buffer.clear
|
138
|
+
end
|
139
|
+
end
|
140
|
+
@buffer << get_message_formatted(bytes, time) if @sysex_buffer.empty?
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# timestamp
|
146
|
+
def timestamp(now)
|
147
|
+
((now - @start_time) * 1000)
|
148
|
+
end
|
149
|
+
|
150
|
+
# give a message its timestamp and package it in a Hash
|
151
|
+
def get_message_formatted(raw, time)
|
152
|
+
{ :data => raw, :timestamp => timestamp(time) }
|
153
|
+
end
|
154
|
+
|
155
|
+
# initialize a coremidi port for this endpoint
|
156
|
+
def initialize_port
|
157
|
+
port_name = Map::CF.CFStringCreateWithCString(nil, "Port #{@resource_id}: #{name}", 0)
|
158
|
+
handle_ptr = FFI::MemoryPointer.new(:pointer)
|
159
|
+
@callback = get_event_callback
|
160
|
+
error = Map.MIDIInputPortCreate(@client, port_name, @callback, nil, handle_ptr)
|
161
|
+
@handle = handle_ptr.read_pointer
|
162
|
+
raise "MIDIInputPortCreate returned error code #{error}" unless error.zero?
|
163
|
+
end
|
164
|
+
|
165
|
+
# initialize the MIDI message buffer
|
166
|
+
def initialize_buffer
|
167
|
+
@pointer = 0
|
168
|
+
@buffer = []
|
169
|
+
def @buffer.clear
|
170
|
+
super
|
171
|
+
@pointer = 0
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# convert an array of numeric byes to a hex string
|
176
|
+
# e.g.
|
177
|
+
# [0x90, 0x40, 0x40] -> "904040"
|
178
|
+
def numeric_bytes_to_hex_string(bytes)
|
179
|
+
bytes.map { |b| s = b.to_s(16).upcase; b < 16 ? s = "0" + s : s; s }.join
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
dir = File.dirname(File.expand_path(__FILE__))
|
4
|
+
$LOAD_PATH.unshift dir + '/../lib'
|
5
|
+
|
6
|
+
require 'test/unit'
|
7
|
+
require 'coremidi'
|
8
|
+
|
9
|
+
module TestHelper
|
10
|
+
|
11
|
+
def self.select_devices
|
12
|
+
$test_device ||= {}
|
13
|
+
{ :input => CoreMIDI::Source.all, :output => CoreMIDI::Destination.all }.each do |type, devs|
|
14
|
+
puts ""
|
15
|
+
puts "select an #{type.to_s}..."
|
16
|
+
while $test_device[type].nil?
|
17
|
+
devs.each do |device|
|
18
|
+
puts "#{device.id}: #{device.name}"
|
19
|
+
end
|
20
|
+
selection = $stdin.gets.chomp
|
21
|
+
if selection != ""
|
22
|
+
selection = selection.to_i
|
23
|
+
$test_device[type] = devs.find { |d| d.id == selection }
|
24
|
+
puts "selected #{selection} for #{type.to_s}" unless $test_device[type]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def bytestrs_to_ints(arr)
|
31
|
+
data = arr.map { |m| m[:data] }.join
|
32
|
+
output = []
|
33
|
+
until (bytestr = data.slice!(0,2)).eql?("")
|
34
|
+
output << bytestr.hex
|
35
|
+
end
|
36
|
+
output
|
37
|
+
end
|
38
|
+
|
39
|
+
# some MIDI messages
|
40
|
+
VariousMIDIMessages = [
|
41
|
+
[0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7], # SysEx
|
42
|
+
[0x90, 100, 100], # note on
|
43
|
+
[0x90, 43, 100], # note on
|
44
|
+
[0x90, 76, 100], # note on
|
45
|
+
[0x90, 60, 100], # note on
|
46
|
+
[0x80, 100, 100] # note off
|
47
|
+
]
|
48
|
+
|
49
|
+
# some MIDI messages
|
50
|
+
VariousMIDIByteStrMessages = [
|
51
|
+
"F04110421240007F0041F7", # SysEx
|
52
|
+
"906440", # note on
|
53
|
+
"804340" # note off
|
54
|
+
]
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
TestHelper.select_devices
|
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'helper'
|
4
|
+
|
5
|
+
class InputBufferTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
include CoreMIDI
|
8
|
+
include TestHelper
|
9
|
+
|
10
|
+
def test_input_buffer
|
11
|
+
sleep(1)
|
12
|
+
|
13
|
+
messages = VariousMIDIMessages
|
14
|
+
bytes = []
|
15
|
+
|
16
|
+
$test_device[:output].open do |output|
|
17
|
+
$test_device[:input].open do |input|
|
18
|
+
|
19
|
+
input.buffer.clear
|
20
|
+
|
21
|
+
messages.each do |msg|
|
22
|
+
|
23
|
+
$>.puts "sending: " + msg.inspect
|
24
|
+
|
25
|
+
output.puts(msg)
|
26
|
+
|
27
|
+
bytes += msg
|
28
|
+
|
29
|
+
sleep(0.5)
|
30
|
+
|
31
|
+
buffer = input.buffer.map { |m| m[:data] }.flatten
|
32
|
+
|
33
|
+
$>.puts "received: " + buffer.to_s
|
34
|
+
|
35
|
+
assert_equal(bytes, buffer)
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
assert_equal(bytes.length, input.buffer.map { |m| m[:data] }.flatten.length)
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
data/test/test_io.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'helper'
|
4
|
+
|
5
|
+
class IoTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
include CoreMIDI
|
8
|
+
include TestHelper
|
9
|
+
|
10
|
+
def test_full_io
|
11
|
+
sleep(2)
|
12
|
+
messages = VariousMIDIMessages
|
13
|
+
messages_arr = messages.inject { |a,b| a+b }.flatten
|
14
|
+
received_arr = []
|
15
|
+
pointer = 0
|
16
|
+
$test_device[:output].open do |output|
|
17
|
+
$test_device[:input].open do |input|
|
18
|
+
|
19
|
+
input.buffer.clear
|
20
|
+
|
21
|
+
messages.each do |msg|
|
22
|
+
|
23
|
+
$>.puts "sending: " + msg.inspect
|
24
|
+
|
25
|
+
output.puts(msg)
|
26
|
+
sleep(1)
|
27
|
+
received = input.gets.map { |m| m[:data] }.flatten
|
28
|
+
|
29
|
+
$>.puts "received: " + received.inspect
|
30
|
+
|
31
|
+
assert_equal(messages_arr.slice(pointer, received.length), received)
|
32
|
+
|
33
|
+
pointer += received.length
|
34
|
+
|
35
|
+
received_arr += received
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
assert_equal(messages_arr.length, received_arr.length)
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# ** this test assumes that TestOutput is connected to TestInput
|
46
|
+
def test_full_io_bytestr
|
47
|
+
sleep(2) # pause between tests
|
48
|
+
|
49
|
+
messages = VariousMIDIByteStrMessages
|
50
|
+
messages_str = messages.join
|
51
|
+
received_str = ""
|
52
|
+
pointer = 0
|
53
|
+
|
54
|
+
$test_device[:output].open do |output|
|
55
|
+
$test_device[:input].open do |input|
|
56
|
+
|
57
|
+
input.buffer.clear
|
58
|
+
|
59
|
+
messages.each do |msg|
|
60
|
+
|
61
|
+
$>.puts "sending: " + msg.inspect
|
62
|
+
|
63
|
+
output.puts_s(msg)
|
64
|
+
sleep(1)
|
65
|
+
received = input.gets_bytestr.map { |m| m[:data] }.flatten.join
|
66
|
+
$>.puts "received: " + received.inspect
|
67
|
+
|
68
|
+
assert_equal(messages_str.slice(pointer, received.length), received)
|
69
|
+
|
70
|
+
pointer += received.length
|
71
|
+
|
72
|
+
received_str += received
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
assert_equal(messages_str, received_str)
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ffi-coremidi
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.7
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ari Russo
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-05 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: ffi
|
16
|
+
requirement: &70309759821860 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70309759821860
|
25
|
+
description: Realtime MIDI IO with Ruby for OSX
|
26
|
+
email:
|
27
|
+
- ari.russo@gmail.com
|
28
|
+
executables: []
|
29
|
+
extensions: []
|
30
|
+
extra_rdoc_files: []
|
31
|
+
files:
|
32
|
+
- lib/coremidi/destination.rb
|
33
|
+
- lib/coremidi/device.rb
|
34
|
+
- lib/coremidi/endpoint.rb
|
35
|
+
- lib/coremidi/entity.rb
|
36
|
+
- lib/coremidi/map.rb
|
37
|
+
- lib/coremidi/source.rb
|
38
|
+
- lib/coremidi.rb
|
39
|
+
- test/helper.rb
|
40
|
+
- test/test_input_buffer.rb
|
41
|
+
- test/test_io.rb
|
42
|
+
- LICENSE
|
43
|
+
- README.rdoc
|
44
|
+
homepage: http://github.com/arirusso/ffi-coremidi
|
45
|
+
licenses: []
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.3.6
|
62
|
+
requirements: []
|
63
|
+
rubyforge_project: ffi-coremidi
|
64
|
+
rubygems_version: 1.8.6
|
65
|
+
signing_key:
|
66
|
+
specification_version: 3
|
67
|
+
summary: Realtime MIDI IO with Ruby for OSX
|
68
|
+
test_files: []
|