ffi-coremidi 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
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: []