burke-coremidi 0.0.5
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.
- data/LICENSE +21 -0
- data/README.rdoc +10 -0
- data/Rakefile +29 -0
- data/VERSION.yml +4 -0
- data/examples/example.rb +24 -0
- data/ext/coremidi.c +391 -0
- data/ext/extconf.rb +5 -0
- data/lib/coremidi.rb +69 -0
- data/spec/parsing_spec.rb +41 -0
- data/spec/spec_helper.rb +5 -0
- metadata +65 -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 burke-coremidi
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rubygems/specification'
|
3
|
+
require 'date'
|
4
|
+
require 'spec/rake/spectask'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'jeweler'
|
8
|
+
Jeweler::Tasks.new do |s|
|
9
|
+
s.name = "coremidi"
|
10
|
+
s.summary = "A very simple access layer to the OS X CoreMIDI library."
|
11
|
+
s.email = "burke@53cr.com"
|
12
|
+
s.homepage = "http://github.com/burke/coremidi"
|
13
|
+
s.description = "A very simple access layer to the OS X CoreMIDI library."
|
14
|
+
s.authors = ["Burke Libbey"]
|
15
|
+
s.files = FileList["[A-Z]*", "{lib,spec,examples}/**/*", "ext/extconf.rb", "ext/coremidi.c"]
|
16
|
+
s.extensions = ["ext/extconf.rb"]
|
17
|
+
end
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
task :default => :spec
|
24
|
+
|
25
|
+
desc "Run specs"
|
26
|
+
Spec::Rake::SpecTask.new do |t|
|
27
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
28
|
+
t.spec_opts = %w(-fs --color)
|
29
|
+
end
|
data/VERSION.yml
ADDED
data/examples/example.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
begin
|
2
|
+
require 'rubygems'
|
3
|
+
rescue LoadError
|
4
|
+
end
|
5
|
+
require 'coremidi'
|
6
|
+
|
7
|
+
# Start archaeopteryx
|
8
|
+
# Start GarageBand (just to make sure it's all working)
|
9
|
+
|
10
|
+
# Open MIDI Patch Bay.app
|
11
|
+
# Create a new input (anyname)
|
12
|
+
# Create a new output (anyname)
|
13
|
+
# GarageBand will announce that it has found a new input
|
14
|
+
# You should have sound, yay
|
15
|
+
|
16
|
+
# Now run this script
|
17
|
+
|
18
|
+
midi_thread = Thread.new do
|
19
|
+
CoreMIDI::Input.register("Test", "Test", "Out") do |event|
|
20
|
+
puts event.inspect
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
gets # Stop on enter
|
data/ext/coremidi.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_coremidi()
|
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/ext/extconf.rb
ADDED
data/lib/coremidi.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../ext/coremidi.bundle'
|
2
|
+
#require 'coremidi/constants'
|
3
|
+
|
4
|
+
module CoreMIDI
|
5
|
+
module API
|
6
|
+
MidiPacket = Struct.new(:timestamp, :data)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Unused, but left here for documentation
|
10
|
+
def self.number_of_sources
|
11
|
+
API.get_num_sources
|
12
|
+
end
|
13
|
+
|
14
|
+
class Packet
|
15
|
+
# http://www.srm.com/qtma/davidsmidispec.html
|
16
|
+
def self.parse(data)
|
17
|
+
spec = {
|
18
|
+
0x80 => Events::NoteOff,
|
19
|
+
0x90 => lambda {|data| (data[Events::NoteOn.members.index("velocity")] == 0) ? Events::NoteOff : Events::NoteOn },
|
20
|
+
0xA0 => Events::KeyPressure,
|
21
|
+
0xC0 => Events::ProgramChange,
|
22
|
+
0xD0 => Events::ChannelPressure
|
23
|
+
}
|
24
|
+
|
25
|
+
klass = spec.detect {|code, _|
|
26
|
+
data[0] & 0xF0 == code # First byte is the type code
|
27
|
+
}
|
28
|
+
|
29
|
+
return Events::Unknown.new(data) if klass.nil?
|
30
|
+
|
31
|
+
klass = klass.last
|
32
|
+
klass = klass.call(data) if klass.respond_to?(:call) # Resolve any lambdas into a class
|
33
|
+
|
34
|
+
klass.new(
|
35
|
+
data[0] & 0x0F, # Second byte contains the channel
|
36
|
+
*data[1..-1]
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module Events
|
42
|
+
class NoteOn < Struct.new(:channel, :pitch, :velocity); end;
|
43
|
+
class NoteOff < Struct.new(:channel, :pitch, :velocity); end;
|
44
|
+
class KeyPressure < Struct.new(:channel, :pitch, :pressure); end;
|
45
|
+
class ProgramChange < Struct.new(:channel, :preset); end;
|
46
|
+
class ChannelPressure < Struct.new(:channel, :pressure); end;
|
47
|
+
class Unknown < Struct.new(:data); end;
|
48
|
+
end
|
49
|
+
|
50
|
+
class Input
|
51
|
+
def self.register(client_name, port_name, source)
|
52
|
+
raise "name must be a String!" unless client_name.class == String
|
53
|
+
|
54
|
+
client = API.create_client(client_name)
|
55
|
+
port = API.create_input_port(client, port_name)
|
56
|
+
API.connect_source_to_port(API.get_sources.index(source), port)
|
57
|
+
|
58
|
+
while true
|
59
|
+
data = API.check_for_new_data
|
60
|
+
if data && !data.empty?
|
61
|
+
data.each do |packet|
|
62
|
+
yield(Packet.parse(packet.data))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
sleep 0.001
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
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,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: burke-coremidi
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Burke Libbey
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-04-22 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: A very simple access layer to the OS X CoreMIDI library.
|
17
|
+
email: burke@53cr.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions:
|
21
|
+
- ext/extconf.rb
|
22
|
+
extra_rdoc_files:
|
23
|
+
- LICENSE
|
24
|
+
- README.rdoc
|
25
|
+
files:
|
26
|
+
- LICENSE
|
27
|
+
- README.rdoc
|
28
|
+
- Rakefile
|
29
|
+
- VERSION.yml
|
30
|
+
- examples/example.rb
|
31
|
+
- ext/coremidi.c
|
32
|
+
- ext/extconf.rb
|
33
|
+
- lib/coremidi.rb
|
34
|
+
- spec/parsing_spec.rb
|
35
|
+
- spec/spec_helper.rb
|
36
|
+
has_rdoc: true
|
37
|
+
homepage: http://github.com/burke/coremidi
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options:
|
40
|
+
- --charset=UTF-8
|
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.2.0
|
59
|
+
signing_key:
|
60
|
+
specification_version: 2
|
61
|
+
summary: A very simple access layer to the OS X CoreMIDI library.
|
62
|
+
test_files:
|
63
|
+
- spec/parsing_spec.rb
|
64
|
+
- spec/spec_helper.rb
|
65
|
+
- examples/example.rb
|