Develop and Download Open Source Software

Browse Subversion Repository

Contents of /trunk/Ruby_bindings/MDRubySequence.m

Parent Directory Parent Directory | Revision Log Revision Log


Revision 178 - (show annotations) (download)
Mon Feb 24 02:00:55 2020 UTC (4 years, 3 months ago) by toshinagata1964
File size: 18522 byte(s)
Ruby: Sequence and Track objects now work as Enumerable
1 /*
2 * MDRubySequence.m
3 * Alchemusica
4 *
5 * Created by Toshi Nagata on 08/03/21.
6 * Copyright 2008-2017 Toshi Nagata. All rights reserved.
7 *
8 This program is free software; you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation version 2 of the License.
11
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
16 */
17
18 #import <Cocoa/Cocoa.h>
19 #include "MDRuby.h"
20 #include <sys/types.h>
21 #include <sys/stat.h>
22 #include <unistd.h>
23
24 #import "MyAppController.h"
25 #import "MyDocument.h"
26 #import "MyMIDISequence.h"
27 #import "MDObjects.h"
28
29 // Sequence class
30 VALUE rb_cMRSequence = Qfalse;
31
32 #pragma mark ====== Housekeeping MyDocument ======
33
34 // Housekeeping MyDocument and Sequence
35 // In the constructor of MyDocument, MRSequenceRegister() is called, which
36 // creates a new Ruby object which wraps "doc" and registers it in a global
37 // Ruby variable $mr_documents. Subsequent request of Sequence always returns
38 // this same object for the same MyDocument.
39 // When MyDocument is deallocated, MRSequenceUnregister() is called, which
40 // removes the Sequence object from $mr_documents, and replace the pointer
41 // to MyDocument with nil. Subsequent request of Sequence reeturns Qnil.
42
43 // Global variable $mr_documents
44 VALUE gMRSequences = Qfalse;
45
46 // Structure to hold a pointer to MyDocument (or nil)
47 typedef struct MRSequenceRecord {
48 MyDocument *doc;
49 } MRSequenceRecord;
50
51 // MyDocument <-> Sequence
52 MyDocument *
53 MyDocumentFromMRSequenceValue(VALUE val)
54 {
55 MRSequenceRecord *rp;
56 if (rb_obj_is_kind_of(val, rb_cMRSequence)) {
57 Data_Get_Struct(val, MRSequenceRecord, rp);
58 if (rp->doc != NULL)
59 return rp->doc;
60 }
61 rb_raise(rb_eTypeError, "Cannot get MyDocument pointer from object");
62 }
63
64 VALUE
65 MRSequenceFromMyDocument(MyDocument *doc)
66 {
67 VALUE *valp;
68 int len, i;
69 MRSequenceRecord *rp;
70 len = (int)RARRAY_LEN(gMRSequences);
71 valp = RARRAY_PTR(gMRSequences);
72 for (i = 0; i < len; i++) {
73 Data_Get_Struct(valp[i], MRSequenceRecord, rp);
74 if (rp->doc == doc)
75 return valp[i]; // Already registered
76 }
77 return Qnil;
78 }
79
80 int
81 MRSequenceRegister(void *myDocument)
82 {
83 VALUE val;
84 MRSequenceRecord *rp;
85
86 val = MRSequenceFromMyDocument(myDocument);
87 if (val != Qnil)
88 return -1; // Already registered
89
90 // Register a new entry
91 val = Data_Make_Struct(rb_cMRSequence, MRSequenceRecord, 0, -1, rp);
92 rp->doc = myDocument;
93 rb_ary_push(gMRSequences, val);
94 return 0;
95 }
96
97 int
98 MRSequenceUnregister(void *myDocument)
99 {
100 VALUE val;
101 MRSequenceRecord *rp;
102
103 val = MRSequenceFromMyDocument(myDocument);
104 if (val == Qnil)
105 return -1; // Unknown object
106
107 Data_Get_Struct(val, MRSequenceRecord, rp);
108 rp->doc = nil;
109 rb_ary_delete(gMRSequences, val);
110 return 0;
111 }
112
113 #pragma mark ====== Ruby methods ======
114
115 /*
116 * call-seq:
117 * sequence.tick_to_time(tick)
118 *
119 * Convert tick to time by referring the conductor track. Time is expressed
120 * in seconds.
121 */
122 static VALUE
123 s_MRSequence_TickToTime(VALUE self, VALUE tval)
124 {
125 MyDocument *doc = MyDocumentFromMRSequenceValue(self);
126 MDCalibrator *calib = [[doc myMIDISequence] sharedCalibrator];
127 MDTickType tick = (MDTickType)floor(NUM2DBL(tval) + 0.5);
128 MDTimeType time = MDCalibratorTickToTime(calib, tick);
129 return rb_float_new((double)time / 1000000.0);
130 }
131
132 /*
133 * call-seq:
134 * sequence.time_to_tick(time)
135 *
136 * Convert tick to time by referring the conductor track. Time is expressed
137 * in seconds.
138 */
139 static VALUE
140 s_MRSequence_TimeToTick(VALUE self, VALUE tval)
141 {
142 MyDocument *doc = MyDocumentFromMRSequenceValue(self);
143 MDCalibrator *calib = [[doc myMIDISequence] sharedCalibrator];
144 MDTimeType time = (MDTimeType)floor((NUM2DBL(tval) * (double)1000000.0) + (double)0.5);
145 MDTickType tick = MDCalibratorTimeToTick(calib, time);
146 return rb_float_new((double)tick);
147 }
148
149 /*
150 * call-seq:
151 * sequence.tick_to_measure(tick)
152 *
153 * Convert tick to bar/beat/subtick by referring the conductor track. Returns an
154 * array containing three integers. Bar and beat are 1-based, and subtick is
155 * 0-based.
156 */
157 static VALUE
158 s_MRSequence_TickToMeasure(VALUE self, VALUE tval)
159 {
160 MyDocument *doc = MyDocumentFromMRSequenceValue(self);
161 MDCalibrator *calib = [[doc myMIDISequence] sharedCalibrator];
162 MDTickType tick = (MDTickType)floor(NUM2DBL(tval) + 0.5);
163 int32_t bar, beat, subtick;
164 VALUE vals[3];
165 MDCalibratorTickToMeasure(calib, tick, &bar, &beat, &subtick);
166 vals[0] = INT2NUM(bar);
167 vals[1] = INT2NUM(beat);
168 vals[2] = INT2NUM(subtick);
169 return rb_ary_new4(3, vals);
170 }
171
172 /*
173 * call-seq:
174 * sequence.measure_to_tick(ary)
175 *
176 * Convert bar/beat/subtick to tick by referring the conductor track. Ary must
177 * be an array containing three numbers, bar/beat/subtick. Bar and beat are
178 * 1-based, and subtick is 0-based.
179 */
180 static VALUE
181 s_MRSequence_MeasureToTick(int argc, VALUE *argv, VALUE self)
182 {
183 MyDocument *doc = MyDocumentFromMRSequenceValue(self);
184 MDCalibrator *calib = [[doc myMIDISequence] sharedCalibrator];
185 MDTickType tick;
186 VALUE val1, val2, val3;
187 int32_t bar, beat, subtick;
188 rb_scan_args(argc, argv, "12", &val1, &val2, &val3);
189 if (NIL_P(val2) && NIL_P(val3)) {
190 bar = NUM2INT(Ruby_ObjectAtIndex(val1, 0));
191 beat = NUM2INT(Ruby_ObjectAtIndex(val2, 1));
192 subtick = NUM2INT(Ruby_ObjectAtIndex(val3, 2));
193 } else {
194 bar = NUM2INT(val1);
195 beat = NUM2INT(val2);
196 subtick = NUM2INT(val3);
197 }
198 tick = MDCalibratorMeasureToTick(calib, bar, beat, subtick);
199 return rb_float_new((double)tick);
200 }
201
202 /*
203 * call-seq:
204 * sequence.tick_for_selection(editable_only = false)
205 *
206 * Returns a pair of ticks representing the tick range of selected events.
207 * If the argument is true, then only the editable tracks are examined.
208 */
209 static VALUE
210 s_MRSequence_TickForSelection(int argc, VALUE *argv, VALUE self)
211 {
212 VALUE fval, startval, endval;
213 MDTickType startTick, endTick;
214 MyDocument *doc = MyDocumentFromMRSequenceValue(self);
215 rb_scan_args(argc, argv, "01", &fval);
216 [doc getSelectionStartTick: &startTick endTick: &endTick editableTracksOnly: RTEST(fval)];
217 startval = INT2NUM(startTick);
218 endval = INT2NUM(endTick);
219 return rb_ary_new3(2, startval, endval);
220 }
221
222 /*
223 * call-seq:
224 * sequence.editing_range
225 *
226 * Returns a pair of ticks representing the editing range.
227 * If editing range is not set, [-1, -1] is returned.
228 */
229 static VALUE
230 s_MRSequence_EditingRange(VALUE self)
231 {
232 MDTickType startTick, endTick;
233 MyDocument *doc = MyDocumentFromMRSequenceValue(self);
234 [doc getEditingRangeStart:&startTick end:&endTick];
235 return rb_ary_new3(2, INT2NUM(startTick), INT2NUM(endTick));
236 }
237
238 /*
239 * call-seq:
240 * sequence.timebase
241 *
242 * Get the timebase of the sequence in tick.
243 */
244 static VALUE
245 s_MRSequence_Timebase(VALUE self)
246 {
247 MyDocument *doc = MyDocumentFromMRSequenceValue(self);
248 float timebase = [doc timebase];
249 return rb_float_new(timebase);
250 }
251
252 /*
253 * call-seq:
254 * sequence.set_timebase(timebase)
255 *
256 * Set the timebase of the sequence.
257 */
258 static VALUE
259 s_MRSequence_SetTimebase(VALUE self, VALUE tval)
260 {
261 MyDocument *doc = MyDocumentFromMRSequenceValue(self);
262 float timebase = (float)NUM2DBL(rb_Float(tval));
263 [doc setTimebase:timebase];
264 return self;
265 }
266
267 /*
268 * call-seq:
269 * sequence.duration
270 *
271 * Get the duration of the sequence in tick.
272 */
273 static VALUE
274 s_MRSequence_Duration(VALUE self)
275 {
276 MyDocument *doc = MyDocumentFromMRSequenceValue(self);
277 MDTickType duration = [[doc myMIDISequence] sequenceDuration];
278 return INT2NUM(duration);
279 }
280
281 /*
282 * call-seq:
283 * sequence.track(n)
284 *
285 * Get an Track object containing the n-th track.
286 */
287 static VALUE
288 s_MRSequence_Track(VALUE self, VALUE nval)
289 {
290 return rb_funcall(rb_cMRTrack, rb_intern("new"), 2, self, nval);
291 }
292
293 /*
294 * call-seq:
295 * sequence.number_of_tracks
296 * sequence.ntracks
297 *
298 * Get the duration of the sequence in tick.
299 */
300 static VALUE
301 s_MRSequence_NumberOfTracks(VALUE self)
302 {
303 MyDocument *doc = MyDocumentFromMRSequenceValue(self);
304 int ntracks = [[doc myMIDISequence] trackCount];
305 return INT2NUM(ntracks);
306 }
307
308 /*
309 * call-seq:
310 * sequence.each_track block
311 * sequence.each block
312 *
313 * Execute the block for each track; the block argument is a Track object.
314 */
315 static VALUE
316 s_MRSequence_EachTrack(VALUE self)
317 {
318 MyDocument *doc = MyDocumentFromMRSequenceValue(self);
319 int ntracks = [[doc myMIDISequence] trackCount];
320 int i;
321 for (i = 0; i < ntracks; i++) {
322 VALUE tval = s_MRSequence_Track(self, INT2NUM(i));
323 rb_yield(tval);
324 }
325 return self;
326 }
327
328 /*
329 * call-seq:
330 * sequence.each_editable_track block
331 *
332 * Execute the block for each editable track; the block argument is a Track object.
333 */
334 static VALUE
335 s_MRSequence_EachEditableTrack(VALUE self)
336 {
337 MyDocument *doc = MyDocumentFromMRSequenceValue(self);
338 int ntracks = [[doc myMIDISequence] trackCount];
339 int i;
340 for (i = 0; i < ntracks; i++) {
341 MDTrackAttribute attr = [doc trackAttributeForTrack: i];
342 if (attr & kMDTrackAttributeEditable) {
343 VALUE tval = s_MRSequence_Track(self, INT2NUM(i));
344 rb_yield(tval);
345 }
346 }
347 return self;
348 }
349
350 /*
351 * call-seq:
352 * sequence.each_selected_track block
353 *
354 * Execute the block for each selected track; the block argument is a Track object.
355 */
356 static VALUE
357 s_MRSequence_EachSelectedTrack(VALUE self)
358 {
359 MyDocument *doc = MyDocumentFromMRSequenceValue(self);
360 int ntracks = [[doc myMIDISequence] trackCount];
361 int i;
362 for (i = 0; i < ntracks; i++) {
363 if ([doc isTrackSelected: i]) {
364 VALUE tval = s_MRSequence_Track(self, INT2NUM(i));
365 rb_yield(tval);
366 }
367 }
368 return self;
369 }
370
371 /*
372 * call-seq:
373 * sequence.insert_track(track[, num]) -> track
374 *
375 * Insert a track at the specified position, or at the end if unspecified.
376 * The track must not belong to any sequence (including self).
377 */
378 static VALUE
379 s_MRSequence_InsertTrack(int argc, VALUE *argv, VALUE self)
380 {
381 VALUE tval, nval;
382 int n;
383 MyDocumentTrackInfo *ip;
384 MDTrackObject *trobj;
385 MyDocument *doc = MyDocumentFromMRSequenceValue(self);
386 rb_scan_args(argc, argv, "11", &tval, &nval);
387 ip = TrackInfoFromMRTrackValue(tval);
388 if (ip->doc != nil)
389 rb_raise(rb_eArgError, "The track to insert must not belong to any sequence");
390 if (nval == Qnil)
391 n = [[doc myMIDISequence] trackCount];
392 else
393 n = NUM2INT(rb_Integer(nval));
394 trobj = [[[MDTrackObject alloc] initWithMDTrack: ip->track] autorelease];
395 [doc insertTrack: trobj atIndex: n];
396 /* ip->doc is automatically updated */
397 return tval;
398 }
399
400 /*
401 * call-seq:
402 * sequence.delete_track(num) -> track
403 *
404 * Delete a track at the specified position. Returns the deleted track,
405 * which has now no parent.
406 */
407 static VALUE
408 s_MRSequence_DeleteTrack(VALUE self, VALUE nval)
409 {
410 int n;
411 VALUE rval;
412 MyDocument *doc = MyDocumentFromMRSequenceValue(self);
413 n = NUM2INT(rb_Integer(nval));
414 if (n < 0 || n >= [[doc myMIDISequence] trackCount])
415 rb_raise(rb_eRangeError, "track number out of range");
416 rval = s_MRSequence_Track(self, nval);
417 [doc deleteTrackAt: n];
418 /* Note: the MDTrack contained in rval is now orphaned. The deleted track
419 is also on the undo buffer of the document, but it is a duplicated copy.
420 See -[MyDocument deleteTrackAt:]. */
421 return rval;
422 }
423
424 /*
425 * call-seq:
426 * sequence.name -> String
427 *
428 * Get the name of the sequence.
429 */
430 static VALUE
431 s_MRSequence_Name(VALUE self)
432 {
433 // int n;
434 MyDocument *doc = MyDocumentFromMRSequenceValue(self);
435 NSString *name = [doc tuneName];
436 return rb_str_new2([name UTF8String]);
437 }
438
439 /*
440 * call-seq:
441 * sequence.path -> String
442 *
443 * Get the path of the sequence, if it is associated with a file.
444 */
445 static VALUE
446 s_MRSequence_Path(VALUE self)
447 {
448 // int n;
449 MyDocument *doc = MyDocumentFromMRSequenceValue(self);
450 NSURL *url = [doc fileURL];
451 NSString *path;
452 if (url != nil && (path = [url path]) != nil)
453 return rb_str_new2([path UTF8String]);
454 else return Qnil;
455 }
456
457 /*
458 * call-seq:
459 * sequence.dir -> String
460 *
461 * Get the directory of the sequence, if it is associated with a file.
462 */
463 static VALUE
464 s_MRSequence_Dir(VALUE self)
465 {
466 // int n;
467 MyDocument *doc = MyDocumentFromMRSequenceValue(self);
468 NSURL *url = [doc fileURL];
469 NSString *path;
470 if (url != nil && (path = [url path]) != nil) {
471 path = [path stringByDeletingLastPathComponent];
472 return rb_str_new2([path UTF8String]);
473 } else return Qnil;
474 }
475
476 /*
477 * call-seq:
478 * Sequence.current
479 *
480 * Get the sequence corresponding to the current document.
481 */
482 VALUE
483 MRSequence_Current(VALUE self)
484 {
485 NSArray *docs = [NSApp orderedDocuments];
486 if (docs == nil || [docs count] == 0)
487 return Qnil;
488 return MRSequenceFromMyDocument([docs objectAtIndex: 0]);
489 }
490
491 /* For DEBUG */
492 VALUE
493 s_MRSequence_Merger(int argc, VALUE *argv, VALUE self)
494 {
495 int i;
496 MyDocument *doc = MyDocumentFromMRSequenceValue(self);
497 MyMIDISequence *seq = [doc myMIDISequence];
498 MDTrackMerger *merger = MDTrackMergerNew();
499 MDTrack *track;
500 int numEvents = 0;
501 MDEvent **ebuf, *ep;
502 char buf[256];
503 FILE *fp;
504 int *ibuf;
505 for (i = 0; i < argc; i++) {
506 int n = NUM2INT(argv[i]);
507 track = [seq getTrackAtIndex:n];
508 if (track != NULL) {
509 MDTrackMergerAddTrack(merger, track);
510 numEvents += MDTrackGetNumberOfEvents(track);
511 }
512 }
513 ebuf = (MDEvent **)calloc(sizeof(MDEvent *), numEvents);
514 ibuf = (int *)calloc(sizeof(int), numEvents);
515 if (ebuf == NULL || ibuf == NULL) {
516 fprintf(stderr, "out of memory\n");
517 return Qnil;
518 }
519 ep = MDTrackMergerCurrent(merger, &track);
520 fp = fopen([[@"~/merger_test.txt" stringByExpandingTildeInPath] UTF8String], "w");
521 if (fp == NULL) {
522 fprintf(stderr, "Cannot open merger_test.txt\n");
523 return Qnil;
524 }
525 for (i = 0; i < numEvents && ep != NULL; i++) {
526 int trno = [seq lookUpTrack:track];
527 ebuf[i] = ep;
528 ibuf[i] = trno;
529 MDEventToString(ep, buf, sizeof(buf));
530 fprintf(fp, "%d:%s\n", trno, buf);
531 ep = MDTrackMergerForward(merger, &track);
532 }
533 fclose(fp);
534 free(ebuf);
535 free(ibuf);
536 return INT2NUM(numEvents);
537 }
538
539 VALUE
540 s_MRSequence_BackMerger(int argc, VALUE *argv, VALUE self)
541 {
542 int i;
543 MyDocument *doc = MyDocumentFromMRSequenceValue(self);
544 MyMIDISequence *seq = [doc myMIDISequence];
545 MDTrackMerger *merger = MDTrackMergerNew();
546 MDTrack *track;
547 int numEvents = 0;
548 MDEvent **ebuf, *ep;
549 char buf[256];
550 FILE *fp;
551 int *ibuf;
552 for (i = 0; i < argc; i++) {
553 int n = NUM2INT(argv[i]);
554 track = [seq getTrackAtIndex:n];
555 if (track != NULL) {
556 MDTrackMergerAddTrack(merger, track);
557 numEvents += MDTrackGetNumberOfEvents(track);
558 }
559 }
560 ebuf = (MDEvent **)calloc(sizeof(MDEvent *), numEvents);
561 ibuf = (int *)calloc(sizeof(int), numEvents);
562 if (ebuf == NULL || ibuf == NULL) {
563 fprintf(stderr, "out of memory\n");
564 return Qnil;
565 }
566 MDTrackMergerJumpToTick(merger, kMDMaxTick, NULL);
567 fp = fopen([[@"~/backmerger_test.txt" stringByExpandingTildeInPath] UTF8String], "w");
568 if (fp == NULL) {
569 fprintf(stderr, "Cannot open backmerger_test.txt\n");
570 return Qnil;
571 }
572 for (i = numEvents - 1; i >= 0; i--) {
573 int trno;
574 ep = MDTrackMergerBackward(merger, &track);
575 if (ep == NULL)
576 break;
577 trno = [seq lookUpTrack:track];
578 ebuf[i] = ep;
579 ibuf[i] = trno;
580 }
581 for (i = 0; i < numEvents; i++) {
582 MDEventToString(ebuf[i], buf, sizeof(buf));
583 fprintf(fp, "%d:%s\n", ibuf[i], buf);
584 }
585 fclose(fp);
586 free(ebuf);
587 free(ibuf);
588 return INT2NUM(numEvents);
589 }
590
591 #pragma mark ====== Initialize class ======
592
593 void
594 MRSequenceInitClass(void)
595 {
596 if (rb_cMRSequence != Qfalse)
597 return;
598
599 rb_cMRSequence = rb_define_class("Sequence", rb_cObject);
600 rb_include_module(rb_cMRSequence, rb_mEnumerable);
601
602 // Define methods
603 /* rb_define_method(rb_cMRSequence, "register_menu", s_MRSequence_RegisterMenu, 2); */
604 rb_define_method(rb_cMRSequence, "tick_to_time", s_MRSequence_TickToTime, 1);
605 rb_define_method(rb_cMRSequence, "time_to_tick", s_MRSequence_TimeToTick, 1);
606 rb_define_method(rb_cMRSequence, "tick_to_measure", s_MRSequence_TickToMeasure, 1);
607 rb_define_method(rb_cMRSequence, "measure_to_tick", s_MRSequence_MeasureToTick, -1);
608 rb_define_method(rb_cMRSequence, "tick_for_selection", s_MRSequence_TickForSelection, -1);
609 rb_define_method(rb_cMRSequence, "editing_range", s_MRSequence_EditingRange, 0);
610 rb_define_method(rb_cMRSequence, "timebase", s_MRSequence_Timebase, 0);
611 rb_define_method(rb_cMRSequence, "set_timebase", s_MRSequence_SetTimebase, 1);
612 rb_define_method(rb_cMRSequence, "duration", s_MRSequence_Duration, 0);
613 rb_define_method(rb_cMRSequence, "track", s_MRSequence_Track, 1);
614 rb_define_method(rb_cMRSequence, "number_of_tracks", s_MRSequence_NumberOfTracks, 0);
615 rb_define_method(rb_cMRSequence, "ntracks", s_MRSequence_NumberOfTracks, 0);
616 rb_define_method(rb_cMRSequence, "each_track", s_MRSequence_EachTrack, 0);
617 rb_define_method(rb_cMRSequence, "each", s_MRSequence_EachTrack, 0);
618 rb_define_method(rb_cMRSequence, "each_editable_track", s_MRSequence_EachEditableTrack, 0);
619 rb_define_method(rb_cMRSequence, "each_selected_track", s_MRSequence_EachSelectedTrack, 0);
620 rb_define_method(rb_cMRSequence, "insert_track", s_MRSequence_InsertTrack, -1);
621 rb_define_method(rb_cMRSequence, "delete_track", s_MRSequence_DeleteTrack, 1);
622 rb_define_method(rb_cMRSequence, "name", s_MRSequence_Name, 0);
623 rb_define_method(rb_cMRSequence, "path", s_MRSequence_Path, 0);
624 rb_define_method(rb_cMRSequence, "dir", s_MRSequence_Dir, 0);
625
626 /* for DEBUG */
627 rb_define_method(rb_cMRSequence, "merger", s_MRSequence_Merger, -1);
628 rb_define_method(rb_cMRSequence, "backmerger", s_MRSequence_BackMerger, -1);
629
630 /* rb_define_method(rb_cMRSequence, "pointer", s_MRSequence_Pointer, 1); */
631 rb_define_singleton_method(rb_cMRSequence, "current", MRSequence_Current, 0);
632
633 /* rb_define_singleton_method(rb_cMRSequence, "register_menu", s_MRSequence_RegisterMenu, 2);
634 rb_define_singleton_method(rb_cMRSequence, "global_settings", MRSequence_GlobalSettings, 1);
635 rb_define_singleton_method(rb_cMRSequence, "set_global_settings", MRSequence_SetGlobalSettings, 2); */
636
637 // Define a global variable "$mr_documents" and assign an empty array
638 rb_define_variable("mr_documents", &gMRSequences);
639 gMRSequences = rb_ary_new();
640 }

Back to OSDN">Back to OSDN
ViewVC Help
Powered by ViewVC 1.1.26