shmidi 0.1

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.
@@ -0,0 +1,259 @@
1
+ module CoreMIDI
2
+
3
+ # Type of endpoint used for input
4
+ class Source
5
+
6
+ include Endpoint
7
+
8
+ attr_reader :buffer
9
+
10
+ #
11
+ # An array of MIDI event hashes as such:
12
+ # [
13
+ # { :data => [144, 60, 100], :timestamp => 1024 },
14
+ # { :data => [128, 60, 100], :timestamp => 1100 },
15
+ # { :data => [144, 40, 120], :timestamp => 1200 }
16
+ # ]
17
+ #
18
+ # The data is an array of Numeric bytes
19
+ # The timestamp is the number of millis since this input was enabled
20
+ #
21
+ # @return [Array<Hash>]
22
+ def gets
23
+ # SINM ADDED
24
+ @queue.pop
25
+ # SINM COMMENTED OUT
26
+ # until queued_messages?
27
+ # # per https://github.com/arirusso/unimidi/issues/20#issuecomment-44761318
28
+ # sleep(0.0001) # patch to prevent 100% CPU issue with some midi controllers
29
+ # end
30
+ # messages = queued_messages
31
+ # @pointer = @buffer.length
32
+ # messages
33
+ end
34
+ alias_method :read, :gets
35
+
36
+ # Same as Source#gets except that it returns message data as string of hex
37
+ # digits as such:
38
+ # [
39
+ # { :data => "904060", :timestamp => 904 },
40
+ # { :data => "804060", :timestamp => 1150 },
41
+ # { :data => "90447F", :timestamp => 1300 }
42
+ # ]
43
+ #
44
+ # @return [Array<Hash>]
45
+ def gets_s
46
+ messages = gets
47
+ messages.each do |message|
48
+ message[:data] = TypeConversion.numeric_bytes_to_hex_string(message[:data])
49
+ end
50
+ messages
51
+ end
52
+ alias_method :gets_bytestr, :gets_s
53
+
54
+ # Enable this the input for use; can be passed a block
55
+ # @return [Source]
56
+ def enable(options = {}, &block)
57
+ @enabled = true unless @enabled
58
+ if block_given?
59
+ begin
60
+ yield(self)
61
+ ensure
62
+ close
63
+ end
64
+ end
65
+ self
66
+ end
67
+ alias_method :open, :enable
68
+ alias_method :start, :enable
69
+
70
+ # Close this input
71
+ # @return [Boolean]
72
+ def close
73
+ #error = API.MIDIPortDisconnectSource( @handle, @resource )
74
+ #raise "MIDIPortDisconnectSource returned error code #{error}" unless error.zero?
75
+ #error = API.MIDIClientDispose(@handle)
76
+ #raise "MIDIClientDispose returned error code #{error}" unless error.zero?
77
+ #error = API.MIDIPortDispose(@handle)
78
+ #raise "MIDIPortDispose returned error code #{error}" unless error.zero?
79
+ #error = API.MIDIEndpointDispose(@resource)
80
+ #raise "MIDIEndpointDispose returned error code #{error}" unless error.zero?
81
+ if @enabled
82
+ @enabled = false
83
+ true
84
+ else
85
+ false
86
+ end
87
+ end
88
+
89
+ # Shortcut to the first available input endpoint
90
+ # @return [Source]
91
+ def self.first
92
+ Endpoint.first(:source)
93
+ end
94
+
95
+ # Shortcut to the last available input endpoint
96
+ # @return [Source]
97
+ def self.last
98
+ Endpoint.last(:source)
99
+ end
100
+
101
+ # All input endpoints
102
+ # @return [Array<Source>]
103
+ def self.all
104
+ Endpoint.all_by_type[:source]
105
+ end
106
+
107
+ protected
108
+
109
+ # Base initialization for this endpoint -- done whether or not the endpoint is enabled to check whether
110
+ # it is truly available for use
111
+ def connect
112
+ enable_client
113
+ initialize_port
114
+ @resource = API.MIDIEntityGetSource(@entity.resource, @resource_id)
115
+ error = API.MIDIPortConnectSource(@handle, @resource, nil )
116
+ initialize_buffer
117
+ # SINM ADDED
118
+ @queue = Queue.new
119
+
120
+ @sysex_buffer = []
121
+ @start_time = Time.now.to_f
122
+
123
+ error.zero?
124
+ end
125
+ alias_method :connect?, :connect
126
+
127
+ private
128
+
129
+ # Add a single message to the buffer
130
+ # @param [Array<Fixnum>] bytes Message data
131
+ # @param [Float] timestamp The system float timestamp
132
+ # @return [Array<Hash>] The resulting buffer
133
+ def enqueue_message(bytes, timestamp)
134
+ if bytes.first.eql?(0xF0) || !@sysex_buffer.empty?
135
+ @sysex_buffer += bytes
136
+ if bytes.last.eql?(0xF7)
137
+ bytes = @sysex_buffer.dup
138
+ @sysex_buffer.clear
139
+ end
140
+ end
141
+ # SINM ADDED
142
+ @sysex_buffer.empty? ? get_message_formatted(bytes, timestamp) : nil
143
+ # SINM COMMENTED OUT
144
+ # @buffer << get_message_formatted(bytes, timestamp) if @sysex_buffer.empty?
145
+ # @buffer
146
+ end
147
+
148
+ # New MIDI messages from the queue
149
+ def queued_messages
150
+ @buffer.slice(@pointer, @buffer.length - @pointer)
151
+ end
152
+
153
+ # Are there new MIDI messages in the queue?
154
+ def queued_messages?
155
+ @pointer < @buffer.length
156
+ end
157
+
158
+ # The callback fired by coremidi when new MIDI messages are in the buffer
159
+ def get_event_callback
160
+ Proc.new do |new_packets, refCon_ptr, connRefCon_ptr|
161
+ begin
162
+ # p "packets received: #{new_packets[:numPackets]}"
163
+ timestamp = Time.now.to_f
164
+ messages = get_messages(new_packets)
165
+ # SINM ADDED
166
+ @queue.push(messages.map do |message|
167
+ enqueue_message(message, timestamp)
168
+ end.compact)
169
+ # SINM COMMENTED OUT
170
+ # messages.each { |message| enqueue_message(message, timestamp) }
171
+ rescue Exception => exception
172
+ Thread.main.raise(exception)
173
+ end
174
+ end
175
+ end
176
+
177
+ # Get MIDI messages from the given CoreMIDI packet list
178
+ # @param [API::MIDIPacketList] new_packets The packet list
179
+ # @return [Array<Array<Fixnum>>] A collection of MIDI messages
180
+ def get_messages(packet_list)
181
+ count = packet_list[:numPackets]
182
+ first = packet_list[:packet][0]
183
+ data = first[:data].to_a
184
+ messages = []
185
+ messages << data.slice!(0, first[:length])
186
+ (count - 1).times do |i|
187
+ length_index = find_next_length_index(data)
188
+ message_length = data[length_index]
189
+ unless message_length.nil?
190
+ packet_start_index = length_index + 2
191
+ packet_end_index = packet_start_index + message_length
192
+ if data.length >= packet_end_index + 1
193
+ packet = data.slice!(0..packet_end_index)
194
+ message = packet.slice(packet_start_index, message_length)
195
+ messages << message
196
+ end
197
+ end
198
+ end
199
+ messages
200
+ end
201
+
202
+ # Get the next index for "length" from the blob of MIDI data
203
+ # @param [Array<Fixnum>] data
204
+ # @return [Fixnum]
205
+ def find_next_length_index(data)
206
+ last_is_zero = false
207
+ data.each_with_index do |num, i|
208
+ if num.zero?
209
+ if last_is_zero
210
+ return i + 1
211
+ else
212
+ last_is_zero = true
213
+ end
214
+ else
215
+ last_is_zero = false
216
+ end
217
+ end
218
+ end
219
+
220
+ # Timestamp for a received MIDI message
221
+ # @return [Fixnum]
222
+ def timestamp(now)
223
+ (now - @start_time) * 1000
224
+ end
225
+
226
+ # Give a message its timestamp and package it in a Hash
227
+ # @return [Hash]
228
+ def get_message_formatted(raw, time)
229
+ {
230
+ :data => raw,
231
+ :timestamp => timestamp(time)
232
+ }
233
+ end
234
+
235
+ # Initialize a coremidi port for this endpoint
236
+ # @return [Boolean]
237
+ def initialize_port
238
+ @callback = get_event_callback
239
+ port = API.create_midi_input_port(@client, @resource_id, @name, @callback)
240
+ @handle = port[:handle]
241
+ raise "MIDIInputPortCreate returned error code #{port[:error]}" unless port[:error].zero?
242
+ true
243
+ end
244
+
245
+ # Initialize the MIDI message buffer
246
+ # @return [Boolean]
247
+ def initialize_buffer
248
+ @pointer = 0
249
+ @buffer = []
250
+ def @buffer.clear
251
+ super
252
+ @pointer = 0
253
+ end
254
+ true
255
+ end
256
+
257
+ end
258
+
259
+ end
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ module Shmidi
3
+ class LedButton < Control
4
+ CTYPE = :LEDBUT
5
+ attr_reader :button, :led
6
+
7
+ def initialize(id, socket, channel, note, led_note = nil)
8
+ super(id, socket, channel, note)
9
+ @button = Button.new(id, socket, channel, note)
10
+ @led = Led.new(id, socket, channel, led_note || note)
11
+ @button.on_press(&lambda { |button| on_button_press(button) })
12
+ @button.on_release(&lambda { |button| on_button_release(button) })
13
+ end
14
+
15
+ protected
16
+
17
+ def on_button_press(button)
18
+ @led.turn_on
19
+ end
20
+
21
+ def on_button_release(button)
22
+ @led.turn_off
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ module Shmidi
3
+ class OnOffClock < Clock
4
+ def initialize(socket, delay = 0.2, delay_off = 0.2)
5
+ @next_on = false
6
+ super(socket, delay)
7
+ @delay_off = delay_off
8
+ end
9
+
10
+ protected
11
+
12
+ def filter
13
+ b = @buffer
14
+ @buffer = []
15
+ bb = b.select { |e| e.message == (@next_on ? :on : :off) }
16
+ b = b - bb
17
+ @buffer = @buffer + b
18
+ bb
19
+ end
20
+
21
+ def wait
22
+ d = @next_on ? @delay : @delay_off
23
+ @next_on = !@next_on
24
+ sleep(d)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,78 @@
1
+ # coding: utf-8
2
+ module Shmidi
3
+ class Socket
4
+ include Base
5
+
6
+ @@controllers = {}
7
+ def self.[](name)
8
+ @@controllers[name]
9
+ end
10
+
11
+ attr_reader :name, :in, :out
12
+
13
+ def initialize(name, in_id, out_id)
14
+ @name = name
15
+ @in = in_id
16
+ @out = out_id
17
+ init
18
+ end
19
+
20
+ def init
21
+ @__in_dev = UniMIDI::Input.all.find { |d| d.name == @in }
22
+ @__out_dev = UniMIDI::Output.all.find { |d| d.name == @out }
23
+
24
+ @@controllers[@name] = self
25
+ @__queue = Queue.new
26
+ @__on_event = []
27
+ # @__sync_threads = Hash.new { |hash, key| hash[key] = Clock.new(key, self) }
28
+ @__listener = Thread.new do
29
+ loop do
30
+ break unless @__in_dev
31
+ begin
32
+ @__in_dev.gets.each do |event|
33
+ event[:source] = @name
34
+ event = Event.new(event)
35
+ Shmidi.TRACE_INTERNAL("> #{@name}\t#{event}")
36
+ @__queue.push(event)
37
+ @__on_event.each do |rule|
38
+ next if (channel = rule[:channel]) && channel != event.channel
39
+ next if (message = rule[:message]) && message != event.message
40
+ next if (note = rule[:note]) && note != event.note
41
+ next if (value = rule[:value]) && value != event.value
42
+
43
+ rule[:block].call(event) rescue Shmidi.ON_EXCEPTION
44
+ end
45
+ end
46
+ rescue
47
+ Shmidi.ON_EXCEPTION
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ def on_event(channel = nil, message = nil, note = nil, value = nil, &block)
54
+ @__on_event << {
55
+ :block => block,
56
+ :channel => channel,
57
+ :message => message,
58
+ :note => note
59
+ }
60
+ end
61
+
62
+ def push(events)
63
+ events = Array(events).reduce([]) do |array, event|
64
+ Shmidi.TRACE_EXTERNAL("< #{@name}\t#{event}")
65
+ array << event.data
66
+ array
67
+ end
68
+ @__out_dev.puts(*events)
69
+ end
70
+
71
+ def self.print_device_list
72
+ $stdout.puts('Inputs:')
73
+ UniMIDI::Input.list
74
+ $stdout.puts('Outputs:')
75
+ UniMIDI::Output.list
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ module Shmidi
3
+ class Switch < LedButton
4
+ CTYPE = :SWI
5
+ attr_reader :switch_state
6
+ def initialize(id, socket, channel, note, led_note = nil)
7
+ super(id, socket, channel, note, led_note)
8
+ @switch_state = false
9
+ @on_switch = []
10
+ end
11
+
12
+ def on_switch_state(&block)
13
+ @on_switch << block
14
+ end
15
+
16
+ protected
17
+
18
+ def on_button_press(button)
19
+ (@switch_state = !@switch_state) ? @led.turn_on : @led.turn_off
20
+ Shmidi.TRACE("#{CTYPE}\t#{@id}\tSTATE\t#{@switch_state ? :ON : :OFF}")
21
+ @on_switch.each { |b| b.call(self) }
22
+ end
23
+
24
+ def on_button_release(button)
25
+ nil
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ # coding: utf-8
2
+
3
+ module Shmidi
4
+ VERSION = '0.1'
5
+ end
data/lib/shmidi.rb ADDED
@@ -0,0 +1,100 @@
1
+ # coding: utf-8
2
+ require 'shmidi/version'
3
+
4
+ require 'timeout'
5
+
6
+ require 'unimidi'
7
+ require 'shmidi/ffi-coremidi-patch'
8
+
9
+ module Shmidi
10
+ PROFILE = !!ENV['PROFILE']
11
+ TRACE = !!ENV['TRACE']
12
+ TRACE_EXTERNAL = !!ENV['TRACE_EXTERNAL']
13
+ TRACE_INTERNAL = !!ENV['TRACE_INTERNAL']
14
+
15
+ def self.timestamp
16
+ t = Time.now
17
+ (t.to_i * 1000) + (t.usec / 1000)
18
+ end
19
+
20
+ @@trace_queue = Queue.new
21
+ @@trace_thread = Thread.new do
22
+ loop do
23
+ begin
24
+ str = @@trace_queue.pop
25
+ $stderr.puts("#{timestamp}\t#{str}")
26
+ rescue
27
+ $stderr.puts("Error in trace thread: #{$!}")
28
+ end
29
+ end
30
+ end
31
+
32
+ def self.TRACE(str)
33
+ return nil unless TRACE
34
+ @@trace_queue.push(str)
35
+ end
36
+
37
+ def self.TRACE_EXTERNAL(str)
38
+ return nil unless TRACE_EXTERNAL
39
+ @@trace_queue.push(str)
40
+ end
41
+
42
+ def self.TRACE_INTERNAL(str)
43
+ return nil unless TRACE_INTERNAL
44
+ @@trace_queue.push(str)
45
+ end
46
+
47
+ def self.ON_EXCEPTION
48
+ back = $!.backtrace.join("\n\t\t")
49
+ @@trace_queue.push("ERROR\t#{$!.class.name}:#{$!}\n\t\t#{back}")
50
+ end
51
+
52
+ require 'oj'
53
+ JSON_CREATE_ID = 'm_type'
54
+ ::Oj.default_options = {
55
+ :mode => :compat,
56
+ :class_cache => true,
57
+ :create_id => JSON_CREATE_ID,
58
+ :time => :unix
59
+ }
60
+
61
+ def self.DUMP(obj, opts={})
62
+ Oj.dump((obj.kind_of?(Base) ? obj.to_hash : obj), opts)
63
+ end
64
+
65
+ def self.JSON_PARSE str, opts = {:warn => true}
66
+ return nil if str.nil?
67
+ Oj.load(str, opts)
68
+ rescue
69
+ if opts[:warn]
70
+ Shmidi::ON_EXCEPTION
71
+ TRACE "#{str}".force_encoding(Encoding::UTF_8)
72
+ end
73
+ nil
74
+ end
75
+
76
+ def self.PRETTY obj
77
+ DUMP((obj.kind_of?(String) ? JSON_PARSE(obj) : obj), :indent=>2)
78
+ end
79
+ end
80
+
81
+ require 'shmidi/base'
82
+
83
+ require 'shmidi/socket'
84
+ require 'shmidi/event'
85
+ require 'shmidi/controller'
86
+
87
+ require 'shmidi/clock'
88
+ require 'shmidi/on_off_clock'
89
+
90
+ require 'shmidi/control'
91
+ require 'shmidi/controls/led'
92
+ require 'shmidi/composite/rgy_led'
93
+ require 'shmidi/controls/knob'
94
+ require 'shmidi/controls/fader'
95
+ require 'shmidi/controls/encoder'
96
+ require 'shmidi/controls/button'
97
+ require 'shmidi/led_button'
98
+ require 'shmidi/switch'
99
+
100
+
data/shmidi.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ require File.expand_path('../lib/shmidi/version.rb', __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'shmidi'
6
+ s.version = Shmidi::VERSION
7
+ s.authors = ['sinm']
8
+ s.email = 'sinm.sinm@gmail.com'
9
+ s.summary = 'Midi experiments'
10
+ s.description = '== Midi experiments'
11
+ s.homepage = 'https://github.com/sinm/shmidi'
12
+ s.license = 'MIT'
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = `git ls-files -- spec/`.split("\n")
15
+ s.require_paths = ['lib']
16
+ s.bindir = 'bin'
17
+ s.executables = `git ls-files -- bin/`.split("\n").map{|f| File.basename(f)}
18
+ s.add_development_dependency 'bundler', '~> 1.7'
19
+ s.add_development_dependency 'rake', '~> 10.1'
20
+ s.add_development_dependency 'minitest', '~> 4.7'
21
+ # NOTE: it's useful to install
22
+ # specified version of perftools.rb w/o bundler first
23
+ s.add_development_dependency 'perftools.rb', '~> 2.0.4'
24
+ s.add_dependency 'ffi-coremidi', '0.3.8'
25
+ s.add_dependency 'unimidi', '0.4.6'
26
+ s.add_dependency 'argparser', '~> 2.1'
27
+ s.add_dependency 'oj', '~> 2.12.12'
28
+ end
@@ -0,0 +1,30 @@
1
+ {
2
+ "folders":
3
+ [
4
+ {
5
+ "path": "."
6
+ },
7
+ {
8
+ "path": "~/.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems"
9
+ }
10
+ ],
11
+ "build_systems":
12
+ [
13
+ {
14
+ "name": "Run tests",
15
+ "cmd": ["bundle", "exec", "rake", "test"],
16
+ "selector": "source.rb"
17
+ }
18
+ ],
19
+ "settings":
20
+ {
21
+ "ensure_newline_at_eof_on_save": true,
22
+ "rulers":
23
+ [
24
+ 80
25
+ ],
26
+ "tab_size": 2,
27
+ "translate_tabs_to_spaces": true,
28
+ "trim_trailing_white_space_on_save": true
29
+ }
30
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "m_type":"Shmidi::Controller",
3
+ "name":"xonek1-ledtest",
4
+ "internals":[
5
+ [
6
+ {
7
+ "m_type":"Shmidi::Socket",
8
+ "name":"xone",
9
+ "in":"ALLEN&HEATH LTD. XONE:K1",
10
+ "out":"ALLEN&HEATH LTD. XONE:K1"
11
+ }
12
+ ],
13
+ {
14
+ "led":{
15
+ "m_type":"Shmidi::RGYLed",
16
+ "leds":{
17
+ "red":{
18
+ "m_type":"Shmidi::Led",
19
+ "socket":"xone",
20
+ "channel":15,
21
+ "note":"C0",
22
+ "turned_on":false
23
+ },
24
+ "green":{
25
+ "m_type":"Shmidi::Led",
26
+ "socket":"xone",
27
+ "channel":15,
28
+ "note":"G#0",
29
+ "turned_on":false
30
+ },
31
+ "yellow":{
32
+ "m_type":"Shmidi::Led",
33
+ "socket":"xone",
34
+ "channel":15,
35
+ "note":"E0",
36
+ "turned_on":false
37
+ }
38
+ }
39
+ }
40
+ }
41
+ ]
42
+ }