ffi-coremidi 0.1.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 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.
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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: []