diff --git a/Gemfile b/Gemfile index eb48132..9a14da8 100644 --- a/Gemfile +++ b/Gemfile @@ -1,10 +1,6 @@ -source "https://rubygems.org" +source 'https://rubygems.org' -group :test do - gem "minitest", "~> 5.5", ">= 5.5.0" - gem "mocha", "~> 1.1", ">= 1.1.0" - gem "rake", "~> 10.4", ">= 10.4.2" - gem "shoulda-context", "~> 1.2", ">= 1.2.1" -end - -gem "ffi", "~> 1.9", ">= 1.9.6" +gem 'ffi', '~> 1.15', '>= 1.15.5' +gem 'rake', '~> 13.0', '>= 13.0.6', groups: %i[development test] +gem 'rspec', '~> 3.11', '>= 3.11.0', groups: %i[test] +gem 'rubocop', '~> 1.25', '>= 1.25.1', groups: %i[development test] diff --git a/LICENSE b/LICENSE index 0de73b9..1bb22b1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2011-2017 Ari Russo +Copyright 2011-2022 Ari Russo Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 899da21..de97939 100644 --- a/README.md +++ b/README.md @@ -48,4 +48,4 @@ Also thank you to [Jeremy Voorhis](http://github.com/jvoorhis) for some useful d Apache 2.0, See the file LICENSE -Copyright (c) 2011-2017 [Ari Russo](http://github.com/arirusso) +Copyright (c) 2011-2022 [Ari Russo](http://github.com/arirusso) diff --git a/Rakefile b/Rakefile index 2548653..e7a3d9f 100644 --- a/Rakefile +++ b/Rakefile @@ -1,10 +1,11 @@ -require "rake" -require "rake/testtask" +# frozen_string_literal: true -Rake::TestTask.new(:test) do |t| - t.libs << "test" - t.test_files = FileList["test/**/*_test.rb"] - t.verbose = true -end +begin + require 'rspec/core/rake_task' + + RSpec::Core::RakeTask.new(:spec) -task :default => [:test] + task default: :spec +rescue LoadError + # no rspec available +end diff --git a/examples/input.rb b/examples/input.rb index ce6a9af..e0d433e 100644 --- a/examples/input.rb +++ b/examples/input.rb @@ -1,7 +1,9 @@ #!/usr/bin/env ruby -$:.unshift(File.join("..", "lib")) +# frozen_string_literal: true -require "coremidi" +$LOAD_PATH.unshift(File.join('..', 'lib')) + +require 'coremidi' # This program selects the first midi input and sends an inspection of the first 10 messages # messages it receives to standard out @@ -12,16 +14,14 @@ # or amidi -l from the command line CoreMIDI::Source.all[0].open do |input| - puts "Using input: #{input.id}, #{input.name}" - puts "send some MIDI to your input now..." + puts 'send some MIDI to your input now...' num_messages.times do m = input.gets puts(m) end - puts "finished" - + puts 'finished' end diff --git a/examples/list_endpoints.rb b/examples/list_endpoints.rb index 8167571..07f21e5 100644 --- a/examples/list_endpoints.rb +++ b/examples/list_endpoints.rb @@ -1,8 +1,10 @@ #!/usr/bin/env ruby -$:.unshift(File.join("..", "lib")) +# frozen_string_literal: true -require "coremidi" -require "pp" +$LOAD_PATH.unshift(File.join('..', 'lib')) + +require 'coremidi' +require 'pp' # This will output a big list of Endpoint objects. Endpoint objects are what's used to input # and output MIDI messages diff --git a/examples/output.rb b/examples/output.rb index 7a6f720..87959a9 100644 --- a/examples/output.rb +++ b/examples/output.rb @@ -1,7 +1,9 @@ #!/usr/bin/env ruby -$:.unshift(File.join("..", "lib")) +# frozen_string_literal: true -require "coremidi" +$LOAD_PATH.unshift(File.join('..', 'lib')) + +require 'coremidi' # This program selects the first midi output and sends some arpeggiated chords to it @@ -13,17 +15,11 @@ # or amidi -l from the command line CoreMIDI::Destination.first.open do |output| - - (0..((octaves-1)*12)).step(12) do |oct| - + (0..((octaves - 1) * 12)).step(12) do |oct| notes.each do |note| - - output.puts(0x90, note + oct, 100) # note on - sleep(duration) # wait - output.puts(0x80, note + oct, 100) # note off - + output.puts(0x90, note + oct, 100) # NOTE: on + sleep(duration) # wait + output.puts(0x80, note + oct, 100) # NOTE: off end - end - end diff --git a/examples/sysex_output.rb b/examples/sysex_output.rb index 1ae96af..5725818 100644 --- a/examples/sysex_output.rb +++ b/examples/sysex_output.rb @@ -1,7 +1,9 @@ #!/usr/bin/env ruby -$:.unshift(File.join("..", "lib")) +# frozen_string_literal: true -require "coremidi" +$LOAD_PATH.unshift(File.join('..', 'lib')) + +require 'coremidi' # This example outputs a raw sysex message to the first Output endpoint # there will not be any output to the console diff --git a/lib/coremidi.rb b/lib/coremidi.rb index aa2aab5..3348558 100644 --- a/lib/coremidi.rb +++ b/lib/coremidi.rb @@ -1,26 +1,28 @@ +# frozen_string_literal: true + # # ffi-coremidi # Realtime MIDI IO with Ruby for OSX # -# (c)2011-2017 Ari Russo +# (c)2011-2022 Ari Russo # https://github.com/arirusso/ffi-coremidi # # Libs -require "ffi" -require "forwardable" +require 'ffi' +require 'forwardable' # Modules -require "coremidi/api" -require "coremidi/endpoint" -require "coremidi/type_conversion" +require 'coremidi/api' +require 'coremidi/endpoint' +require 'coremidi/type_conversion' # Classes -require "coremidi/entity" -require "coremidi/device" -require "coremidi/source" -require "coremidi/destination" +require 'coremidi/entity' +require 'coremidi/device' +require 'coremidi/source' +require 'coremidi/destination' module CoreMIDI - VERSION = "0.4.1" + VERSION = '0.5.0' end diff --git a/lib/coremidi/api.rb b/lib/coremidi/api.rb index 44f1ad8..9c7271e 100644 --- a/lib/coremidi/api.rb +++ b/lib/coremidi/api.rb @@ -1,8 +1,8 @@ -module CoreMIDI +# frozen_string_literal: true +module CoreMIDI # Coremidi C binding module API - extend FFI::Library ffi_lib '/System/Library/Frameworks/CoreMIDI.framework/Versions/Current/CoreMIDI' @@ -17,12 +17,11 @@ module API typedef :pointer, :MIDIEntityRef typedef :pointer, :MIDIObjectRef typedef :pointer, :MIDIPortRef - #typedef :pointer, :MIDIReadProc + # typedef :pointer, :MIDIReadProc typedef :uint32, :MIDITimeStamp typedef :int32, :OSStatus class MIDISysexSendRequest < FFI::Struct - layout :destination, :MIDIEndpointRef, :data, :pointer, :bytes_to_send, :uint32, @@ -33,18 +32,15 @@ class MIDISysexSendRequest < FFI::Struct end class MIDIPacket < FFI::Struct - layout :timestamp, :MIDITimeStamp, :nothing, :uint32, # no idea... :length, :uint16, :data, [:uint8, 256] - end class MIDIPacketList < FFI::Struct layout :numPackets, :uint32, :packet, [MIDIPacket.by_value, 1] - end def self.get_callback(*args, &block) @@ -53,7 +49,7 @@ def self.get_callback(*args, &block) # Pack the given data into a coremidi MIDI packet (used by Destination) def self.get_midi_packet(data) - format = "C" * data.size + format = 'C' * data.size packed_data = data.pack(format) char_size = FFI.type_size(:char) * data.size bytes = FFI::MemoryPointer.new(char_size) @@ -67,8 +63,8 @@ def self.create_midi_client(resource_id, name) error = API.MIDIClientCreate(client_name, nil, nil, client_pointer) client = client_pointer.read_pointer { - :error => error, - :resource => client + error: error, + resource: client } end @@ -78,8 +74,8 @@ def self.create_midi_input_port(client, resource_id, name, callback) error = API.MIDIInputPortCreate(client, port_name, callback, nil, handle_ptr) handle = handle_ptr.read_pointer { - :error => error, - :handle => handle + error: error, + handle: handle } end @@ -89,8 +85,8 @@ def self.create_midi_output_port(client, resource_id, name) error = API.MIDIOutputPortCreate(client, port_name, port_pointer) handle = port_pointer.read_pointer { - :error => error, - :handle => handle + error: error, + handle: handle } end @@ -100,11 +96,12 @@ def self.get_midi_packet_list(bytes, size) packet_ptr = API.MIDIPacketListInit(packet_list) time = HostTime.AudioGetCurrentHostTime packet_ptr = if X86_64 - API.MIDIPacketListAdd(packet_list, 256, packet_ptr, time, size, bytes) - else - # Pass in two 32-bit 0s for the 64 bit time - time1 = API.MIDIPacketListAdd(packet_list, 256, packet_ptr, time >> 32, time & 0xFFFFFFFF, size, bytes) - end + API.MIDIPacketListAdd(packet_list, 256, packet_ptr, time, size, bytes) + else + # Pass in two 32-bit 0s for the 64 bit time + time1 = API.MIDIPacketListAdd(packet_list, 256, packet_ptr, time >> 32, time & 0xFFFFFFFF, size, + bytes) + end packet_list end @@ -132,7 +129,7 @@ def self.get_string(resource, name) bytes = FFI::MemoryPointer.new(length + 1) if CF.CFStringGetCString(string, bytes, length + 1, :kCFStringEncodingUTF8) - bytes.read_string.force_encoding("utf-8") + bytes.read_string.force_encoding('utf-8') end ensure CF.CFRelease(string) unless string.nil? || string.null? @@ -145,13 +142,13 @@ def self.get_string(resource, name) callback :MIDIReadProc, [MIDIPacketList.by_ref, :pointer, :pointer], :pointer # OSStatus MIDIClientCreate(CFStringRef name, MIDINotifyProc notifyProc, void *notifyRefCon, MIDIClientRef *outClient); - attach_function :MIDIClientCreate, [:pointer, :pointer, :pointer, :pointer], :int + attach_function :MIDIClientCreate, %i[pointer pointer pointer pointer], :int # OSStatus MIDIClientDispose(MIDIClientRef client); attach_function :MIDIClientDispose, [:pointer], :int # MIDIEntityRef MIDIDeviceGetEntity(MIDIDeviceRef device, ItemCount entityIndex0); - attach_function :MIDIDeviceGetEntity, [:MIDIDeviceRef, :ItemCount], :MIDIEntityRef + attach_function :MIDIDeviceGetEntity, %i[MIDIDeviceRef ItemCount], :MIDIEntityRef # MIDIEndpointRef MIDIGetDestination(ItemCount destIndex0); attach_function :MIDIGetNumberOfDestinations, [], :ItemCount @@ -162,11 +159,11 @@ def self.get_string(resource, name) # MIDIEndpointRef MIDIEntityGetDestination(MIDIEntityRef entity, ItemCount destIndex0); attach_function :MIDIGetDestination, [:int], :pointer - #extern OSStatus MIDIEndpointDispose( MIDIEndpointRef endpt ); + # extern OSStatus MIDIEndpointDispose( MIDIEndpointRef endpt ); attach_function :MIDIEndpointDispose, [:MIDIEndpointRef], :OSStatus # MIDIEndpointRef MIDIEntityGetDestination( MIDIEntityRef entity, ItemCount destIndex0 ); - attach_function :MIDIEntityGetDestination, [:MIDIEntityRef, :int], :MIDIEndpointRef + attach_function :MIDIEntityGetDestination, %i[MIDIEntityRef int], :MIDIEndpointRef # ItemCount MIDIEntityGetNumberOfDestinations (MIDIEntityRef entity); attach_function :MIDIEntityGetNumberOfDestinations, [:MIDIEntityRef], :ItemCount @@ -175,77 +172,76 @@ def self.get_string(resource, name) attach_function :MIDIEntityGetNumberOfSources, [:MIDIEntityRef], :ItemCount # MIDIEndpointRef MIDIEntityGetSource (MIDIEntityRef entity, ItemCount sourceIndex0); - attach_function :MIDIEntityGetSource, [:MIDIEntityRef, :ItemCount], :MIDIEndpointRef + attach_function :MIDIEntityGetSource, %i[MIDIEntityRef ItemCount], :MIDIEndpointRef # MIDIDeviceRef MIDIGetDevice(ItemCount deviceIndex0); attach_function :MIDIGetDevice, [:ItemCount], :MIDIDeviceRef # extern OSStatus MIDIInputPortCreate( MIDIClientRef client, CFStringRef portName, # MIDIReadProc readProc, void * refCon, MIDIPortRef * outPort ); - attach_function :MIDIInputPortCreate, [:MIDIClientRef, :CFStringRef, :MIDIReadProc, :pointer, :MIDIPortRef], :OSStatus + attach_function :MIDIInputPortCreate, %i[MIDIClientRef CFStringRef MIDIReadProc pointer MIDIPortRef], + :OSStatus # extern OSStatus MIDIObjectGetIntegerProperty( MIDIObjectRef obj, CFStringRef propertyID, SInt32 * outValue ); - attach_function :MIDIObjectGetIntegerProperty, [:MIDIObjectRef, :CFStringRef, :pointer], :OSStatus + attach_function :MIDIObjectGetIntegerProperty, %i[MIDIObjectRef CFStringRef pointer], :OSStatus # OSStatus MIDIObjectGetStringProperty (MIDIObjectRef obj, CFStringRef propertyID, CFStringRef *str); - attach_function :MIDIObjectGetStringProperty, [:MIDIObjectRef, :CFStringRef, :pointer], :OSStatus + attach_function :MIDIObjectGetStringProperty, %i[MIDIObjectRef CFStringRef pointer], :OSStatus # extern OSStatus MIDIOutputPortCreate( MIDIClientRef client, CFStringRef portName, MIDIPortRef * outPort ); - attach_function :MIDIOutputPortCreate, [:MIDIClientRef, :CFStringRef, :pointer], :int + attach_function :MIDIOutputPortCreate, %i[MIDIClientRef CFStringRef pointer], :int # (MIDIPacket*) MIDIPacketListInit(MIDIPacketList *pktlist); attach_function :MIDIPacketListInit, [:pointer], :pointer - #extern OSStatus MIDIPortConnectSource( MIDIPortRef port, MIDIEndpointRef source, void * connRefCon ) - attach_function :MIDIPortConnectSource, [:MIDIPortRef, :MIDIEndpointRef, :pointer], :OSStatus + # extern OSStatus MIDIPortConnectSource( MIDIPortRef port, MIDIEndpointRef source, void * connRefCon ) + attach_function :MIDIPortConnectSource, %i[MIDIPortRef MIDIEndpointRef pointer], :OSStatus - #extern OSStatus MIDIPortDisconnectSource( MIDIPortRef port, MIDIEndpointRef source ); - attach_function :MIDIPortDisconnectSource, [:MIDIPortRef, :MIDIEndpointRef], :OSStatus + # extern OSStatus MIDIPortDisconnectSource( MIDIPortRef port, MIDIEndpointRef source ); + attach_function :MIDIPortDisconnectSource, %i[MIDIPortRef MIDIEndpointRef], :OSStatus - #extern OSStatus MIDIPortDispose(MIDIPortRef port ); + # extern OSStatus MIDIPortDispose(MIDIPortRef port ); attach_function :MIDIPortDispose, [:MIDIPortRef], :OSStatus - #extern OSStatus MIDISend(MIDIPortRef port,MIDIEndpointRef dest,const MIDIPacketList *pktlist); - attach_function :MIDISend, [:MIDIPortRef, :MIDIEndpointRef, :pointer], :int + # extern OSStatus MIDISend(MIDIPortRef port,MIDIEndpointRef dest,const MIDIPacketList *pktlist); + attach_function :MIDISend, %i[MIDIPortRef MIDIEndpointRef pointer], :int - #OSStatus MIDISendSysex(MIDISysexSendRequest *request); + # OSStatus MIDISendSysex(MIDISysexSendRequest *request); attach_function :MIDISendSysex, [:pointer], :int # extern MIDIPacket * MIDIPacketListAdd( MIDIPacketList * pktlist, ByteCount listSize, # MIDIPacket * curPacket, MIDITimeStamp time, # ByteCount nData, const Byte * data) if X86_64 - attach_function :MIDIPacketListAdd, [:pointer, :int, :pointer, :uint64, :int, :pointer], :pointer + attach_function :MIDIPacketListAdd, %i[pointer int pointer uint64 int pointer], :pointer else - attach_function :MIDIPacketListAdd, [:pointer, :int, :pointer, :int, :int, :int, :pointer], :pointer + attach_function :MIDIPacketListAdd, %i[pointer int pointer int int int pointer], :pointer end module CF - extend FFI::Library ffi_lib '/System/Library/Frameworks/CoreFoundation.framework/Versions/Current/CoreFoundation' typedef :pointer, :CFStringRef typedef :long, :CFIndex - enum :CFStringEncoding, [ :kCFStringEncodingUTF8, 0x08000100 ] + enum :CFStringEncoding, [:kCFStringEncodingUTF8, 0x08000100] # CFString* CFStringCreateWithCString( ?, CString, encoding) - attach_function :CFStringCreateWithCString, [:pointer, :string, :int], :pointer + attach_function :CFStringCreateWithCString, %i[pointer string int], :pointer # CString* CFStringGetCStringPtr(CFString*, encoding) - attach_function :CFStringGetCStringPtr, [:pointer, :int], :pointer + attach_function :CFStringGetCStringPtr, %i[pointer int], :pointer # CFIndex CFStringGetLength(CFStringRef theString); - attach_function :CFStringGetLength, [ :CFStringRef ], :CFIndex + attach_function :CFStringGetLength, [:CFStringRef], :CFIndex # CFIndex CFStringGetMaximumSizeForEncoding(CFIndex length, CFStringEncoding encoding); - attach_function :CFStringGetMaximumSizeForEncoding, [ :CFIndex, :CFStringEncoding ], :long + attach_function :CFStringGetMaximumSizeForEncoding, %i[CFIndex CFStringEncoding], :long # Boolean CFStringGetCString(CFStringRef theString, char *buffer, CFIndex bufferSize, CFStringEncoding encoding); - attach_function :CFStringGetCString, [ :CFStringRef, :pointer, :CFIndex, :CFStringEncoding ], :bool + attach_function :CFStringGetCString, %i[CFStringRef pointer CFIndex CFStringEncoding], :bool # void CFRelease (CFTypeRef cf); - attach_function :CFRelease, [ :pointer ], :void - + attach_function :CFRelease, [:pointer], :void end module HostTime @@ -257,7 +253,5 @@ module HostTime # UInt64 AudioGetCurrentHostTime() attach_function :AudioGetCurrentHostTime, [], :uint64 end - end - end diff --git a/lib/coremidi/destination.rb b/lib/coremidi/destination.rb index 9d88aba..df6d1c3 100644 --- a/lib/coremidi/destination.rb +++ b/lib/coremidi/destination.rb @@ -1,8 +1,8 @@ -module CoreMIDI +# frozen_string_literal: true +module CoreMIDI # Type of endpoint used for output class Destination - include Endpoint attr_reader :entity @@ -24,14 +24,14 @@ def close def puts_s(data) data = data.dup bytes = [] - until (str = data.slice!(0,2)).eql?("") + until (str = data.slice!(0, 2)).eql?('') bytes << str.hex end puts_bytes(*bytes) true end - alias_method :puts_bytestr, :puts_s - alias_method :puts_hex, :puts_s + alias puts_bytestr puts_s + alias puts_hex puts_s # Send a MIDI message comprised of numeric bytes # @param [*Integer] data Numeric bytes eg 0x90, 0x40, 0x40 @@ -39,7 +39,7 @@ def puts_s(data) def puts_bytes(*data) type = sysex?(data) ? :sysex : :small bytes = API.get_midi_packet(data) - send("puts_#{type.to_s}", bytes, data.size) + send("puts_#{type}", bytes, data.size) true end @@ -53,12 +53,12 @@ def puts(*args) when String then puts_bytestr(*args) end end - alias_method :write, :puts + alias write puts # Enable this device # @return [Destination] - def enable(options = {}, &block) - @enabled = true unless @enabled + def enable(_options = {}) + @enabled ||= true if block_given? begin yield(self) @@ -68,8 +68,8 @@ def enable(options = {}, &block) end self end - alias_method :open, :enable - alias_method :start, :enable + alias open enable + alias start enable # Shortcut to the first output endpoint available # @return [Destination] @@ -97,10 +97,10 @@ def self.all def connect client_error = enable_client port_error = initialize_port - @resource = API.MIDIEntityGetDestination( @entity.resource, @resource_id ) + @resource = API.MIDIEntityGetDestination(@entity.resource, @resource_id) !@resource.address.zero? && client_error.zero? && port_error.zero? end - alias_method :connect?, :connect + alias connect? connect private @@ -126,7 +126,7 @@ def puts_sysex(bytes, size) SysexCompletionCallback = API.get_callback([:pointer]) do |sysex_request_ptr| - # this isn't working for some reason. as of now, it's not needed though + # this isn't working for some reason. as of now, it's not needed though end # Initialize a coremidi port for this endpoint @@ -140,7 +140,5 @@ def initialize_port def sysex?(data) data.first.eql?(0xF0) && data.last.eql?(0xF7) end - end - end diff --git a/lib/coremidi/device.rb b/lib/coremidi/device.rb index 1f577c8..3f7d61c 100644 --- a/lib/coremidi/device.rb +++ b/lib/coremidi/device.rb @@ -1,12 +1,12 @@ -module CoreMIDI +# frozen_string_literal: true +module CoreMIDI # A MIDI device may have multiple logically distinct sub-components. For example, one device may # encompass a MIDI synthesizer and a pair of MIDI ports, both addressable via a USB port. Each # such element of a device is called a MIDI entity. # # https://developer.apple.com/library/ios/documentation/CoreMidi/Reference/MIDIServices_Reference/Reference/reference.html class Device - attr_reader :entities, :id, # Unique Numeric id :name # Device name from coremidi @@ -25,8 +25,8 @@ def initialize(id, device_pointer, options = {}) # Endpoints for this device # @return [Array] def endpoints - endpoints = { :source => [], :destination => [] } - endpoints.keys.each do |key| + endpoints = { source: [], destination: [] } + endpoints.each_key do |key| endpoint_group = entities.map { |entity| entity.endpoints[key] }.flatten endpoints[key] += endpoint_group end @@ -53,8 +53,8 @@ def self.all(options = {}) if !populated? || !use_cache @devices = [] counter = 0 - while !(device_pointer = API.MIDIGetDevice(counter)).null? - device = new(counter, device_pointer, :include_offline => include_offline) + until (device_pointer = API.MIDIGetDevice(counter)).null? + device = new(counter, device_pointer, include_offline: include_offline) @devices << device counter += 1 end @@ -79,8 +79,8 @@ def self.populated? # Populate the device name def populate_name - @name = API.get_string(@resource, "name") - raise RuntimeError.new("Can't get device name") unless @name + @name = API.get_string(@resource, 'name') + raise "Can't get device name" unless @name end # All of the endpoints for all devices a consecutive local id @@ -97,8 +97,8 @@ def self.populate_endpoint_ids def populate_entities(options = {}) include_if_offline = options[:include_offline] || false i = 0 - while !(entity_pointer = API.MIDIDeviceGetEntity(@resource, i)).null? - @entities << Entity.new(entity_pointer, :include_offline => include_if_offline) + until (entity_pointer = API.MIDIDeviceGetEntity(@resource, i)).null? + @entities << Entity.new(entity_pointer, include_offline: include_if_offline) i += 1 end i @@ -107,9 +107,7 @@ def populate_entities(options = {}) # Populate the instance def populate(options = {}) populate_name - populate_entities(:include_offline => options[:include_offline]) + populate_entities(include_offline: options[:include_offline]) end - end - end diff --git a/lib/coremidi/endpoint.rb b/lib/coremidi/endpoint.rb index 98f0ce8..d2600e8 100644 --- a/lib/coremidi/endpoint.rb +++ b/lib/coremidi/endpoint.rb @@ -1,10 +1,10 @@ -module CoreMIDI +# frozen_string_literal: true +module CoreMIDI # A source or destination of a 16-channel MIDI stream # # https://developer.apple.com/library/ios/documentation/CoreMidi/Reference/MIDIServices_Reference/Reference/reference.html module Endpoint - extend Forwardable attr_reader :enabled, # has the endpoint been initialized? @@ -15,7 +15,7 @@ module Endpoint def_delegators :entity, :manufacturer, :model, :name - alias_method :enabled?, :enabled + alias enabled? enabled # @param [Integer] resource_id # @param [Entity] entity @@ -24,6 +24,9 @@ def initialize(resource_id, entity) @resource_id = resource_id @type = get_type @enabled = false + + @threads_sync_semaphore = Mutex.new + @threads_waiting = [] end # Is this endpoint online? @@ -102,7 +105,5 @@ def enable_client @client = client[:resource] client[:error] end - end - end diff --git a/lib/coremidi/entity.rb b/lib/coremidi/entity.rb index d0bd39c..09c3546 100644 --- a/lib/coremidi/entity.rb +++ b/lib/coremidi/entity.rb @@ -1,5 +1,6 @@ -module CoreMIDI +# frozen_string_literal: true +module CoreMIDI # A MIDI entity can have any number of MIDI endpoints, each of which is a source or destination # of a 16-channel MIDI stream. By grouping a device's endpoints into entities, the system has # enough information for an application to make reasonable default assumptions about how to @@ -8,7 +9,6 @@ module CoreMIDI # # https://developer.apple.com/library/ios/documentation/CoreMidi/Reference/MIDIServices_Reference/Reference/reference.html class Entity - attr_reader :endpoints, :manufacturer, :model, @@ -31,18 +31,13 @@ def initialize(resource, options = {}) # @param [Integer] starting_id # @return [Integer] def populate_endpoint_ids(starting_id) - counter = 0 - @endpoints.values.flatten.each do |endpoint| - endpoint.id = counter + starting_id - counter += 1 - end - counter + @endpoints.values.flatten.map.with_index { |endpoint, index| endpoint.id = index + starting_id }.length end # Is the entity online? # @return [Boolean] def online? - get_int(:offline) == 0 + get_int(:offline).zero? end private @@ -63,9 +58,8 @@ def populate_endpoints_by_type(type, options = {}) num_endpoints = number_of_endpoints(type) (0..num_endpoints).each do |i| endpoint = endpoint_class.new(i, self) - if endpoint.online? || options[:include_offline] - @endpoints[type] << endpoint - end + should_include_message = endpoint.online? || options[:include_offline] + @endpoints[type] << endpoint if should_include_message end @endpoints[type].size end @@ -82,8 +76,8 @@ def populate_endpoints(options = {}) # @param [Symbol] type The endpoint type eg :source, :destination def number_of_endpoints(type) case type - when :source then API.MIDIEntityGetNumberOfSources(@resource) - when :destination then API.MIDIEntityGetNumberOfDestinations(@resource) + when :source then API.MIDIEntityGetNumberOfSources(@resource) + when :destination then API.MIDIEntityGetNumberOfDestinations(@resource) end end @@ -110,7 +104,5 @@ def populate(options = {}) @name = get_name populate_endpoints(options) end - end - end diff --git a/lib/coremidi/source.rb b/lib/coremidi/source.rb index 4c5234d..b708fd7 100644 --- a/lib/coremidi/source.rb +++ b/lib/coremidi/source.rb @@ -1,8 +1,8 @@ -module CoreMIDI +# frozen_string_literal: true +module CoreMIDI # Type of endpoint used for input class Source - include Endpoint # The buffer of received messages since instantiation @@ -25,9 +25,9 @@ def buffer # # @return [Array] def gets - fill_buffer + fill_buffer(locking: true) end - alias_method :read, :gets + alias read gets # Same as Source#gets except that it returns message data as string of hex # digits as such: @@ -45,12 +45,12 @@ def gets_s end messages end - alias_method :gets_bytestr, :gets_s + alias gets_bytestr gets_s # Enable this the input for use; can be passed a block # @return [Source] - def enable(options = {}, &block) - @enabled = true unless @enabled + def enable(_options = {}) + @enabled ||= true if block_given? begin yield(self) @@ -60,20 +60,20 @@ def enable(options = {}, &block) end self end - alias_method :open, :enable - alias_method :start, :enable + alias open enable + alias start enable # Close this input # @return [Boolean] def close - #error = API.MIDIPortDisconnectSource( @handle, @resource ) - #raise "MIDIPortDisconnectSource returned error code #{error}" unless error.zero? - #error = API.MIDIClientDispose(@handle) - #raise "MIDIClientDispose returned error code #{error}" unless error.zero? - #error = API.MIDIPortDispose(@handle) - #raise "MIDIPortDispose returned error code #{error}" unless error.zero? - #error = API.MIDIEndpointDispose(@resource) - #raise "MIDIEndpointDispose returned error code #{error}" unless error.zero? + # error = API.MIDIPortDisconnectSource( @handle, @resource ) + # raise "MIDIPortDisconnectSource returned error code #{error}" unless error.zero? + # error = API.MIDIClientDispose(@handle) + # raise "MIDIClientDispose returned error code #{error}" unless error.zero? + # error = API.MIDIPortDispose(@handle) + # raise "MIDIPortDispose returned error code #{error}" unless error.zero? + # error = API.MIDIEndpointDispose(@resource) + # raise "MIDIEndpointDispose returned error code #{error}" unless error.zero? if @enabled @enabled = false true @@ -102,14 +102,27 @@ def self.all protected + def truncate_buffer + @buffer.slice!(-1024, 1024) + end + # Migrate new received messages from the callback queue to # the buffer - def fill_buffer + def fill_buffer(locking: nil) + locking ||= false + messages = [] - until @queue.empty? - messages << @queue.pop + + if locking && @queue.empty? + @threads_sync_semaphore.synchronize do + @threads_waiting << Thread.current + end + sleep end + + messages << @queue.pop until @queue.empty? @buffer += messages + truncate_buffer @pointer = @buffer.length messages end @@ -120,14 +133,14 @@ def connect enable_client initialize_port @resource = API.MIDIEntityGetSource(@entity.resource, @resource_id) - error = API.MIDIPortConnectSource(@handle, @resource, nil ) + error = API.MIDIPortConnectSource(@handle, @resource, nil) initialize_buffer @queue = Queue.new @sysex_buffer = [] error.zero? end - alias_method :connect?, :connect + alias connect? connect private @@ -145,23 +158,27 @@ def enqueue_message(bytes, timestamp) end message = get_message_formatted(bytes, timestamp) @queue << message + + @threads_sync_semaphore.synchronize do + @threads_waiting.each(&:run) + @threads_waiting.clear + end + message end # The callback fired by coremidi when new MIDI messages are received def get_event_callback Thread.abort_on_exception = true - Proc.new do |new_packets, refCon_ptr, connRefCon_ptr| - begin - # p "packets received: #{new_packets[:numPackets]}" - timestamp = Time.now.to_f - messages = get_messages(new_packets) - messages.each do |message| - enqueue_message(message, timestamp) - end - rescue Exception => exception - Thread.main.raise(exception) + proc do |new_packets, _refCon_ptr, _connRefCon_ptr| + # p "packets received: #{new_packets[:numPackets]}" + timestamp = Time.now.to_f + messages = get_messages(new_packets) + messages.each do |message| + enqueue_message(message, timestamp) end + rescue Exception => e + Thread.main.raise(e) end end @@ -174,18 +191,18 @@ def get_messages(packet_list) data = first[:data].to_a messages = [] messages << data.slice!(0, first[:length]) - (count - 1).times do |i| + (count - 1).times do |_i| length_index = find_next_length_index(data) message_length = data[length_index] - unless message_length.nil? - packet_start_index = length_index + 2 - packet_end_index = packet_start_index + message_length - if data.length >= packet_end_index + 1 - packet = data.slice!(0..packet_end_index) - message = packet.slice(packet_start_index, message_length) - messages << message - end - end + next if message_length.nil? + + packet_start_index = length_index + 2 + packet_end_index = packet_start_index + message_length + next unless data.length >= packet_end_index + 1 + + packet = data.slice!(0..packet_end_index) + message = packet.slice(packet_start_index, message_length) + messages << message end messages end @@ -224,6 +241,7 @@ def initialize_port port = API.create_midi_input_port(@client, @resource_id, @name, @callback) @handle = port[:handle] raise "MIDIInputPortCreate returned error code #{port[:error]}" unless port[:error].zero? + true end @@ -238,7 +256,5 @@ def @buffer.clear end true end - end - end diff --git a/lib/coremidi/type_conversion.rb b/lib/coremidi/type_conversion.rb index ddb5a34..ac07e1a 100644 --- a/lib/coremidi/type_conversion.rb +++ b/lib/coremidi/type_conversion.rb @@ -1,9 +1,9 @@ -module CoreMIDI +# frozen_string_literal: true - # Helper for convertig MIDI data +module CoreMIDI + # Helper for converting MIDI data module TypeConversion - - extend self + module_function # Convert an array of numeric byes to a hex string (e.g. [0x90, 0x40, 0x40] becomes "904040") # @param [Array] bytes @@ -11,11 +11,10 @@ module TypeConversion def numeric_bytes_to_hex_string(bytes) string_bytes = bytes.map do |byte| str = byte.to_s(16).upcase - str = "0" + str if byte < 16 + str = "0#{str}" if byte < 16 str end string_bytes.join - end - + end end end diff --git a/spec/helper.rb b/spec/helper.rb new file mode 100644 index 0000000..a88a136 --- /dev/null +++ b/spec/helper.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +dir = File.dirname(File.expand_path(__FILE__)) +$LOAD_PATH.unshift "#{dir}/../lib" + +require 'rspec' +require 'coremidi' + +module SpecHelper + module_function + + def device + @device ||= select_devices + end + + def select_devices + @device ||= {} + { input: CoreMIDI::Source.all, output: CoreMIDI::Destination.all }.each do |type, devs| + puts '' + puts "select an #{type}..." + while @device[type].nil? + devs.each do |device| + puts "#{device.id}: #{device.name}" + end + selection = $stdin.gets.chomp + next unless selection != '' + + selection = selection.to_i + @device[type] = devs.find { |d| d.id == selection } + puts "selected #{selection} for #{type}" unless @device[type] + end + end + @device + end + + def bytestrs_to_ints(arr) + data = arr.map { |m| m[:data] }.join + output = [] + until (bytestr = data.slice!(0, 2)).eql?('') + output << bytestr.hex + end + output + end + + # some MIDI messages + MIDI_MESSAGES = [ + [0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7], # SysEx + [0x90, 100, 100], # NOTE: on + [0x90, 43, 100], # NOTE: on + [0x90, 76, 100], # NOTE: on + [0x90, 60, 100], # NOTE: on + [0x80, 100, 100] # NOTE: off + ].freeze + + # some MIDI messages + MIDI_MESSAGE_STRINGS = [ + 'F04110421240007F0041F7', # SysEx + '906440', # NOTE: on + '804340' # NOTE: off + ].freeze +end + +SpecHelper.select_devices diff --git a/spec/input_buffer_spec.rb b/spec/input_buffer_spec.rb new file mode 100644 index 0000000..5be1e52 --- /dev/null +++ b/spec/input_buffer_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'helper' + +describe 'InputBuffer' do + # these tests assume that the test output is connected to the test input + before { sleep(0.1) } + + describe 'Source#buffer' do + let(:output) { SpecHelper.device[:output].open } + let(:input) { SpecHelper.device[:input].open } + before { input.buffer.clear } + + it 'has the correct messages in the buffer' do + sent_bytes = [] + SpecHelper::MIDI_MESSAGES.each do |message| + puts "sending: #{message.inspect}" + output.puts(message) + sent_bytes += message + + sleep 0.1 + + buffer = input.buffer.map { |m| m[:data] }.flatten + puts "received: #{buffer}" + expect(buffer).to eq(sent_bytes) + end + end + end +end diff --git a/spec/io_spec.rb b/spec/io_spec.rb new file mode 100644 index 0000000..2166a68 --- /dev/null +++ b/spec/io_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'helper' + +describe 'IOTest' do + # these tests assume that the test output is connected to the test input + before { sleep 0.1 } + + describe 'full IO' do + let(:input) { SpecHelper.device[:input].open } + let(:output) { SpecHelper.device[:output].open } + let(:number_of_bytes_expected) { SpecHelper::MIDI_MESSAGES.inject { |a, b| a + b }.flatten.length } + + before { input.buffer.clear } + + describe 'using Arrays' do + it 'does IO' do + result = SpecHelper::MIDI_MESSAGES.map do |message| + puts "sending: #{message.inspect}" + + output.puts(message) + sleep 0.1 + + data_from_device = input.gets.map { |m| m[:data] }.flatten + puts "received: #{data_from_device.inspect}" + + expect(data_from_device).to eq(message) + data_from_device + end + + expect(result).to eq(SpecHelper::MIDI_MESSAGES) + end + end + + describe 'using byte Strings' do + it 'does IO' do + result = SpecHelper::MIDI_MESSAGE_STRINGS.map do |message| + puts "sending: #{message.inspect}" + + output.puts(message) + sleep 1 + data_from_device = input.gets_bytestr.map { |m| m[:data] }.flatten.join + puts "received: #{data_from_device.inspect}" + + expect(data_from_device).to eq(message) + data_from_device + end + + expect(result).to eq(SpecHelper::MIDI_MESSAGE_STRINGS) + end + end + end +end diff --git a/test/helper.rb b/test/helper.rb deleted file mode 100644 index 6e525fb..0000000 --- a/test/helper.rb +++ /dev/null @@ -1,66 +0,0 @@ -dir = File.dirname(File.expand_path(__FILE__)) -$LOAD_PATH.unshift dir + "/../lib" - -require "minitest/autorun" -require "mocha/test_unit" -require "shoulda-context" - -require "coremidi" - -module TestHelper - - extend self - - def device - @device ||= select_devices - end - - def select_devices - @device ||= {} - { :input => CoreMIDI::Source.all, :output => CoreMIDI::Destination.all }.each do |type, devs| - puts "" - puts "select an #{type.to_s}..." - while @device[type].nil? - devs.each do |device| - puts "#{device.id}: #{device.name}" - end - selection = $stdin.gets.chomp - if selection != "" - selection = selection.to_i - @device[type] = devs.find { |d| d.id == selection } - puts "selected #{selection} for #{type.to_s}" unless @device[type] - end - end - end - @device - end - - def bytestrs_to_ints(arr) - data = arr.map { |m| m[:data] }.join - output = [] - until (bytestr = data.slice!(0,2)).eql?("") - output << bytestr.hex - end - output - end - - # some MIDI messages - VariousMIDIMessages = [ - [0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7], # SysEx - [0x90, 100, 100], # note on - [0x90, 43, 100], # note on - [0x90, 76, 100], # note on - [0x90, 60, 100], # note on - [0x80, 100, 100] # note off - ] - - # some MIDI messages - VariousMIDIByteStrMessages = [ - "F04110421240007F0041F7", # SysEx - "906440", # note on - "804340" # note off - ] - -end - -TestHelper.select_devices diff --git a/test/input_buffer_test.rb b/test/input_buffer_test.rb deleted file mode 100644 index ba059d5..0000000 --- a/test/input_buffer_test.rb +++ /dev/null @@ -1,42 +0,0 @@ -require "helper" - -class InputBufferTest < Minitest::Test - - context "CoreMIDI" do - - setup do - sleep(1) - end - - context "Source#buffer" do - - setup do - @messages = TestHelper::VariousMIDIMessages - @messages_arr = @messages.inject { |a,b| a+b }.flatten - @received_arr = [] - @pointer = 0 - - @output = TestHelper.device[:output].open - @input = TestHelper.device[:input].open - @input.buffer.clear - end - - should "have the correct messages in the buffer" do - bytes = [] - @messages.each do |message| - puts "sending: #{message.inspect}" - @output.puts(message) - bytes += message - - sleep(0.5) - - buffer = @input.buffer.map { |m| m[:data] }.flatten - puts "received: #{buffer.to_s}" - assert_equal(bytes, buffer) - end - assert_equal(bytes.length, @input.buffer.map { |m| m[:data] }.flatten.length) - end - end - - end -end diff --git a/test/io_test.rb b/test/io_test.rb deleted file mode 100644 index 1983341..0000000 --- a/test/io_test.rb +++ /dev/null @@ -1,90 +0,0 @@ -require "helper" - -class CoreMIDI::IOTest < Minitest::Test - - # ** these tests assume that TestOutput is connected to TestInput - context "CoreMIDI" do - - setup do - sleep(1) - end - - context "full IO" do - - context "using Arrays" do - - setup do - @messages = TestHelper::VariousMIDIMessages - @messages_arr = @messages.inject { |a,b| a+b }.flatten - @received_arr = [] - @pointer = 0 - end - - should "do IO" do - TestHelper.device[:output].open do |output| - TestHelper.device[:input].open do |input| - - input.buffer.clear - - @messages.each do |msg| - - $>.puts "sending: " + msg.inspect - - output.puts(msg) - sleep(1) - received = input.gets.map { |m| m[:data] }.flatten - - $>.puts "received: " + received.inspect - - assert_equal(@messages_arr.slice(@pointer, received.length), received) - @pointer += received.length - @received_arr += received - end - assert_equal(@messages_arr.length, @received_arr.length) - end - end - - end - end - - context "using byte Strings" do - - setup do - @messages = TestHelper::VariousMIDIByteStrMessages - @messages_str = @messages.join - @received_str = "" - @pointer = 0 - end - - should "do IO" do - TestHelper.device[:output].open do |output| - TestHelper.device[:input].open do |input| - - @messages.each do |msg| - - $>.puts "sending: " + msg.inspect - - output.puts(msg) - sleep(1) - received = input.gets_bytestr.map { |m| m[:data] }.flatten.join - $>.puts "received: " + received.inspect - - assert_equal(@messages_str.slice(@pointer, received.length), received) - @pointer += received.length - @received_str += received - end - assert_equal(@messages_str, @received_str) - - end - end - - - end - - end - - end - - end - -end