xaviershay-rbcoremidi 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +21 -0
- data/README.rdoc +10 -0
- data/Rakefile +57 -0
- data/examples/example.rb +20 -0
- data/ext/extconf.rb +5 -0
- data/ext/rbcoremidi.c +391 -0
- data/lib/coremidi.rb +68 -0
- data/spec/parsing_spec.rb +41 -0
- data/spec/spec_helper.rb +5 -0
- metadata +63 -0
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2008 Markus Prinz
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
Provide easy access to CoreMIDI for Ruby.
|
2
|
+
|
3
|
+
Please note that this is a work in progress. The code isn't tested and for all I know,
|
4
|
+
it sets your house on fire, steals your car and runs over your dog and/or cat. So proceed
|
5
|
+
with caution.
|
6
|
+
|
7
|
+
== Installation
|
8
|
+
|
9
|
+
$ gem sources -a http://gems.github.com
|
10
|
+
$ sudo gem install xaviershay-rbcoremidi
|
data/Rakefile
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
require 'rubygems/specification'
|
4
|
+
require 'date'
|
5
|
+
require 'spec/rake/spectask'
|
6
|
+
|
7
|
+
GEM = "rbcoremidi"
|
8
|
+
GEM_VERSION = "0.0.1"
|
9
|
+
AUTHOR = "Your Name"
|
10
|
+
EMAIL = "Your Email"
|
11
|
+
HOMEPAGE = "http://example.com"
|
12
|
+
SUMMARY = "A gem that provides..."
|
13
|
+
|
14
|
+
spec = Gem::Specification.new do |s|
|
15
|
+
s.name = GEM
|
16
|
+
s.version = GEM_VERSION
|
17
|
+
s.platform = Gem::Platform::RUBY
|
18
|
+
s.has_rdoc = true
|
19
|
+
s.extra_rdoc_files = ['README.rdoc', 'LICENSE']
|
20
|
+
s.summary = SUMMARY
|
21
|
+
s.description = s.summary
|
22
|
+
s.author = AUTHOR
|
23
|
+
s.email = EMAIL
|
24
|
+
s.homepage = HOMEPAGE
|
25
|
+
|
26
|
+
# Uncomment this to add a dependency
|
27
|
+
# s.add_dependency "foo"
|
28
|
+
|
29
|
+
s.require_path = 'lib'
|
30
|
+
s.extensions = ['ext/extconf.rb']
|
31
|
+
s.files = %w(LICENSE README.rdoc Rakefile) + Dir.glob("{examples,ext,lib,spec}/**/*")
|
32
|
+
end
|
33
|
+
|
34
|
+
task :default => :spec
|
35
|
+
|
36
|
+
desc "Run specs"
|
37
|
+
Spec::Rake::SpecTask.new do |t|
|
38
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
39
|
+
t.spec_opts = %w(-fs --color)
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
44
|
+
pkg.gem_spec = spec
|
45
|
+
end
|
46
|
+
|
47
|
+
desc "install the gem locally"
|
48
|
+
task :install => [:package] do
|
49
|
+
sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION}}
|
50
|
+
end
|
51
|
+
|
52
|
+
desc "create a gemspec file"
|
53
|
+
task :make_spec do
|
54
|
+
File.open("#{GEM}.gemspec", "w") do |file|
|
55
|
+
file.puts spec.to_ruby
|
56
|
+
end
|
57
|
+
end
|
data/examples/example.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'coremidi'
|
2
|
+
|
3
|
+
# Start archaeopteryx
|
4
|
+
# Start GarageBand (just to make sure it's all working)
|
5
|
+
|
6
|
+
# Open MIDI Patch Bay.app
|
7
|
+
# Create a new input (anyname)
|
8
|
+
# Create a new output (anyname)
|
9
|
+
# GarageBand will announce that it has found a new input
|
10
|
+
# You should have sound, yay
|
11
|
+
|
12
|
+
# Now run this script
|
13
|
+
|
14
|
+
midi_thread = Thread.new do
|
15
|
+
CoreMIDI::Input.register("Test", "Test", "Out") do |event|
|
16
|
+
puts event.inspect
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
gets # Stop on enter
|
data/ext/extconf.rb
ADDED
data/ext/rbcoremidi.c
ADDED
@@ -0,0 +1,391 @@
|
|
1
|
+
/*
|
2
|
+
* Copyright 2008 Markus Prinz
|
3
|
+
* Released unter an MIT licence
|
4
|
+
*
|
5
|
+
*/
|
6
|
+
|
7
|
+
#include <ruby.h>
|
8
|
+
#include <CoreMIDI/MIDIServices.h>
|
9
|
+
#include <pthread.h>
|
10
|
+
#include <stdlib.h>
|
11
|
+
|
12
|
+
pthread_mutex_t mutex;
|
13
|
+
|
14
|
+
CFMutableArrayRef midi_data = NULL;
|
15
|
+
|
16
|
+
VALUE mCoreMIDI = Qnil;
|
17
|
+
VALUE mCoreMIDIAPI = Qnil;
|
18
|
+
|
19
|
+
VALUE cInputPort = Qnil;
|
20
|
+
VALUE cMIDIClient = Qnil;
|
21
|
+
|
22
|
+
// We need our own data structure since MIDIPacket defines data to be a 256 byte array,
|
23
|
+
// even though it can be larger than that
|
24
|
+
typedef struct RbMIDIPacket_t {
|
25
|
+
MIDITimeStamp timeStamp;
|
26
|
+
UInt16 length;
|
27
|
+
Byte* data;
|
28
|
+
} RbMIDIPacket;
|
29
|
+
|
30
|
+
// A struct that contains the input port
|
31
|
+
typedef struct RbInputPort_t {
|
32
|
+
MIDIPortRef input_port;
|
33
|
+
} RbInputPort;
|
34
|
+
|
35
|
+
// A struct that contains the midi client
|
36
|
+
typedef struct RbMIDIClient_t {
|
37
|
+
MIDIClientRef client;
|
38
|
+
} RbMIDIClient;
|
39
|
+
|
40
|
+
// Forward declare free_objects
|
41
|
+
static void free_objects();
|
42
|
+
|
43
|
+
// The callback function that we'll eventually supply to MIDIInputPortCreate
|
44
|
+
static void RbMIDIReadProc(const MIDIPacketList* packetList, void* readProcRefCon, void* srcConnRefCon)
|
45
|
+
{
|
46
|
+
if( pthread_mutex_lock(&mutex) != 0 )
|
47
|
+
{
|
48
|
+
// uh oh
|
49
|
+
// Not much we can do
|
50
|
+
return;
|
51
|
+
}
|
52
|
+
|
53
|
+
MIDIPacket* current_packet = (MIDIPacket*) packetList->packet;
|
54
|
+
|
55
|
+
unsigned int j;
|
56
|
+
for( j = 0; j < packetList->numPackets; ++j )
|
57
|
+
{
|
58
|
+
RbMIDIPacket* rb_packet = (RbMIDIPacket*) malloc( sizeof(RbMIDIPacket) );
|
59
|
+
|
60
|
+
if( rb_packet == NULL )
|
61
|
+
{
|
62
|
+
fprintf(stderr, "Failed to allocate memory for RbMIDIPacket!\n");
|
63
|
+
abort();
|
64
|
+
}
|
65
|
+
|
66
|
+
rb_packet->timeStamp = current_packet->timeStamp;
|
67
|
+
rb_packet->length = current_packet->length;
|
68
|
+
|
69
|
+
size_t size = sizeof(Byte) * rb_packet->length;
|
70
|
+
rb_packet->data = (Byte*) malloc( size );
|
71
|
+
|
72
|
+
if( rb_packet->data == NULL )
|
73
|
+
{
|
74
|
+
fprintf(stderr, "Failed to allocate memory for RbMIDIPacket data!\n");
|
75
|
+
abort();
|
76
|
+
}
|
77
|
+
|
78
|
+
memcpy(rb_packet->data, current_packet->data, size);
|
79
|
+
|
80
|
+
CFArrayAppendValue(midi_data, rb_packet);
|
81
|
+
|
82
|
+
current_packet = MIDIPacketNext(current_packet);
|
83
|
+
}
|
84
|
+
|
85
|
+
pthread_mutex_unlock(&mutex);
|
86
|
+
}
|
87
|
+
|
88
|
+
// Checks for new data and copies it over if there is some.
|
89
|
+
static VALUE t_check_for_new_data(VALUE self)
|
90
|
+
{
|
91
|
+
if( pthread_mutex_trylock(&mutex) != 0 )
|
92
|
+
{
|
93
|
+
// no data for us yet
|
94
|
+
return Qfalse;
|
95
|
+
}
|
96
|
+
|
97
|
+
// Switch out the arrays. Possibly evil
|
98
|
+
CFArrayRef data = midi_data;
|
99
|
+
midi_data = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
|
100
|
+
|
101
|
+
pthread_mutex_unlock(&mutex);
|
102
|
+
|
103
|
+
// We'll use a Ruby Struct to store the data
|
104
|
+
VALUE cMidiPacket = rb_const_get(mCoreMIDIAPI, rb_intern("MidiPacket"));
|
105
|
+
|
106
|
+
VALUE rb_midi_data = rb_ary_new();
|
107
|
+
|
108
|
+
CFIndex idx = 0;
|
109
|
+
CFIndex array_size = CFArrayGetCount(data);
|
110
|
+
const RbMIDIPacket* current_packet = NULL;
|
111
|
+
|
112
|
+
for( ; idx < array_size; ++idx )
|
113
|
+
{
|
114
|
+
current_packet = (const RbMIDIPacket*) CFArrayGetValueAtIndex(data, idx);
|
115
|
+
|
116
|
+
VALUE byte_array = rb_ary_new2(current_packet->length);
|
117
|
+
|
118
|
+
int i;
|
119
|
+
for (i = 0; i < current_packet->length; ++i)
|
120
|
+
{
|
121
|
+
rb_ary_push(byte_array, INT2FIX(current_packet->data[i]));
|
122
|
+
}
|
123
|
+
|
124
|
+
// relies on sizeof(MIDITimeStamp) == sizeof(unsigned long long)
|
125
|
+
assert(sizeof(MIDITimeStamp) == sizeof(unsigned long long));
|
126
|
+
|
127
|
+
VALUE midi_packet_args[2];
|
128
|
+
midi_packet_args[0] = ULL2NUM(current_packet->timeStamp);
|
129
|
+
midi_packet_args[1] = byte_array;
|
130
|
+
|
131
|
+
rb_ary_push(rb_midi_data, rb_class_new_instance((sizeof(midi_packet_args)/sizeof(midi_packet_args[0])), midi_packet_args, cMidiPacket));
|
132
|
+
|
133
|
+
// While we're at it..
|
134
|
+
// Free the memory! Save the whales!
|
135
|
+
free(current_packet->data);
|
136
|
+
}
|
137
|
+
|
138
|
+
// Free the memory! Save the whales! Part 2!
|
139
|
+
CFRelease(data);
|
140
|
+
|
141
|
+
return rb_midi_data;
|
142
|
+
}
|
143
|
+
|
144
|
+
static VALUE t_create_client(VALUE self, VALUE client_name)
|
145
|
+
{
|
146
|
+
VALUE midiclient_instance = rb_class_new_instance(0, 0, cMIDIClient);
|
147
|
+
if( midiclient_instance == Qnil )
|
148
|
+
{
|
149
|
+
free_objects();
|
150
|
+
rb_fatal("Couldn't create an instance of MIDIClient!");
|
151
|
+
}
|
152
|
+
|
153
|
+
MIDIClientRef midi_client;
|
154
|
+
|
155
|
+
CFStringRef client_str = CFStringCreateWithCString(kCFAllocatorDefault, RSTRING(client_name)->ptr, kCFStringEncodingASCII);
|
156
|
+
MIDIClientCreate(client_str, NULL, NULL, &midi_client);
|
157
|
+
CFRelease(client_str);
|
158
|
+
|
159
|
+
RbMIDIClient* client_struct;
|
160
|
+
Data_Get_Struct(midiclient_instance, RbMIDIClient, client_struct);
|
161
|
+
|
162
|
+
client_struct->client = midi_client;
|
163
|
+
|
164
|
+
return midiclient_instance;
|
165
|
+
}
|
166
|
+
|
167
|
+
// Create a new Input Port and saves the Ruby Callback proc.
|
168
|
+
static VALUE t_create_input_port(VALUE self, VALUE client_instance, VALUE port_name)
|
169
|
+
{
|
170
|
+
MIDIPortRef in_port;
|
171
|
+
|
172
|
+
RbMIDIClient* client;
|
173
|
+
Data_Get_Struct(client_instance, RbMIDIClient, client);
|
174
|
+
|
175
|
+
CFStringRef port_str = CFStringCreateWithCString(kCFAllocatorDefault, RSTRING(port_name)->ptr, kCFStringEncodingASCII);
|
176
|
+
MIDIInputPortCreate(client->client, port_str, RbMIDIReadProc, NULL, &in_port);
|
177
|
+
CFRelease(port_str);
|
178
|
+
|
179
|
+
VALUE inputport_instance = rb_class_new_instance(0, 0, cInputPort);
|
180
|
+
if( inputport_instance == Qnil )
|
181
|
+
{
|
182
|
+
free_objects();
|
183
|
+
rb_fatal("Couldn't create an instance of InputPort!");
|
184
|
+
}
|
185
|
+
|
186
|
+
RbInputPort* port_struct;
|
187
|
+
Data_Get_Struct(inputport_instance, RbInputPort, port_struct);
|
188
|
+
|
189
|
+
port_struct->input_port = in_port;
|
190
|
+
|
191
|
+
return inputport_instance;
|
192
|
+
}
|
193
|
+
|
194
|
+
// Return an array of all available sources, filled with the names of the sources
|
195
|
+
static VALUE t_get_sources(VALUE self)
|
196
|
+
{
|
197
|
+
int number_of_sources = MIDIGetNumberOfSources();
|
198
|
+
|
199
|
+
VALUE source_ary = rb_ary_new2(number_of_sources);
|
200
|
+
|
201
|
+
int idx;
|
202
|
+
for(idx = 0; idx < number_of_sources; ++idx)
|
203
|
+
{
|
204
|
+
MIDIEndpointRef src = MIDIGetSource(idx);
|
205
|
+
CFStringRef pname;
|
206
|
+
char name[64];
|
207
|
+
|
208
|
+
MIDIObjectGetStringProperty(src, kMIDIPropertyName, &pname);
|
209
|
+
CFStringGetCString(pname, name, sizeof(name), 0);
|
210
|
+
CFRelease(pname);
|
211
|
+
|
212
|
+
rb_ary_push(source_ary, rb_str_new2(name));
|
213
|
+
}
|
214
|
+
|
215
|
+
return source_ary;
|
216
|
+
}
|
217
|
+
|
218
|
+
static VALUE t_get_num_sources(VALUE self)
|
219
|
+
{
|
220
|
+
return INT2FIX(MIDIGetNumberOfSources());
|
221
|
+
}
|
222
|
+
|
223
|
+
// source is identified by the index in the array returned by get_sources
|
224
|
+
// input_port is an InputPort class
|
225
|
+
static VALUE t_connect_source_to_port(VALUE self, VALUE source_idx, VALUE input_port)
|
226
|
+
{
|
227
|
+
RbInputPort* port;
|
228
|
+
Data_Get_Struct(input_port, RbInputPort, port);
|
229
|
+
|
230
|
+
MIDIEndpointRef source = MIDIGetSource(FIX2INT(source_idx));
|
231
|
+
|
232
|
+
MIDIPortConnectSource(port->input_port, source, NULL);
|
233
|
+
|
234
|
+
return Qtrue;
|
235
|
+
}
|
236
|
+
|
237
|
+
// source is identified by the index in the array returned by get_sources
|
238
|
+
// input_port is an InputPort class
|
239
|
+
static VALUE t_disconnect_source_from_port(VALUE self, VALUE source_idx, VALUE input_port)
|
240
|
+
{
|
241
|
+
RbInputPort* port;
|
242
|
+
Data_Get_Struct(input_port, RbInputPort, port);
|
243
|
+
|
244
|
+
MIDIEndpointRef source = MIDIGetSource(FIX2INT(source_idx));
|
245
|
+
|
246
|
+
MIDIPortDisconnectSource(port->input_port, source);
|
247
|
+
|
248
|
+
return Qtrue;
|
249
|
+
}
|
250
|
+
|
251
|
+
/*
|
252
|
+
*
|
253
|
+
* RbInputPort related methods
|
254
|
+
*
|
255
|
+
*/
|
256
|
+
|
257
|
+
static void inputport_free(void* ptr)
|
258
|
+
{
|
259
|
+
if( ptr != NULL)
|
260
|
+
free(ptr);
|
261
|
+
}
|
262
|
+
|
263
|
+
static VALUE inputport_alloc(VALUE klass)
|
264
|
+
{
|
265
|
+
RbInputPort* port = (RbInputPort*) malloc(sizeof(RbInputPort));
|
266
|
+
port->input_port = NULL;
|
267
|
+
|
268
|
+
VALUE obj;
|
269
|
+
obj = Data_Wrap_Struct(klass, 0, inputport_free, port);
|
270
|
+
|
271
|
+
return obj;
|
272
|
+
}
|
273
|
+
|
274
|
+
static VALUE inputport_initialize(VALUE self)
|
275
|
+
{
|
276
|
+
return self;
|
277
|
+
}
|
278
|
+
|
279
|
+
/*
|
280
|
+
*
|
281
|
+
* RbMIDIClient related methods
|
282
|
+
*
|
283
|
+
*/
|
284
|
+
|
285
|
+
static void midiclient_free(void* ptr)
|
286
|
+
{
|
287
|
+
if( ptr != NULL)
|
288
|
+
free(ptr);
|
289
|
+
}
|
290
|
+
|
291
|
+
static VALUE midiclient_alloc(VALUE klass)
|
292
|
+
{
|
293
|
+
RbMIDIClient* client = (RbMIDIClient*) malloc(sizeof(RbMIDIClient));
|
294
|
+
client->client = NULL;
|
295
|
+
|
296
|
+
VALUE obj;
|
297
|
+
obj = Data_Wrap_Struct(klass, 0, midiclient_free, client);
|
298
|
+
|
299
|
+
return obj;
|
300
|
+
}
|
301
|
+
|
302
|
+
static VALUE midiclient_initialize(VALUE self)
|
303
|
+
{
|
304
|
+
return self;
|
305
|
+
}
|
306
|
+
|
307
|
+
/*
|
308
|
+
*
|
309
|
+
* util methods
|
310
|
+
*
|
311
|
+
*/
|
312
|
+
|
313
|
+
static void free_objects()
|
314
|
+
{
|
315
|
+
pthread_mutex_destroy(&mutex);
|
316
|
+
|
317
|
+
if( midi_data != NULL )
|
318
|
+
{
|
319
|
+
if( CFArrayGetCount(midi_data) > 0 )
|
320
|
+
{
|
321
|
+
int i;
|
322
|
+
for( i = 0; i < CFArrayGetCount(midi_data); ++i )
|
323
|
+
{
|
324
|
+
free(((const RbMIDIPacket*) CFArrayGetValueAtIndex(midi_data, i))->data);
|
325
|
+
}
|
326
|
+
}
|
327
|
+
|
328
|
+
CFRelease(midi_data);
|
329
|
+
}
|
330
|
+
}
|
331
|
+
|
332
|
+
static void init_mutex()
|
333
|
+
{
|
334
|
+
int mutex_init_result = pthread_mutex_init(&mutex, NULL);
|
335
|
+
|
336
|
+
if( mutex_init_result != 0 )
|
337
|
+
{
|
338
|
+
rb_sys_fail("Failed to allocate mutex");
|
339
|
+
}
|
340
|
+
}
|
341
|
+
|
342
|
+
static void init_midi_data()
|
343
|
+
{
|
344
|
+
midi_data = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
|
345
|
+
|
346
|
+
if( midi_data == NULL )
|
347
|
+
{
|
348
|
+
free_objects();
|
349
|
+
rb_sys_fail("Failed to allocate CFMutableArray");
|
350
|
+
}
|
351
|
+
}
|
352
|
+
|
353
|
+
static void install_at_exit_handler()
|
354
|
+
{
|
355
|
+
// Poor Ruby programmers destructor
|
356
|
+
if( atexit(free_objects) != 0 )
|
357
|
+
{
|
358
|
+
free_objects();
|
359
|
+
rb_sys_fail("Failed to register atexit function");
|
360
|
+
}
|
361
|
+
}
|
362
|
+
|
363
|
+
void Init_rbcoremidi()
|
364
|
+
{
|
365
|
+
init_mutex();
|
366
|
+
|
367
|
+
init_midi_data();
|
368
|
+
|
369
|
+
install_at_exit_handler();
|
370
|
+
|
371
|
+
mCoreMIDI = rb_define_module("CoreMIDI");
|
372
|
+
mCoreMIDIAPI = rb_define_module_under(mCoreMIDI, "API");
|
373
|
+
|
374
|
+
rb_define_singleton_method(mCoreMIDIAPI, "create_input_port", t_create_input_port, 2);
|
375
|
+
rb_define_singleton_method(mCoreMIDIAPI, "create_client", t_create_client, 1);
|
376
|
+
rb_define_singleton_method(mCoreMIDIAPI, "get_sources", t_get_sources, 0);
|
377
|
+
rb_define_singleton_method(mCoreMIDIAPI, "get_num_sources", t_get_num_sources, 0);
|
378
|
+
rb_define_singleton_method(mCoreMIDIAPI, "connect_source_to_port", t_connect_source_to_port, 2);
|
379
|
+
rb_define_singleton_method(mCoreMIDIAPI, "disconnect_source_from_port", t_disconnect_source_from_port, 2);
|
380
|
+
rb_define_singleton_method(mCoreMIDIAPI, "check_for_new_data", t_check_for_new_data, 0);
|
381
|
+
|
382
|
+
// Define CoreMIDI::API::InputPort class
|
383
|
+
cInputPort = rb_define_class_under(mCoreMIDIAPI, "InputPort", rb_cObject);
|
384
|
+
rb_define_alloc_func(cInputPort, inputport_alloc);
|
385
|
+
rb_define_method(cInputPort, "initialize", inputport_initialize, 0);
|
386
|
+
|
387
|
+
// Define CoreMIDI::API::MIDIClient class
|
388
|
+
cMIDIClient = rb_define_class_under(mCoreMIDIAPI, "MIDIClient", rb_cObject);
|
389
|
+
rb_define_alloc_func(cMIDIClient, midiclient_alloc);
|
390
|
+
rb_define_method(cMIDIClient, "initialize", midiclient_initialize, 0);
|
391
|
+
}
|
data/lib/coremidi.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../ext/rbcoremidi.bundle'
|
2
|
+
|
3
|
+
module CoreMIDI
|
4
|
+
module API
|
5
|
+
MidiPacket = Struct.new(:timestamp, :data)
|
6
|
+
end
|
7
|
+
|
8
|
+
# Unused, but left here for documentation
|
9
|
+
def self.number_of_sources
|
10
|
+
API.get_num_sources
|
11
|
+
end
|
12
|
+
|
13
|
+
class Packet
|
14
|
+
# http://www.srm.com/qtma/davidsmidispec.html
|
15
|
+
def self.parse(data)
|
16
|
+
spec = {
|
17
|
+
0x80 => Events::NoteOff,
|
18
|
+
0x90 => lambda {|data| (data[Events::NoteOn.members.index("velocity")] == 0) ? Events::NoteOff : Events::NoteOn },
|
19
|
+
0xA0 => Events::KeyPressure,
|
20
|
+
0xC0 => Events::ProgramChange,
|
21
|
+
0xD0 => Events::ChannelPressure
|
22
|
+
}
|
23
|
+
|
24
|
+
klass = spec.detect {|code, _|
|
25
|
+
data[0] & 0xF0 == code # First byte is the type code
|
26
|
+
}
|
27
|
+
|
28
|
+
return Events::Unknown.new(data) if klass.nil?
|
29
|
+
|
30
|
+
klass = klass.last
|
31
|
+
klass = klass.call(data) if klass.respond_to?(:call) # Resolve any lambdas into a class
|
32
|
+
|
33
|
+
klass.new(
|
34
|
+
data[0] & 0x0F, # Second byte contains the channel
|
35
|
+
*data[1..-1]
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module Events
|
41
|
+
class NoteOn < Struct.new(:channel, :pitch, :velocity); end;
|
42
|
+
class NoteOff < Struct.new(:channel, :pitch, :velocity); end;
|
43
|
+
class KeyPressure < Struct.new(:channel, :pitch, :pressure); end;
|
44
|
+
class ProgramChange < Struct.new(:channel, :preset); end;
|
45
|
+
class ChannelPressure < Struct.new(:channel, :pressure); end;
|
46
|
+
class Unknown < Struct.new(:data); end;
|
47
|
+
end
|
48
|
+
|
49
|
+
class Input
|
50
|
+
def self.register(client_name, port_name, source)
|
51
|
+
raise "name must be a String!" unless client_name.class == String
|
52
|
+
|
53
|
+
client = API.create_client(client_name)
|
54
|
+
port = API.create_input_port(client, port_name)
|
55
|
+
API.connect_source_to_port(API.get_sources.index(source), port)
|
56
|
+
|
57
|
+
while true
|
58
|
+
data = API.check_for_new_data
|
59
|
+
if data && !data.empty?
|
60
|
+
data.each do |packet|
|
61
|
+
yield(Packet.parse(packet.data))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
sleep 0.001
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe 'CoreMIDI::Packet.parse' do
|
4
|
+
def self.it_parses(data, expected)
|
5
|
+
describe "given data #{data.inspect}, creates an event" do
|
6
|
+
before(:each) do
|
7
|
+
@packet = CoreMIDI::Packet.parse(data)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "of type #{expected.class}" do
|
11
|
+
@packet.class.should == expected.class
|
12
|
+
end
|
13
|
+
|
14
|
+
expected.members.each do |member|
|
15
|
+
it "that has a #{member} of #{expected.send(member).inspect}" do
|
16
|
+
@packet.send(member).should == expected.send(member)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it_parses([0x90, 0x3C, 0x40], CoreMIDI::Events::NoteOn.new(0x00, 0x3C, 0x40)) # Channel 0, Middle C, half velocity
|
23
|
+
it_parses([0x91, 0x3C, 0x40], CoreMIDI::Events::NoteOn.new(0x01, 0x3C, 0x40)) # Channel 1, Middle C, half velocity
|
24
|
+
it_parses([0x80, 0x3C, 0x40], CoreMIDI::Events::NoteOff.new(0x00, 0x3C, 0x40)) # Channel 0, Middle C, half velocity
|
25
|
+
it_parses([0x81, 0x3C, 0x40], CoreMIDI::Events::NoteOff.new(0x01, 0x3C, 0x40)) # Channel 1, Middle C, half velocity
|
26
|
+
it_parses([0xC0, 0x01], CoreMIDI::Events::ProgramChange.new(0x00, 0x01)) # Channel 0, Preset #1
|
27
|
+
it_parses([0xC1, 0x02], CoreMIDI::Events::ProgramChange.new(0x01, 0x02)) # Channel 1, Preset #2
|
28
|
+
it_parses([0xA0, 0x3C, 0x64], CoreMIDI::Events::KeyPressure.new(0x00, 0x3C, 0x64)) # Channel 0, Middle C, half pressure
|
29
|
+
it_parses([0xA1, 0x3C, 0xFF], CoreMIDI::Events::KeyPressure.new(0x01, 0x3C, 0xFF)) # Channel 1, Middle C, full pressure
|
30
|
+
it_parses([0xD0, 0x64], CoreMIDI::Events::ChannelPressure.new(0x00, 0x64)) # Channel 0, half pressure
|
31
|
+
it_parses([0xD1, 0xFF], CoreMIDI::Events::ChannelPressure.new(0x01, 0xFF)) # Channel 1, full pressure
|
32
|
+
|
33
|
+
# This is technically a NoteOn event, but convention uses it most often in place of a note off event (setting velocity to 0)
|
34
|
+
it_parses([0x90, 0x3C, 0x00], CoreMIDI::Events::NoteOff.new(0x00, 0x3C, 0x00)) # Channel 0, Middle C, no velocity
|
35
|
+
|
36
|
+
describe 'when data does not match a known MIDI event,' do
|
37
|
+
it_parses([0xFF, 0xFF], CoreMIDI::Events::Unknown.new([0xFF, 0xFF]))
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'creates a NoteOn event when status byte was provided by the last packet'
|
41
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: xaviershay-rbcoremidi
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Xavier Shay
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-09-22 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: A gem that provides MIDI in to ruby via OSX CoreMIDI
|
17
|
+
email: contact@rhnh.net
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions:
|
21
|
+
- ext/extconf.rb
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.rdoc
|
24
|
+
- LICENSE
|
25
|
+
files:
|
26
|
+
- LICENSE
|
27
|
+
- README.rdoc
|
28
|
+
- Rakefile
|
29
|
+
- examples/example.rb
|
30
|
+
- ext/extconf.rb
|
31
|
+
- ext/rbcoremidi.c
|
32
|
+
- lib/coremidi.rb
|
33
|
+
- spec/parsing_spec.rb
|
34
|
+
- spec/spec_helper.rb
|
35
|
+
has_rdoc: false
|
36
|
+
homepage: http://github.com/xaviershay/rbcoremidi
|
37
|
+
licenses:
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: "0"
|
48
|
+
version:
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
requirements: []
|
56
|
+
|
57
|
+
rubyforge_project:
|
58
|
+
rubygems_version: 1.3.5
|
59
|
+
signing_key:
|
60
|
+
specification_version: 2
|
61
|
+
summary: A gem that provides MIDI in to ruby via OSX CoreMIDI
|
62
|
+
test_files: []
|
63
|
+
|