ffi-coremidi 0.3.8 → 0.5.0

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