diff --git a/Gemfile b/Gemfile index 740fca4..fea51a1 100644 --- a/Gemfile +++ b/Gemfile @@ -1,9 +1,8 @@ -source "https://rubygems.org" +# frozen_string_literal: true -group :test do - gem "mocha" - gem "rake" - gem "shoulda-context" -end +source 'https://rubygems.org' -gem "ffi" +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.10', '>= 1.10.0', groups: %i[development test], require: false diff --git a/LICENSE b/LICENSE index a38df06..6172eb1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2010-2017 Ari Russo +Copyright 2010-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 9383069..8be3c1c 100644 --- a/README.md +++ b/README.md @@ -45,4 +45,4 @@ Otherwise Apache 2.0, See the file LICENSE -Copyright (c) 2010-2017 Ari Russo +Copyright (c) 2010-2022 Ari Russo 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 8e0633a..6f473d6 100644 --- a/examples/input.rb +++ b/examples/input.rb @@ -1,9 +1,10 @@ #!/usr/bin/env ruby +# frozen_string_literal: true dir = File.dirname(File.expand_path(__FILE__)) -$LOAD_PATH.unshift dir + "/../lib" +$LOAD_PATH.unshift "#{dir}/../lib" -require "alsa-rawmidi" +require 'alsa-rawmidi' # Selects the first MIDI input and prints the first 10 messages it receives to standard out @@ -13,14 +14,12 @@ # or amidi -l from the command line AlsaRawMIDI::Input.first.open do |input| - - 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_devices.rb b/examples/list_devices.rb index 7018779..913490d 100644 --- a/examples/list_devices.rb +++ b/examples/list_devices.rb @@ -1,11 +1,12 @@ #!/usr/bin/env ruby +# frozen_string_literal: true dir = File.dirname(File.expand_path(__FILE__)) -$LOAD_PATH.unshift dir + "/../lib" +$LOAD_PATH.unshift "#{dir}/../lib" # Lists all of the available MIDI devices -require "alsa-rawmidi" -require "pp" +require 'alsa-rawmidi' +require 'pp' pp AlsaRawMIDI::Device.all_by_type diff --git a/examples/output.rb b/examples/output.rb index 1f360fa..6fb0f8a 100644 --- a/examples/output.rb +++ b/examples/output.rb @@ -1,9 +1,10 @@ #!/usr/bin/env ruby +# frozen_string_literal: true dir = File.dirname(File.expand_path(__FILE__)) -$LOAD_PATH.unshift dir + "/../lib" +$LOAD_PATH.unshift "#{dir}/../lib" -require "alsa-rawmidi" +require 'alsa-rawmidi' # Selects the first MIDI output and sends some arpeggiated chords to it @@ -14,23 +15,17 @@ # AlsaRawMIDI::Device.all.to_s will list your midi devices # or amidi -l from the command line -puts "Press Control-C to exit..." +puts 'Press Control-C to exit...' AlsaRawMIDI::Output.first.open do |output| - loop do - (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 sleep(duration) - end - end end - end diff --git a/examples/sysex_output.rb b/examples/sysex_output.rb index 540320b..7ee3de6 100644 --- a/examples/sysex_output.rb +++ b/examples/sysex_output.rb @@ -1,11 +1,12 @@ #!/usr/bin/env ruby +# frozen_string_literal: true dir = File.dirname(File.expand_path(__FILE__)) -$LOAD_PATH.unshift dir + "/../lib" +$LOAD_PATH.unshift "#{dir}/../lib" # Sends a MIDI system exclusive message to the selected output -require "alsa-rawmidi" +require 'alsa-rawmidi' output = AlsaRawMIDI::Output.first sysex_msg = [0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7] diff --git a/lib/alsa-rawmidi.rb b/lib/alsa-rawmidi.rb index 1e605ad..291b6b4 100644 --- a/lib/alsa-rawmidi.rb +++ b/lib/alsa-rawmidi.rb @@ -1,25 +1,27 @@ +# frozen_string_literal: true + # # alsa-rawmidi # ALSA Driver Interface # -# (c) 2010-2014 Ari Russo +# (c) 2010-2022 Ari Russo # Licensed under Apache 2.0 # https://github.com/arirusso/alsa-rawmidi # # libs -require "ffi" +require 'ffi' # modules -require "alsa-rawmidi/api" -require "alsa-rawmidi/device" -require "alsa-rawmidi/version" +require 'alsa-rawmidi/api' +require 'alsa-rawmidi/device' +require 'alsa-rawmidi/type_conversion' +require 'alsa-rawmidi/version' # class -require "alsa-rawmidi/input" -require "alsa-rawmidi/output" -require "alsa-rawmidi/soundcard" +require 'alsa-rawmidi/input' +require 'alsa-rawmidi/output' +require 'alsa-rawmidi/soundcard' module AlsaRawMIDI - end diff --git a/lib/alsa-rawmidi/api.rb b/lib/alsa-rawmidi/api.rb index 2ece1c7..2aeba66 100644 --- a/lib/alsa-rawmidi/api.rb +++ b/lib/alsa-rawmidi/api.rb @@ -1,18 +1,18 @@ -module AlsaRawMIDI +# frozen_string_literal: true +module AlsaRawMIDI # libasound RawMIDI struct, enum and function bindings module API - extend FFI::Library - ffi_lib "libasound" + ffi_lib 'libasound' CONSTANTS = { - :SND_RAWMIDI_STREAM_OUTPUT => 0, - :SND_RAWMIDI_STREAM_INPUT => 1, - :SND_RAWMIDI_APPEND => 0x0001, - :SND_RAWMIDI_NONBLOCK => 0x0002, - :SND_RAWMIDI_SYNC => 0x0004 - } + SND_RAWMIDI_STREAM_OUTPUT: 0, + SND_RAWMIDI_STREAM_INPUT: 1, + SND_RAWMIDI_APPEND: 0x0001, + SND_RAWMIDI_NONBLOCK: 0x0002, + SND_RAWMIDI_SYNC: 0x0004 + }.freeze typedef :ulong, :SndCtlType typedef :ulong, :SndCtl @@ -21,72 +21,72 @@ module API # snd_ctl class SndCtl < FFI::Struct layout :dl_handle, :pointer, # void* - :name, :pointer, # char* - :type, :SndCtlType, - :ops, :pointer, # const snd_ctl_ops_t* - :private_data, :pointer, # void* - :nonblock, :ulong, - :poll_fd, :ulong, - :async_handlers, :ulong + :name, :pointer, # char* + :type, :SndCtlType, + :ops, :pointer, # const snd_ctl_ops_t* + :private_data, :pointer, # void* + :nonblock, :ulong, + :poll_fd, :ulong, + :async_handlers, :ulong end # snd_ctl_card_info class SndCtlCardInfo < FFI::Struct layout :card, :int, # card number - :pad, :int, # reserved for future (was type) - :id, [:uchar, 16], # ID of card (user selectable) - :driver, [:uchar, 16], # Driver name - :name, [:uchar, 32], # Short name of soundcard - :longname, [:uchar, 80], # name + info text about soundcard - :reserved_, [:uchar, 16], # reserved for future (was ID of mixer) - :mixername, [:uchar, 80], # visual mixer identification - :components, [:uchar, 128] # card components / fine identification, delimited with one space (AC97 etc..) + :pad, :int, # reserved for future (was type) + :id, [:uchar, 16], # ID of card (user selectable) + :driver, [:uchar, 16], # Driver name + :name, [:uchar, 32], # Short name of soundcard + :longname, [:uchar, 80], # name + info text about soundcard + :reserved_, [:uchar, 16], # reserved for future (was ID of mixer) + :mixername, [:uchar, 80], # visual mixer identification + :components, [:uchar, 128] # card components / fine identification, delimited with one space (AC97 etc..) end # snd_rawmidi_info class SndRawMIDIInfo < FFI::Struct layout :device, :uint, # RO/WR (control): device number - :subdevice, :uint, # RO/WR (control): subdevice number - :stream, :int, # WR: stream - :card, :int, # R: card number - :flags, :uint, # SNDRV_RAWMIDI_INFO_XXXX - :id, [:uchar, 64], # ID (user selectable) - :name, [:uchar, 80], # name of device - :subname, [:uchar, 32], # name of active or selected subdevice - :subdevices_count, :uint, - :subdevices_avail, :uint, - :reserved, [:uchar, 64] # reserved for future use + :subdevice, :uint, # RO/WR (control): subdevice number + :stream, :int, # WR: stream + :card, :int, # R: card number + :flags, :uint, # SNDRV_RAWMIDI_INFO_XXXX + :id, [:uchar, 64], # ID (user selectable) + :name, [:uchar, 80], # name of device + :subname, [:uchar, 32], # name of active or selected subdevice + :subdevices_count, :uint, + :subdevices_avail, :uint, + :reserved, [:uchar, 64] # reserved for future use end # timespec class Timespec < FFI::Struct layout :tv_sec, :time_t, # Seconds since 00:00:00 GMT - :tv_nsec, :long # Additional nanoseconds since + :tv_nsec, :long # Additional nanoseconds since end # snd_rawmidi_status class SndRawMIDIStatus < FFI::Struct layout :stream, :int, - :timestamp, Timespec.by_value, # Timestamp - :avail, :size_t, # available bytes - :xruns, :size_t, # count of overruns since last status (in bytes) - :reserved, [:uchar, 64] # reserved for future use + :timestamp, Timespec.by_value, # Timestamp + :avail, :size_t, # available bytes + :xruns, :size_t, # count of overruns since last status (in bytes) + :reserved, [:uchar, 64] # reserved for future use end # Simple doubly linked list implementation class LinkedList < FFI::Struct layout :next, :pointer, # *LinkedList - :prev, :pointer # *LinkedList + :prev, :pointer # *LinkedList end # snd_rawmidi class SndRawMIDI < FFI::Struct layout :card, :pointer, # *snd_card - :list, LinkedList.by_value, - :device, :uint, # device number - :info_flags, :uint, # SNDRV_RAWMIDI_INFO_XXXX - :id, [:char, 64], - :name, [:char, 80] + :list, LinkedList.by_value, + :device, :uint, # device number + :info_flags, :uint, # SNDRV_RAWMIDI_INFO_XXXX + :id, [:char, 64], + :name, [:char, 80] end # spinlock_t @@ -97,7 +97,7 @@ class Spinlock < FFI::Struct # wait_queue_head_t class WaitQueueHead < FFI::Struct layout :lock, Spinlock.by_value, - :task_list, LinkedList.by_value + :task_list, LinkedList.by_value end class AtomicT < FFI::Struct @@ -105,43 +105,43 @@ class AtomicT < FFI::Struct end class Tasklet < FFI::Struct - layout :next, :pointer, # pointer to the next tasklet in the list / void (*func) (unsigned long) - :state, :ulong, # state of the tasklet - :count, AtomicT.by_value, # reference counter - :func, :pointer, # tasklet handler function / void (*func) (unsigned long) - :data, :ulong # argument to the tasklet function + layout :next, :pointer, # pointer to the next tasklet in the list / void (*func) (unsigned long) + :state, :ulong, # state of the tasklet + :count, AtomicT.by_value, # reference counter + :func, :pointer, # tasklet handler function / void (*func) (unsigned long) + :data, :ulong # argument to the tasklet function end # snd_rawmidi_runtime class SndRawMIDIRuntime < FFI::Struct layout :drain, :uint, 1, # drain stage - :oss, :uint, 1, # OSS compatible mode - # midi stream buffer - :buffer, :pointer, # uchar* / buffer for MIDI data - :buffer_size, :size_t, # size of buffer - :appl_ptr, :size_t, # application pointer - :hw_ptr, :size_t, # hardware pointer - :avail_min, :size_t, # min avail for wakeup - :avail, :size_t, # max used buffer for wakeup - :xruns, :size_t, # over/underruns counter - # misc - :lock, Spinlock.by_value, - :sleep, WaitQueueHead.by_value, - # event handler (new bytes, input only) - :substream, :pointer, # void (*event)(struct snd_rawmidi_substream *substream); - # defers calls to event [input] or ops->trigger [output] - :tasklet, Tasklet.by_value, - :private_data, :pointer, # void* - :private_free, :pointer # void (*private_free)(struct snd_rawmidi_substream *substream) + :oss, :uint, 1, # OSS compatible mode + # midi stream buffer + :buffer, :pointer, # uchar* / buffer for MIDI data + :buffer_size, :size_t, # size of buffer + :appl_ptr, :size_t, # application pointer + :hw_ptr, :size_t, # hardware pointer + :avail_min, :size_t, # min avail for wakeup + :avail, :size_t, # max used buffer for wakeup + :xruns, :size_t, # over/underruns counter + # misc + :lock, Spinlock.by_value, + :sleep, WaitQueueHead.by_value, + # event handler (new bytes, input only) + :substream, :pointer, # void (*event)(struct snd_rawmidi_substream *substream); + # defers calls to event [input] or ops->trigger [output] + :tasklet, Tasklet.by_value, + :private_data, :pointer, # void* + :private_free, :pointer # void (*private_free)(struct snd_rawmidi_substream *substream) end # snd_rawmidi_params class SndRawMIDIParams < FFI::Struct layout :stream, :int, - :buffer_size, :size_t, # queue size in bytes - :avail_min, :size_t, # minimum avail bytes for wakeup - :no_active_sensing, :uint, 1, # do not send active sensing byte in close() - :reserved, [:uchar, 16] # reserved for future use + :buffer_size, :size_t, # queue size in bytes + :avail_min, :size_t, # minimum avail bytes for wakeup + :no_active_sensing, :uint, 1, # do not send active sensing byte in close() + :reserved, [:uchar, 16] # reserved for future use end # @@ -155,30 +155,30 @@ class SndRawMIDIParams < FFI::Struct # Convert card string to an integer value. attach_function :snd_card_get_index, [:pointer], :int # (const char* name) # Obtain the card name. - attach_function :snd_card_get_name, [:int, :pointer], :int # (int card, char **name) + attach_function :snd_card_get_name, %i[int pointer], :int # (int card, char **name) # Obtain the card long name. - attach_function :snd_card_get_longname, [:int, :pointer], :int # (int card, char **name) + attach_function :snd_card_get_longname, %i[int pointer], :int # (int card, char **name) # # snd_ctl # # Opens a CTL. - attach_function :snd_ctl_open, [:pointer, :pointer, :int], :int # (snd_ctl_t **ctl, const char *name, int mode) + attach_function :snd_ctl_open, %i[pointer pointer int], :int # (snd_ctl_t **ctl, const char *name, int mode) # Opens a CTL using local configuration. - attach_function :snd_ctl_open_lconf, [:pointer, :pointer, :int, :pointer], :int # (snd_ctl_t **ctl, const char *name, int mode, snd_config_t *lconf) + attach_function :snd_ctl_open_lconf, %i[pointer pointer int pointer], :int # (snd_ctl_t **ctl, const char *name, int mode, snd_config_t *lconf) # close CTL handle - attach_function :snd_ctl_close, [:pointer], :int #(snd_ctl_t *ctl) + attach_function :snd_ctl_close, [:pointer], :int # (snd_ctl_t *ctl) # set nonblock mode - attach_function :snd_ctl_nonblock, [:pointer, :int], :int # (snd_ctl_t *ctl, int nonblock) + attach_function :snd_ctl_nonblock, %i[pointer int], :int # (snd_ctl_t *ctl, int nonblock) # Get card related information. - attach_function :snd_ctl_card_info, [:pointer, :pointer], :int # (snd_ctl_t *ctl, snd_ctl_card_info_t *info) + attach_function :snd_ctl_card_info, %i[pointer pointer], :int # (snd_ctl_t *ctl, snd_ctl_card_info_t *info) # Get card name from a CTL card info. attach_function :snd_ctl_card_info_get_name, [:pointer], :string # (const snd_ctl_card_info_t *obj) / const char* # Get info about a RawMidi device. - attach_function :snd_ctl_rawmidi_info, [:SndCtl, :pointer], :int # (snd_ctl_t *ctl, snd_rawmidi_info_t *info) + attach_function :snd_ctl_rawmidi_info, %i[SndCtl pointer], :int # (snd_ctl_t *ctl, snd_rawmidi_info_t *info) # Get next RawMidi device number. - attach_function :snd_ctl_rawmidi_next_device, [:SndCtl, :pointer], :int # (snd_ctl_t *ctl, int *device) + attach_function :snd_ctl_rawmidi_next_device, %i[SndCtl pointer], :int # (snd_ctl_t *ctl, int *device) # # snd_rawmidi @@ -191,36 +191,36 @@ class SndRawMIDIParams < FFI::Struct # drop all bytes in the rawmidi I/O ring buffer immediately attach_function :snd_rawmidi_drop, [:SndRawMIDI], :int # int ( snd_rawmidi_t * rawmidi) # set nonblock mode - attach_function :snd_rawmidi_nonblock, [:SndRawMIDI, :int], :int # (snd_rawmidi_t *rmidi, int nonblock) + attach_function :snd_rawmidi_nonblock, %i[SndRawMIDI int], :int # (snd_rawmidi_t *rmidi, int nonblock) # Opens a new connection to the RawMidi interface. - attach_function :snd_rawmidi_open, [:pointer, :pointer, :string, :int], :int # (snd_rawmidi_t **in_rmidi, snd_rawmidi_t **out_rmidi, const char *name, int mode) + attach_function :snd_rawmidi_open, %i[pointer pointer string int], :int # (snd_rawmidi_t **in_rmidi, snd_rawmidi_t **out_rmidi, const char *name, int mode) # Opens a new connection to the RawMidi interface using local configuration. - attach_function :snd_rawmidi_open_lconf, [:pointer, :pointer, :string, :int, :pointer], :int #(snd_rawmidi_t **in_rmidi, snd_rawmidi_t **out_rmidi, const char *name, int mode, snd_config_t *lconf) + attach_function :snd_rawmidi_open_lconf, %i[pointer pointer string int pointer], :int # (snd_rawmidi_t **in_rmidi, snd_rawmidi_t **out_rmidi, const char *name, int mode, snd_config_t *lconf) # read MIDI bytes from MIDI stream - attach_function :snd_rawmidi_read, [:SndRawMIDI, :pointer, :size_t], :ssize_t # (snd_rawmidi_t *rmidi, void *buffer, size_t size) + attach_function :snd_rawmidi_read, %i[SndRawMIDI pointer size_t], :ssize_t # (snd_rawmidi_t *rmidi, void *buffer, size_t size) # write MIDI bytes to MIDI stream - attach_function :snd_rawmidi_write, [:SndRawMIDI, :ulong, :size_t], :ssize_t # (snd_rawmidi_t *rmidi, const void *buffer, size_t size) + attach_function :snd_rawmidi_write, %i[SndRawMIDI ulong size_t], :ssize_t # (snd_rawmidi_t *rmidi, const void *buffer, size_t size) # # snd_rawmidi_info # enum :snd_rawmidi_stream, [ - "SND_RAWMIDI_STREAM_OUTPUT", 0, - "SND_RAWMIDI_STREAM_INPUT", 1, - "SND_RAWMIDI_STREAM_LAST", 1 + 'SND_RAWMIDI_STREAM_OUTPUT', 0, + 'SND_RAWMIDI_STREAM_INPUT', 1, + 'SND_RAWMIDI_STREAM_LAST', 1 ] # get information about RawMidi handle - attach_function :snd_rawmidi_info, [:pointer, :pointer], :int # (snd_rawmidi_t *rmidi, snd_rawmidi_info_t *info) + attach_function :snd_rawmidi_info, %i[pointer pointer], :int # (snd_rawmidi_t *rmidi, snd_rawmidi_info_t *info) # get rawmidi count of subdevices attach_function :snd_rawmidi_info_get_subdevices_count, [:pointer], :uint # (const snd_rawmidi_info_t *obj) # set rawmidi device number - attach_function :snd_rawmidi_info_set_device, [:pointer, :uint], :void # (snd_rawmidi_info_t *obj, unsigned int val) + attach_function :snd_rawmidi_info_set_device, %i[pointer uint], :void # (snd_rawmidi_info_t *obj, unsigned int val) # set rawmidi subdevice number - attach_function :snd_rawmidi_info_set_subdevice, [:pointer, :uint], :void # (snd_rawmidi_info_t *obj, unsigned int val) + attach_function :snd_rawmidi_info_set_subdevice, %i[pointer uint], :void # (snd_rawmidi_info_t *obj, unsigned int val) # set rawmidi stream identifier - attach_function :snd_rawmidi_info_set_stream, [:pointer, :snd_rawmidi_stream], :void # (snd_rawmidi_info_t *obj, snd_rawmidi_stream_t val) + attach_function :snd_rawmidi_info_set_stream, %i[pointer snd_rawmidi_stream], :void # (snd_rawmidi_info_t *obj, snd_rawmidi_stream_t val) # get size of the snd_rawmidi_info_t structure in bytes attach_function :snd_rawmidi_info_sizeof, [], :size_t # (void) @@ -235,17 +235,16 @@ class SndRawMIDIParams < FFI::Struct # Wrapper for ALSA methods dealing with input module Input - BUFFER_SIZE = 256 - extend self + module_function # Open the output with the given ID # @param [Integer] id # @return [Integer] def open(id) API::Device.open(id) do |pointer| - API.snd_rawmidi_open(pointer, nil, id, API::CONSTANTS[:SND_RAWMIDI_NONBLOCK]) + API.snd_rawmidi_open(pointer, nil, id, API::CONSTANTS[:SND_RAWMIDI_NONBLOCK]) end end @@ -253,30 +252,29 @@ def open(id) # @return [String] def poll(handle) buffer = FFI::MemoryPointer.new(:uint8, BUFFER_SIZE) - if (err = API.snd_rawmidi_read(handle, buffer, BUFFER_SIZE)) < 0 - raise "Can't read MIDI input: #{API.snd_strerror(err)}" unless err.eql?(-11) + if (err = API.snd_rawmidi_read(handle, buffer, BUFFER_SIZE)) < (0) && !err.eql?(-11) + raise "Can't read MIDI input: #{API.snd_strerror(err)}" end + # Upon success, err is positive and equal to the number of bytes read # into the buffer. - if err > 0 - bytes = buffer.get_bytes(0,err).unpack("a*").first.unpack("H*") + if err.positive? + bytes = buffer.get_bytes(0, err).unpack1('a*').unpack('H*') bytes.first.upcase end end - end # Wrapper for ALSA methods dealing with output module Output - - extend self + module_function # Send the given MIDI data to the output with the given handle # @param [Integer] handle # @param [Array] data # @return [Boolean] def puts(handle, data) - format = "C" * data.size + format = 'C' * data.size pointer = FFI::MemoryPointer.new(data.size) bytes = pointer.put_bytes(0, data.pack(format)) @@ -293,22 +291,20 @@ def open(id) API.snd_rawmidi_open(nil, pointer, id, 0) end end - end # Wrapper for ALSA methods dealing with devices module Device - - extend self + module_function # @param [Integer] id # @param [Symbol] direction # @return [SndCtlCardInfo] def get_info(id, direction) stream_key = case direction - when :input then :SND_RAWMIDI_STREAM_INPUT - when :output then :SND_RAWMIDI_STREAM_OUTPUT - end + when :input then :SND_RAWMIDI_STREAM_INPUT + when :output then :SND_RAWMIDI_STREAM_OUTPUT + end stream = API::CONSTANTS[stream_key] info = API::SndRawMIDIInfo.new API.snd_rawmidi_info_set_device(info.pointer, id) @@ -329,18 +325,16 @@ def close(handle) # @param [Integer] id # @param [Proc] block # @return [Integer] - def open(id, &block) + def open(_id) handle_pointer = FFI::MemoryPointer.new(FFI.type_size(:int)) yield(handle_pointer) handle_pointer.read_int end - end # Wrapper for ALSA methods dealing with the soundcard and subdevices module Soundcard - - extend self + module_function # @param [SndCtlCardInfo] info # @return [Integer] @@ -355,9 +349,9 @@ def get_subdevice_count(info) # @param [Integer] id # @return [String] def get_subdevice_id(soundcard_id, device_id, subdev_count, id) - ext = (subdev_count > 1) ? ",#{id}" : '' + ext = subdev_count > 1 ? ",#{id}" : '' name = API::Soundcard.get_name(soundcard_id) - "#{name},#{device_id.to_s}#{ext}" + "#{name},#{device_id}#{ext}" end # @param [SndCtlCardInfo] info @@ -374,7 +368,7 @@ def valid_subdevice?(info, id, handle) # @param [Symbol] direction # @param [Proc] block # @return [Array] - def get_subdevices(direction, soundcard_id, device_id, &block) + def get_subdevices(direction, soundcard_id, device_id) handle = API::Soundcard.get_handle(soundcard_id) info = API::Device.get_info(device_id, direction) i = 0 @@ -385,9 +379,9 @@ def get_subdevices(direction, soundcard_id, device_id, &block) subdev_count = API::Soundcard.get_subdevice_count(info) if i.zero? system_id = API::Soundcard.get_subdevice_id(soundcard_id, device_id, subdev_count, i) device_hash = { - :id => system_id, - :name => info[:name].to_s, - :subname => info[:subname].to_s + id: system_id, + name: info[:name].to_s, + subname: info[:subname].to_s } available << yield(device_hash) i += 1 @@ -411,7 +405,7 @@ def get_device_ids(id) # @param [Integer] soundcard_id # @return [String] def get_name(soundcard_id) - "hw:#{soundcard_id.to_s}" + "hw:#{soundcard_id}" end # Does a soundcard exist for the given id? @@ -428,8 +422,6 @@ def get_handle(soundcard_id) API.snd_ctl_open(handle_pointer, get_name(soundcard_id), 0) handle_pointer.read_int end - end - end end diff --git a/lib/alsa-rawmidi/device.rb b/lib/alsa-rawmidi/device.rb index 59a1704..0e3f2bd 100644 --- a/lib/alsa-rawmidi/device.rb +++ b/lib/alsa-rawmidi/device.rb @@ -1,10 +1,9 @@ -module AlsaRawMIDI +# frozen_string_literal: true +module AlsaRawMIDI # Functionality common to both inputs and outputs module Device - module ClassMethods - # Select the first device of the given direction # @param [Symbol] direction # @return [Input, Output] @@ -37,38 +36,37 @@ def all # @return [Hash] def get_devices available_devices = { - :input => [], - :output => [] + input: [], + output: [] } device_count = 0 32.times do |i| card = Soundcard.find(i) - unless card.nil? - available_devices.keys.each do |direction| - devices = card.subdevices[direction] - devices.each do |dev| - dev.send(:id=, device_count) - device_count += 1 - end - available_devices[direction] += devices + next if card.nil? + + available_devices.each_key do |direction| + devices = card.subdevices[direction] + devices.each do |dev| + dev.send(:id=, device_count) + device_count += 1 end + available_devices[direction] += devices end end available_devices end - end extend ClassMethods attr_reader :enabled, # has the device been initialized? - :system_id, # the alsa id of the device - :id, # a local uuid for the device - :name, - :subname, - :type # :input or :output + :system_id, # the alsa id of the device + :id, # a local uuid for the device + :name, + :subname, + :type # :input or :output - alias_method :enabled?, :enabled + alias enabled? enabled def self.included(base) base.send(:extend, ClassMethods) @@ -102,7 +100,5 @@ def id=(id) def get_type self.class.name.split('::').last.downcase.to_sym end - end - end diff --git a/lib/alsa-rawmidi/input.rb b/lib/alsa-rawmidi/input.rb index fecf89d..a2fce81 100644 --- a/lib/alsa-rawmidi/input.rb +++ b/lib/alsa-rawmidi/input.rb @@ -1,8 +1,8 @@ -module AlsaRawMIDI +# frozen_string_literal: true +module AlsaRawMIDI # Input device class class Input - include Device attr_reader :buffer @@ -25,7 +25,7 @@ def gets @pointer = @buffer.length msgs end - alias_method :read, :gets + alias read gets # Like Input#gets but returns message data as string of hex digits as such: # [ @@ -37,17 +37,17 @@ def gets # @return [Array] def gets_s msgs = gets - msgs.each { |m| m[:data] = numeric_bytes_to_hex_string(m[:data]) } + msgs.each { |m| m[:data] = TypeConversion.numeric_bytes_to_hex_string(m[:data]) } msgs end - alias_method :gets_bytestr, :gets_s - alias_method :gets_hex, :gets_s + alias gets_bytestr gets_s + alias gets_hex gets_s # Enable this the input for use; yields # @param [Hash] options # @param [Proc] block # @return [Input] self - def enable(options = {}, &block) + def enable(_options = {}) unless @enabled @start_time = Time.now.to_f @resource = API::Input.open(@system_id) @@ -64,8 +64,8 @@ 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] @@ -125,8 +125,8 @@ def now # @return [Hash] def get_message_formatted(hexstring, timestamp) { - :data => hex_string_to_numeric_bytes(hexstring), - :timestamp => timestamp + data: TypeConversion.hex_string_to_numeric_bytes(hexstring), + timestamp: timestamp } end @@ -146,7 +146,7 @@ def enqueued_messages? # and holds them for the next call to gets* # @return [Thread] def spawn_listener - interval = 1.0/1000 + interval = 1.0 / 1000 @listener = Thread.new do begin loop do @@ -155,8 +155,8 @@ def spawn_listener end populate_buffer(messages) unless messages.nil? end - rescue Exception => exception - Thread.main.raise(exception) + rescue Exception => e + Thread.main.raise(e) end end @listener.abort_on_exception = true @@ -168,32 +168,5 @@ def spawn_listener def populate_buffer(messages) @buffer << get_message_formatted(messages, now) unless messages.nil? end - - # Convert a hex string to an array of numeric bytes eg "904040" -> [0x90, 0x40, 0x40] - # @param [String] string - # @return [Array] - def hex_string_to_numeric_bytes(string) - string = string.dup - bytes = [] - until string.length.zero? - string_byte = string.slice!(0, 2) - bytes << string_byte.hex - end - bytes - end - - # Convert an array of numeric bytes to a hex string eg [0x90, 0x40, 0x40] -> "904040" - # @param [Array] bytes - # @return [String] - def numeric_bytes_to_hex_string(bytes) - string_bytes = bytes.map do |byte| - string_byte = byte.to_s(16).upcase - string_byte = "0#{string_byte}" if byte < 16 - string_byte - end - string_bytes.join - end - end - end diff --git a/lib/alsa-rawmidi/output.rb b/lib/alsa-rawmidi/output.rb index b84b78f..80fc5fc 100644 --- a/lib/alsa-rawmidi/output.rb +++ b/lib/alsa-rawmidi/output.rb @@ -1,8 +1,8 @@ -module AlsaRawMIDI +# frozen_string_literal: true +module AlsaRawMIDI # Output device class class Output - include Device # Close this output @@ -23,14 +23,14 @@ def close def puts_s(data) data = data.dup output = [] - until (str = data.slice!(0,2)) == "" + until (str = data.slice!(0, 2)) == '' output << str.hex end puts_bytes(*output) true end - alias_method :puts_bytestr, :puts_s - alias_method :puts_hex, :puts_s + alias puts_bytestr puts_s + alias puts_hex puts_s # Output a MIDI message in numeric byte format # @param [*Integer] data @@ -45,18 +45,18 @@ def puts_bytes(*data) # @return [Boolean] def puts(*args) case args.first - when Array then args.each { |arg| puts(*arg) } - when Numeric then puts_bytes(*args) - when String then puts_bytestr(*args) + when Array then args.each { |arg| puts(*arg) } + when Numeric then puts_bytes(*args) + when String then puts_bytestr(*args) end end - alias_method :write, :puts + alias write puts # Enable this device; yields # @param [Hash] options # @param [Proc] block # @return [Output] - def enable(options = {}, &block) + def enable(_options = {}) unless @enabled @resource = API::Output.open(@system_id) @enabled = true @@ -70,8 +70,8 @@ def enable(options = {}, &block) end self end - alias_method :open, :enable - alias_method :start, :enable + alias open enable + alias start enable # The first available output # @return [Output] @@ -90,7 +90,5 @@ def self.last def self.all Device.all_by_type[:output] end - end - end diff --git a/lib/alsa-rawmidi/soundcard.rb b/lib/alsa-rawmidi/soundcard.rb index 2e99f05..5a84d0d 100644 --- a/lib/alsa-rawmidi/soundcard.rb +++ b/lib/alsa-rawmidi/soundcard.rb @@ -1,14 +1,14 @@ -module AlsaRawMIDI +# frozen_string_literal: true +module AlsaRawMIDI class Soundcard - attr_reader :id, :subdevices # @param [Integer] id def initialize(id) @subdevices = { - :input => [], - :output => [] + input: [], + output: [] } @id = id populate_subdevices @@ -19,9 +19,7 @@ def initialize(id) # @return [Soundcard] def self.find(id) @soundcards ||= {} - if API::Soundcard.exists?(id) - @soundcards[id] ||= Soundcard.new(id) - end + @soundcards[id] ||= Soundcard.new(id) if API::Soundcard.exists?(id) end private @@ -30,7 +28,7 @@ def self.find(id) def populate_subdevices device_ids = API::Soundcard.get_device_ids(@id) device_ids.each do |device_id| - @subdevices.keys.each do |direction| + @subdevices.each_key do |direction| devices = API::Soundcard.get_subdevices(direction, @id, device_id) do |device_hash| new_device(direction, device_hash) end @@ -45,17 +43,15 @@ def populate_subdevices # @return [Input, Output] def new_device(direction, device_hash) device_class = case direction - when :input then Input - when :output then Output - end + when :input then Input + when :output then Output + end device_properties = { - :system_id => device_hash[:id], - :name => device_hash[:name], - :subname => device_hash[:subname] + system_id: device_hash[:id], + name: device_hash[:name], + subname: device_hash[:subname] } device_class.new(device_properties) end - end - end diff --git a/lib/alsa-rawmidi/type_conversion.rb b/lib/alsa-rawmidi/type_conversion.rb new file mode 100644 index 0000000..0920751 --- /dev/null +++ b/lib/alsa-rawmidi/type_conversion.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module AlsaRawMIDI + # Helper for converting MIDI data + module TypeConversion + module_function + + # Convert a hex string to an array of numeric bytes eg "904040" -> [0x90, 0x40, 0x40] + # @param [String] string + # @return [Array] + def hex_string_to_numeric_bytes(string) + string = string.dup + bytes = [] + until string.length.zero? + string_byte = string.slice!(0, 2) + bytes << string_byte.hex + end + bytes + end + + # Convert an array of numeric bytes to a hex string eg [0x90, 0x40, 0x40] -> "904040" + # @param [Array] bytes + # @return [String] + def numeric_bytes_to_hex_string(bytes) + string_bytes = bytes.map do |byte| + string_byte = byte.to_s(16).upcase + string_byte = "0#{string_byte}" if byte < 16 + string_byte + end + string_bytes.join + end + end +end diff --git a/lib/alsa-rawmidi/version.rb b/lib/alsa-rawmidi/version.rb index 327eb93..39c24bf 100644 --- a/lib/alsa-rawmidi/version.rb +++ b/lib/alsa-rawmidi/version.rb @@ -1,5 +1,5 @@ -module AlsaRawMIDI - - VERSION = "0.3.2" +# frozen_string_literal: true +module AlsaRawMIDI + VERSION = '0.4.0' end diff --git a/spec/helper.rb b/spec/helper.rb new file mode 100644 index 0000000..f86356e --- /dev/null +++ b/spec/helper.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +dir = File.dirname(File.expand_path(__FILE__)) +$LOAD_PATH.unshift "#{dir}/../lib" + +require 'rspec' +require 'alsa-rawmidi' + +module SpecHelper + module_function + + 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 + def numeric_messages + [ + [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 + [0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7] # SysEx + ] + end + + # some MIDI messages + def string_messages + [ + '906440', # NOTE: on + '804340', # NOTE: off + 'F04110421240007F0041F7' # SysEx + ] + end + + def device + @device ||= select_devices + end + + def select_devices + @device ||= {} + { input: AlsaRawMIDI::Input.all, output: AlsaRawMIDI::Output.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 +end diff --git a/spec/input_buffer_spec.rb b/spec/input_buffer_spec.rb new file mode 100644 index 0000000..0e1246e --- /dev/null +++ b/spec/input_buffer_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +require 'helper' + +describe 'input buffer' do + let(:output) { SpecHelper.device[:output].open } + let(:input) { SpecHelper.device[:input].open } + + before do + sleep 0.3 + input.buffer.clear + end + + context 'Source#buffer' do + let(:messages) { SpecHelper.numeric_messages } + + after do + input.close + output.close + end + + it 'has the correct messages in the buffer' do + bytes = [] + buffer = nil + messages.each do |message| + p "sending: #{message}" + output.puts(message) + bytes += message + + sleep 0.3 + + buffer = input.buffer.map { |m| m[:data] }.flatten + p "received: #{buffer}" + expect(buffer).to eq(bytes) + end + expect(buffer.length).to eq(bytes.length) + end + end +end diff --git a/spec/io_spec.rb b/spec/io_spec.rb new file mode 100644 index 0000000..f357f24 --- /dev/null +++ b/spec/io_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'helper' + +describe 'io' do + # ** this spec assumes that the test input is connected to the test output + let(:output) { SpecHelper.device[:output].open } + let(:input) { SpecHelper.device[:input].open } + + before do + sleep 0.3 + input.buffer.clear + end + + describe 'full IO' do + describe 'using Arrays' do + let(:messages) { SpecHelper.numeric_messages } + let(:messages_as_bytes) { messages.inject(&:+).flatten } + + after do + input.close + output.close + end + + it 'does IO' do + pointer = 0 + result = messages.map do |message| + p "sending: #{message}" + + output.puts(message) + sleep 0.3 + received = input.gets.map { |m| m[:data] }.flatten + + p "received: #{received}" + + expect(received).to eq(messages_as_bytes.slice(pointer, received.length)) + pointer += received.length + received + end + expect(result.flatten.length).to eq(messages_as_bytes.length) + end + end + + context 'using byte Strings' do + let(:messages) { SpecHelper.string_messages } + let(:messages_as_string) { messages.join } + + it 'does IO' do + pointer = 0 + result = messages.map do |message| + p "sending: #{message}" + + output.puts(message) + sleep 0.3 + received = input.gets_bytestr.map { |m| m[:data] }.flatten.join + p "received: #{received}" + + expect(received).to eq(messages_as_string.slice(pointer, received.length)) + pointer += received.length + received + end + expect(result).to eq(messages) + end + end + end +end diff --git a/test/helper.rb b/test/helper.rb deleted file mode 100644 index 9c26088..0000000 --- a/test/helper.rb +++ /dev/null @@ -1,51 +0,0 @@ -dir = File.dirname(File.expand_path(__FILE__)) -$LOAD_PATH.unshift dir + "/../lib" - -require "test/unit" -require "mocha/test_unit" -require "shoulda-context" -require "alsa-rawmidi" - -module TestHelper - - extend self - - 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 - def numeric_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 - ] - end - - # some MIDI messages - def string_messages - [ - "F04110421240007F0041F7", # SysEx - "906440", # note on - "804340" # note off - ] - end - - def input - AlsaRawMIDI::Input.first - end - - def output - AlsaRawMIDI::Output.first - end - -end diff --git a/test/input_buffer_test.rb b/test/input_buffer_test.rb deleted file mode 100644 index f76d450..0000000 --- a/test/input_buffer_test.rb +++ /dev/null @@ -1,46 +0,0 @@ -require "helper" - -class AlsaRawMIDI::InputBufferTest < Test::Unit::TestCase - - context "AlsaRawMIDI" do - - setup do - sleep(1) - @input = TestHelper.input.open - @output = TestHelper.output.open - @input.buffer.clear - @pointer = 0 - end - - context "Source#buffer" do - - setup do - @messages = TestHelper.numeric_messages - @messages_arr = @messages.inject(&:+).flatten - @received_arr = [] - end - - teardown do - @input.close - @output.close - end - - should "have the correct messages in the buffer" do - bytes = [] - @messages.each do |message| - p "sending: #{message}" - @output.puts(message) - bytes += message - - sleep(1) - - buffer = @input.buffer.map { |m| m[:data] }.flatten - p "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 f6b1443..0000000 --- a/test/io_test.rb +++ /dev/null @@ -1,80 +0,0 @@ -require "helper" - -class AlsaRawMIDI::IoTest < Test::Unit::TestCase - - # ** this test assumes that TestOutput is connected to TestInput - context "AlsaRawMIDI" do - - setup do - sleep(1) - @input = TestHelper.input.open - @output = TestHelper.output.open - @input.buffer.clear - @pointer = 0 - end - - context "full IO" do - - context "using Arrays" do - - setup do - @messages = TestHelper.numeric_messages - @messages_arr = @messages.inject(&:+).flatten - @received_arr = [] - end - - teardown do - @input.close - @output.close - end - - should "do IO" do - @messages.each do |message| - - p "sending: #{message}" - - @output.puts(message) - sleep(1) - received = @input.gets.map { |m| m[:data] }.flatten - - p "received: #{received}" - - 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 - - context "using byte Strings" do - - setup do - @messages = TestHelper.string_messages - @messages_str = @messages.join - @received_str = "" - end - - should "do IO" do - @messages.each do |message| - - p "sending: #{message}" - - @output.puts(message) - sleep(1) - received = @input.gets_bytestr.map { |m| m[:data] }.flatten.join - p "received: #{received}" - - 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