ffi-coremidi 0.3.8 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,48 +1,43 @@
1
- module CoreMIDI
1
+ # frozen_string_literal: true
2
2
 
3
- # A MIDI entity can have any number of MIDI endpoints, each of which is a source or destination
4
- # of a 16-channel MIDI stream. By grouping a device's endpoints into entities, the system has
5
- # enough information for an application to make reasonable default assumptions about how to
6
- # communicate in a bi-directional manner with each entity, as is necessary in MIDI librarian
3
+ module CoreMIDI
4
+ # A MIDI entity can have any number of MIDI endpoints, each of which is a source or destination
5
+ # of a 16-channel MIDI stream. By grouping a device's endpoints into entities, the system has
6
+ # enough information for an application to make reasonable default assumptions about how to
7
+ # communicate in a bi-directional manner with each entity, as is necessary in MIDI librarian
7
8
  # applications.
8
9
  #
9
10
  # https://developer.apple.com/library/ios/documentation/CoreMidi/Reference/MIDIServices_Reference/Reference/reference.html
10
11
  class Entity
11
-
12
- attr_reader :endpoints,
12
+ attr_reader :endpoints,
13
13
  :manufacturer,
14
14
  :model,
15
15
  :name,
16
16
  :resource
17
-
17
+
18
18
  # @param [FFI::Pointer] resource A pointer to the underlying entity
19
19
  # @param [Hash] options
20
20
  # @option options [Boolean] :include_offline Include offline endpoints in the list
21
21
  def initialize(resource, options = {})
22
- @endpoints = {
23
- :source => [],
24
- :destination => []
22
+ @endpoints = {
23
+ source: [],
24
+ destination: []
25
25
  }
26
26
  @resource = resource
27
27
  populate(options)
28
28
  end
29
-
29
+
30
30
  # Assign all of this Entity's endpoints an consecutive local id
31
- # @param [Fixnum] starting_id
32
- # @return [Fixnum]
31
+ # @param [Integer] starting_id
32
+ # @return [Integer]
33
33
  def populate_endpoint_ids(starting_id)
34
- counter = 0
35
- @endpoints.values.flatten.each do |endpoint|
36
- endpoint.id = counter + starting_id
37
- counter += 1
38
- end
39
- counter
34
+ @endpoints.values.flatten.map.with_index { |endpoint, index| endpoint.id = index + starting_id }.length
40
35
  end
41
-
36
+
42
37
  # Is the entity online?
43
38
  # @return [Boolean]
44
39
  def online?
45
- get_int(:offline) == 0
40
+ get_int(:offline).zero?
46
41
  end
47
42
 
48
43
  private
@@ -52,54 +47,53 @@ module CoreMIDI
52
47
  def get_name
53
48
  "#{@manufacturer} #{@model}"
54
49
  end
55
-
50
+
56
51
  # Populate endpoints of a specified type for this entity
57
52
  # @param [Symbol] type The endpoint type eg :source, :destination
58
53
  # @param [Hash] options
59
54
  # @option options [Boolean] :include_offline Include offline endpoints in the list
60
- # @return [Fixnum]
55
+ # @return [Integer]
61
56
  def populate_endpoints_by_type(type, options = {})
62
57
  endpoint_class = Endpoint.get_class(type)
63
58
  num_endpoints = number_of_endpoints(type)
64
59
  (0..num_endpoints).each do |i|
65
60
  endpoint = endpoint_class.new(i, self)
66
- if endpoint.online? || options[:include_offline]
67
- @endpoints[type] << endpoint
68
- end
69
- end
70
- @endpoints[type].size
61
+ should_include_message = endpoint.online? || options[:include_offline]
62
+ @endpoints[type] << endpoint if should_include_message
63
+ end
64
+ @endpoints[type].size
71
65
  end
72
66
 
73
67
  # Populate the endpoints for this entity
74
68
  # @param [Hash] options
75
69
  # @option options [Boolean] :include_offline Include offline endpoints in the list
76
- # @return [Fixnum]
70
+ # @return [Integer]
77
71
  def populate_endpoints(options = {})
78
72
  @endpoints.keys.map { |type| populate_endpoints_by_type(type, options) }.reduce(&:+)
79
73
  end
80
-
74
+
81
75
  # The number of endpoints for this entity
82
76
  # @param [Symbol] type The endpoint type eg :source, :destination
83
77
  def number_of_endpoints(type)
84
78
  case type
85
- when :source then API.MIDIEntityGetNumberOfSources(@resource)
86
- when :destination then API.MIDIEntityGetNumberOfDestinations(@resource)
79
+ when :source then API.MIDIEntityGetNumberOfSources(@resource)
80
+ when :destination then API.MIDIEntityGetNumberOfDestinations(@resource)
87
81
  end
88
82
  end
89
-
83
+
90
84
  # A CFString property from the underlying entity
91
85
  # @param [Symbol, String] name The property name
92
86
  # @return [String, nil]
93
87
  def get_string(name)
94
88
  API.get_string(@resource, name)
95
89
  end
96
-
90
+
97
91
  # An Integer property from the underlying entity
98
92
  # @param [Symbol, String] name The property name
99
- # @return [Fixnum, nil]
93
+ # @return [Integer, nil]
100
94
  def get_int(name)
101
95
  API.get_int(@resource, name)
102
- end
96
+ end
103
97
 
104
98
  # Populate the entity properties from the underlying resource
105
99
  # @param [Hash] options
@@ -110,7 +104,5 @@ module CoreMIDI
110
104
  @name = get_name
111
105
  populate_endpoints(options)
112
106
  end
113
-
114
107
  end
115
-
116
108
  end
@@ -1,11 +1,16 @@
1
- module CoreMIDI
1
+ # frozen_string_literal: true
2
2
 
3
+ module CoreMIDI
3
4
  # Type of endpoint used for input
4
5
  class Source
5
-
6
6
  include Endpoint
7
7
 
8
- attr_reader :buffer
8
+ # The buffer of received messages since instantiation
9
+ # @return [Array<Hash>]
10
+ def buffer
11
+ fill_buffer
12
+ @buffer
13
+ end
9
14
 
10
15
  #
11
16
  # An array of MIDI event hashes as such:
@@ -20,15 +25,9 @@ module CoreMIDI
20
25
  #
21
26
  # @return [Array<Hash>]
22
27
  def gets
23
- until queued_messages?
24
- # per https://github.com/arirusso/unimidi/issues/20#issuecomment-44761318
25
- sleep(0.0001) # patch to prevent 100% CPU issue with some midi controllers
26
- end
27
- messages = queued_messages
28
- @pointer = @buffer.length
29
- messages
28
+ fill_buffer(locking: true)
30
29
  end
31
- alias_method :read, :gets
30
+ alias read gets
32
31
 
33
32
  # Same as Source#gets except that it returns message data as string of hex
34
33
  # digits as such:
@@ -46,12 +45,12 @@ module CoreMIDI
46
45
  end
47
46
  messages
48
47
  end
49
- alias_method :gets_bytestr, :gets_s
48
+ alias gets_bytestr gets_s
50
49
 
51
50
  # Enable this the input for use; can be passed a block
52
51
  # @return [Source]
53
- def enable(options = {}, &block)
54
- @enabled = true unless @enabled
52
+ def enable(_options = {})
53
+ @enabled ||= true
55
54
  if block_given?
56
55
  begin
57
56
  yield(self)
@@ -61,20 +60,20 @@ module CoreMIDI
61
60
  end
62
61
  self
63
62
  end
64
- alias_method :open, :enable
65
- alias_method :start, :enable
63
+ alias open enable
64
+ alias start enable
66
65
 
67
66
  # Close this input
68
67
  # @return [Boolean]
69
68
  def close
70
- #error = API.MIDIPortDisconnectSource( @handle, @resource )
71
- #raise "MIDIPortDisconnectSource returned error code #{error}" unless error.zero?
72
- #error = API.MIDIClientDispose(@handle)
73
- #raise "MIDIClientDispose returned error code #{error}" unless error.zero?
74
- #error = API.MIDIPortDispose(@handle)
75
- #raise "MIDIPortDispose returned error code #{error}" unless error.zero?
76
- #error = API.MIDIEndpointDispose(@resource)
77
- #raise "MIDIEndpointDispose returned error code #{error}" unless error.zero?
69
+ # error = API.MIDIPortDisconnectSource( @handle, @resource )
70
+ # raise "MIDIPortDisconnectSource returned error code #{error}" unless error.zero?
71
+ # error = API.MIDIClientDispose(@handle)
72
+ # raise "MIDIClientDispose returned error code #{error}" unless error.zero?
73
+ # error = API.MIDIPortDispose(@handle)
74
+ # raise "MIDIPortDispose returned error code #{error}" unless error.zero?
75
+ # error = API.MIDIEndpointDispose(@resource)
76
+ # raise "MIDIEndpointDispose returned error code #{error}" unless error.zero?
78
77
  if @enabled
79
78
  @enabled = false
80
79
  true
@@ -103,24 +102,49 @@ module CoreMIDI
103
102
 
104
103
  protected
105
104
 
105
+ def truncate_buffer
106
+ @buffer.slice!(-1024, 1024)
107
+ end
108
+
109
+ # Migrate new received messages from the callback queue to
110
+ # the buffer
111
+ def fill_buffer(locking: nil)
112
+ locking ||= false
113
+
114
+ messages = []
115
+
116
+ if locking && @queue.empty?
117
+ @threads_sync_semaphore.synchronize do
118
+ @threads_waiting << Thread.current
119
+ end
120
+ sleep
121
+ end
122
+
123
+ messages << @queue.pop until @queue.empty?
124
+ @buffer += messages
125
+ truncate_buffer
126
+ @pointer = @buffer.length
127
+ messages
128
+ end
129
+
106
130
  # Base initialization for this endpoint -- done whether or not the endpoint is enabled to check whether
107
131
  # it is truly available for use
108
132
  def connect
109
133
  enable_client
110
134
  initialize_port
111
135
  @resource = API.MIDIEntityGetSource(@entity.resource, @resource_id)
112
- error = API.MIDIPortConnectSource(@handle, @resource, nil )
136
+ error = API.MIDIPortConnectSource(@handle, @resource, nil)
113
137
  initialize_buffer
138
+ @queue = Queue.new
114
139
  @sysex_buffer = []
115
- @start_time = Time.now.to_f
116
140
 
117
141
  error.zero?
118
142
  end
119
- alias_method :connect?, :connect
143
+ alias connect? connect
120
144
 
121
145
  private
122
146
 
123
- # Add a single message to the buffer
147
+ # Add a single message to the callback queue
124
148
  # @param [Array<Fixnum>] bytes Message data
125
149
  # @param [Float] timestamp The system float timestamp
126
150
  # @return [Array<Hash>] The resulting buffer
@@ -132,31 +156,29 @@ module CoreMIDI
132
156
  @sysex_buffer.clear
133
157
  end
134
158
  end
135
- @buffer << get_message_formatted(bytes, timestamp) if @sysex_buffer.empty?
136
- @buffer
137
- end
159
+ message = get_message_formatted(bytes, timestamp)
160
+ @queue << message
138
161
 
139
- # New MIDI messages from the queue
140
- def queued_messages
141
- @buffer.slice(@pointer, @buffer.length - @pointer)
142
- end
162
+ @threads_sync_semaphore.synchronize do
163
+ @threads_waiting.each(&:run)
164
+ @threads_waiting.clear
165
+ end
143
166
 
144
- # Are there new MIDI messages in the queue?
145
- def queued_messages?
146
- @pointer < @buffer.length
167
+ message
147
168
  end
148
169
 
149
- # The callback fired by coremidi when new MIDI messages are in the buffer
170
+ # The callback fired by coremidi when new MIDI messages are received
150
171
  def get_event_callback
151
- Proc.new do |new_packets, refCon_ptr, connRefCon_ptr|
152
- begin
153
- # p "packets received: #{new_packets[:numPackets]}"
154
- timestamp = Time.now.to_f
155
- messages = get_messages(new_packets)
156
- messages.each { |message| enqueue_message(message, timestamp) }
157
- rescue Exception => exception
158
- Thread.main.raise(exception)
172
+ Thread.abort_on_exception = true
173
+ proc do |new_packets, _refCon_ptr, _connRefCon_ptr|
174
+ # p "packets received: #{new_packets[:numPackets]}"
175
+ timestamp = Time.now.to_f
176
+ messages = get_messages(new_packets)
177
+ messages.each do |message|
178
+ enqueue_message(message, timestamp)
159
179
  end
180
+ rescue Exception => e
181
+ Thread.main.raise(e)
160
182
  end
161
183
  end
162
184
 
@@ -169,18 +191,18 @@ module CoreMIDI
169
191
  data = first[:data].to_a
170
192
  messages = []
171
193
  messages << data.slice!(0, first[:length])
172
- (count - 1).times do |i|
194
+ (count - 1).times do |_i|
173
195
  length_index = find_next_length_index(data)
174
196
  message_length = data[length_index]
175
- unless message_length.nil?
176
- packet_start_index = length_index + 2
177
- packet_end_index = packet_start_index + message_length
178
- if data.length >= packet_end_index + 1
179
- packet = data.slice!(0..packet_end_index)
180
- message = packet.slice(packet_start_index, message_length)
181
- messages << message
182
- end
183
- end
197
+ next if message_length.nil?
198
+
199
+ packet_start_index = length_index + 2
200
+ packet_end_index = packet_start_index + message_length
201
+ next unless data.length >= packet_end_index + 1
202
+
203
+ packet = data.slice!(0..packet_end_index)
204
+ message = packet.slice(packet_start_index, message_length)
205
+ messages << message
184
206
  end
185
207
  messages
186
208
  end
@@ -203,18 +225,12 @@ module CoreMIDI
203
225
  end
204
226
  end
205
227
 
206
- # Timestamp for a received MIDI message
207
- # @return [Fixnum]
208
- def timestamp(now)
209
- (now - @start_time) * 1000
210
- end
211
-
212
228
  # Give a message its timestamp and package it in a Hash
213
229
  # @return [Hash]
214
230
  def get_message_formatted(raw, time)
215
231
  {
216
- :data => raw,
217
- :timestamp => timestamp(time)
232
+ data: raw,
233
+ timestamp: time
218
234
  }
219
235
  end
220
236
 
@@ -225,6 +241,7 @@ module CoreMIDI
225
241
  port = API.create_midi_input_port(@client, @resource_id, @name, @callback)
226
242
  @handle = port[:handle]
227
243
  raise "MIDIInputPortCreate returned error code #{port[:error]}" unless port[:error].zero?
244
+
228
245
  true
229
246
  end
230
247
 
@@ -239,7 +256,5 @@ module CoreMIDI
239
256
  end
240
257
  true
241
258
  end
242
-
243
259
  end
244
-
245
260
  end
@@ -1,21 +1,20 @@
1
- module CoreMIDI
1
+ # frozen_string_literal: true
2
2
 
3
- # Helper for convertig MIDI data
3
+ module CoreMIDI
4
+ # Helper for converting MIDI data
4
5
  module TypeConversion
5
-
6
- extend self
6
+ module_function
7
7
 
8
8
  # Convert an array of numeric byes to a hex string (e.g. [0x90, 0x40, 0x40] becomes "904040")
9
- # @param [Array<Fixnum>] bytes
9
+ # @param [Array<Integer>] bytes
10
10
  # @return [String]
11
11
  def numeric_bytes_to_hex_string(bytes)
12
- string_bytes = bytes.map do |byte|
12
+ string_bytes = bytes.map do |byte|
13
13
  str = byte.to_s(16).upcase
14
- str = "0" + str if byte < 16
14
+ str = "0#{str}" if byte < 16
15
15
  str
16
16
  end
17
17
  string_bytes.join
18
- end
19
-
18
+ end
20
19
  end
21
20
  end
data/lib/coremidi.rb CHANGED
@@ -1,23 +1,28 @@
1
- # ffi-coremidi
1
+ # frozen_string_literal: true
2
+
2
3
  #
4
+ # ffi-coremidi
3
5
  # Realtime MIDI IO with Ruby for OSX
4
- # (c)2011-2015 Ari Russo
6
+ #
7
+ # (c)2011-2022 Ari Russo
8
+ # https://github.com/arirusso/ffi-coremidi
9
+ #
5
10
 
6
11
  # Libs
7
- require "ffi"
8
- require "forwardable"
12
+ require 'ffi'
13
+ require 'forwardable'
9
14
 
10
15
  # Modules
11
- require "coremidi/api"
12
- require "coremidi/endpoint"
13
- require "coremidi/type_conversion"
16
+ require 'coremidi/api'
17
+ require 'coremidi/endpoint'
18
+ require 'coremidi/type_conversion'
14
19
 
15
20
  # Classes
16
- require "coremidi/entity"
17
- require "coremidi/device"
18
- require "coremidi/source"
19
- require "coremidi/destination"
21
+ require 'coremidi/entity'
22
+ require 'coremidi/device'
23
+ require 'coremidi/source'
24
+ require 'coremidi/destination'
20
25
 
21
26
  module CoreMIDI
22
- VERSION = "0.3.8"
27
+ VERSION = '0.5.0'
23
28
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ffi-coremidi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.8
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ari Russo
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-05-31 00:00:00.000000000 Z
11
+ date: 2022-02-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -16,100 +16,100 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '5.5'
19
+ version: '5.15'
20
20
  - - ">="
21
21
  - !ruby/object:Gem::Version
22
- version: 5.5.0
22
+ version: 5.15.0
23
23
  type: :development
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
27
  - - "~>"
28
28
  - !ruby/object:Gem::Version
29
- version: '5.5'
29
+ version: '5.15'
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 5.5.0
32
+ version: 5.15.0
33
33
  - !ruby/object:Gem::Dependency
34
- name: mocha
34
+ name: rake
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '1.1'
39
+ version: '13.0'
40
40
  - - ">="
41
41
  - !ruby/object:Gem::Version
42
- version: 1.1.0
42
+ version: 13.0.6
43
43
  type: :development
44
44
  prerelease: false
45
45
  version_requirements: !ruby/object:Gem::Requirement
46
46
  requirements:
47
47
  - - "~>"
48
48
  - !ruby/object:Gem::Version
49
- version: '1.1'
49
+ version: '13.0'
50
50
  - - ">="
51
51
  - !ruby/object:Gem::Version
52
- version: 1.1.0
52
+ version: 13.0.6
53
53
  - !ruby/object:Gem::Dependency
54
- name: rake
54
+ name: rspec
55
55
  requirement: !ruby/object:Gem::Requirement
56
56
  requirements:
57
57
  - - "~>"
58
58
  - !ruby/object:Gem::Version
59
- version: '10.4'
59
+ version: '3.11'
60
60
  - - ">="
61
61
  - !ruby/object:Gem::Version
62
- version: 10.4.2
62
+ version: 3.11.0
63
63
  type: :development
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
66
66
  requirements:
67
67
  - - "~>"
68
68
  - !ruby/object:Gem::Version
69
- version: '10.4'
69
+ version: '3.11'
70
70
  - - ">="
71
71
  - !ruby/object:Gem::Version
72
- version: 10.4.2
72
+ version: 3.11.0
73
73
  - !ruby/object:Gem::Dependency
74
- name: shoulda-context
74
+ name: rubocop
75
75
  requirement: !ruby/object:Gem::Requirement
76
76
  requirements:
77
77
  - - "~>"
78
78
  - !ruby/object:Gem::Version
79
- version: '1.2'
79
+ version: '1.25'
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
- version: 1.2.1
82
+ version: 1.25.1
83
83
  type: :development
84
84
  prerelease: false
85
85
  version_requirements: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '1.2'
89
+ version: '1.25'
90
90
  - - ">="
91
91
  - !ruby/object:Gem::Version
92
- version: 1.2.1
92
+ version: 1.25.1
93
93
  - !ruby/object:Gem::Dependency
94
94
  name: ffi
95
95
  requirement: !ruby/object:Gem::Requirement
96
96
  requirements:
97
97
  - - "~>"
98
98
  - !ruby/object:Gem::Version
99
- version: '1.9'
99
+ version: '1.15'
100
100
  - - ">="
101
101
  - !ruby/object:Gem::Version
102
- version: 1.9.6
102
+ version: 1.15.5
103
103
  type: :runtime
104
104
  prerelease: false
105
105
  version_requirements: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
- version: '1.9'
109
+ version: '1.15'
110
110
  - - ">="
111
111
  - !ruby/object:Gem::Version
112
- version: 1.9.6
112
+ version: 1.15.5
113
113
  description: Perform realtime MIDI IO with Ruby for OSX
114
114
  email:
115
115
  - ari.russo@gmail.com
@@ -127,14 +127,11 @@ files:
127
127
  - lib/coremidi/entity.rb
128
128
  - lib/coremidi/source.rb
129
129
  - lib/coremidi/type_conversion.rb
130
- - test/helper.rb
131
- - test/input_buffer_test.rb
132
- - test/io_test.rb
133
130
  homepage: http://github.com/arirusso/ffi-coremidi
134
131
  licenses:
135
- - Apache 2.0
132
+ - Apache-2.0
136
133
  metadata: {}
137
- post_install_message:
134
+ post_install_message:
138
135
  rdoc_options: []
139
136
  require_paths:
140
137
  - lib
@@ -149,9 +146,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
149
146
  - !ruby/object:Gem::Version
150
147
  version: 1.3.6
151
148
  requirements: []
152
- rubyforge_project: ffi-coremidi
153
- rubygems_version: 2.4.6
154
- signing_key:
149
+ rubygems_version: 3.3.3
150
+ signing_key:
155
151
  specification_version: 4
156
152
  summary: Realtime MIDI IO with Ruby for OSX
157
153
  test_files: []