shmidi 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ }