midictl.c revision 1.1.2.3 1 /* $NetBSD: midictl.c,v 1.1.2.3 2006/06/10 22:32:27 chap Exp $ */
2
3 /*-
4 * Copyright (c) 2006 The NetBSD Foundation, Inc.
5 * All rights reserved.
6 *
7 * This code is derived from software contributed to The NetBSD Foundation
8 * by Chapman Flack.
9 *
10 * Redistribution and use in source and binary forms, with or without
11 * modification, are permitted provided that the following conditions
12 * are met:
13 * 1. Redistributions of source code must retain the above copyright
14 * notice, this list of conditions and the following disclaimer.
15 * 2. Redistributions in binary form must reproduce the above copyright
16 * notice, this list of conditions and the following disclaimer in the
17 * documentation and/or other materials provided with the distribution.
18 * 3. All advertising materials mentioning features or use of this software
19 * must display the following acknowledgement:
20 * This product includes software developed by the NetBSD
21 * Foundation, Inc. and its contributors.
22 * 4. Neither the name of The NetBSD Foundation nor the names of its
23 * contributors may be used to endorse or promote products derived
24 * from this software without specific prior written permission.
25 *
26 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
27 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
28 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
29 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
30 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
31 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
32 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
33 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
34 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
35 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
36 * POSSIBILITY OF SUCH DAMAGE.
37 */
38 #include <sys/cdefs.h>
39 __KERNEL_RCSID(0, "$NetBSD: midictl.c,v 1.1.2.3 2006/06/10 22:32:27 chap Exp $");
40
41 /*
42 * See midictl.h for an overview of the purpose and use of this module.
43 */
44
45 #if defined(_KERNEL)
46 #define _MIDICTL_ASSERT(x) KASSERT(x)
47 #define _MIDICTL_MALLOC(s,t) malloc((s), (t), M_WAITOK)
48 #define _MIDICTL_FREE(s,t) free((s), (t))
49 #include <sys/systm.h>
50 #include <sys/types.h>
51 #else
52 #include <assert.h>
53 #include <stdio.h>
54 #include <stdlib.h>
55 #define _MIDICTL_ASSERT(x) assert(x)
56 #define _MIDICTL_MALLOC(s,t) malloc((s))
57 #define _MIDICTL_FREE(s,t) free((s))
58 #endif
59
60 #include "midictl.h"
61
62 /*
63 * The upper part of this file is MIDI-aware, and deals with things like
64 * decoding MIDI Control Change messages, dealing with the ones that require
65 * special handling as mode messages or parameter updates, and so on.
66 *
67 * It relies on a "store" layer (implemented in the lower part of this file)
68 * that only must be able to stash away 2-, 8-, or 16-bit quantities (which
69 * it may pack into larger units as it sees fit) and find them again given
70 * a class, channel, and key (controller/parameter number).
71 *
72 * The MIDI controllers can have 1-, 7-, or 14-bit values; the parameters are
73 * also 14-bit. The 14-bit values have to be set in two MIDI messages, 7 bits
74 * at a time. The MIDI layer uses store-managed 2- or 8-bit slots for the
75 * smaller types, and uses the free high bit to indicate that it has explicitly
76 * set the value. (Because the store is allowed to pack things, it may 'find'
77 * a zero entry for a value we never set, because it shares a word with a
78 * different value that has been set. We know it is not a real value because
79 * the high bit is clear.)
80 *
81 * The 14-bit values are handled similarly: 16-bit store slots are used to hold
82 * them, with the two free high bits indicating independently whether the MSB
83 * and the LSB have been explicitly set--as two separate MIDI messages are
84 * required. If such a control is queried when only one half has been explicitly
85 * set, the result is as if it had been set to the specified default value
86 * before the explicit set.
87 */
88 typedef struct bucket bucket; /* the store layer completes this type */
89
90 typedef enum { CTL1, CTL7, CTL14, RPN, NRPN } class;
91
92 /*
93 * assert(does_not_apply(KNFNamespaceArgumentAgainstNamesInPrototypes,
94 * PrototypesOfStaticFunctionsWithinNonIncludedFile));
95 */
96 static void reset_all_controllers(midictl *mc, uint_fast8_t chan);
97 static void enter14(midictl *mc, uint_fast8_t chan, class c,
98 uint_fast16_t key, _Bool islsb, uint8_t val);
99 static uint_fast16_t read14(midictl *mc, uint_fast8_t chan, class c,
100 uint_fast16_t key, uint_fast16_t dflt);
101 static class classify(uint_fast16_t *key, _Bool *islsb);
102 static midictl_notify notify_no_one;
103
104 static midictl_store *store_init(void);
105 static void store_done(midictl_store *s);
106 static bucket *store_locate(midictl_store *s, class c,
107 uint_fast8_t chan, uint_fast16_t key);
108 static uint16_t store_extract(bucket *b, class c,
109 uint_fast8_t chan, uint_fast16_t key);
110 static void store_update(midictl_store *s, bucket *b, class c,
111 uint_fast8_t chan, uint_fast16_t key, uint16_t value);
112
113 #define PN_SET 0x8000 /* a parameter number has been explicitly set */
114 #define C14MSET 0x8000 /* MSB of a 14-bit val has been set */
115 #define C14LSET 0x4000 /* LSB of a 14-bit val has been set */
116 #define C7_SET 0x80 /* a 7-bit ctl has been set */
117 #define C1_SET 2 /* a 1-bit ctl has been set */
118
119 #if defined(_MIDICTL_MAIN)
120 #define XS(s) [MIDICTL_##s]=#s
121 char const * const evt_strings[] = {
122 XS(CTLR), XS(RPN), XS(NRPN), XS(RESET), XS(NOTES_OFF),
123 XS(SOUND_OFF), XS(LOCAL), XS(MODE)
124 };
125 #undef XS
126
127 void
128 dbgnotify(void *cookie, midictl_evt e, uint_fast8_t chan, uint_fast16_t key)
129 {
130 printf("NFY %p %s chan %u #%u\n", cookie, evt_strings[e], chan, key);
131 }
132
133 midictl mc = {
134 .accept_any_ctl_rpn = 0,
135 .accept_any_nrpn = 0,
136 .base_channel = 16,
137 .cookie = NULL,
138 .notify = dbgnotify
139 };
140
141 int
142 main(int argc, char **argv)
143 {
144 int cnt, a, b, c;
145
146 midictl_open(&mc);
147 do {
148 cnt = scanf("%i %i %i", &a, &b, &c);
149 if ( 3 == cnt ) {
150 midictl_change(&mc, a, (uint8_t[]){b,c});
151 }
152 } while ( EOF != cnt );
153 midictl_close(&mc);
154 return 0;
155 }
156 #endif /* defined(_MIDICTL_MAIN) */
157
158 void
159 midictl_open(midictl *mc)
160 {
161 if ( NULL == mc->notify )
162 mc->notify = notify_no_one;
163 mc->store = store_init();
164 }
165
166 void
167 midictl_close(midictl *mc)
168 {
169 store_done(mc->store);
170 }
171
172 void
173 midictl_change(midictl *mc, uint_fast8_t chan, uint8_t *ctlval)
174 {
175 class c;
176 uint_fast16_t key, val;
177 _Bool islsb;
178 bucket *bkt;
179
180 switch ( ctlval[0] ) {
181 /*
182 * Channel mode messages:
183 */
184 case MIDI_CTRL_OMNI_OFF:
185 case MIDI_CTRL_OMNI_ON:
186 case MIDI_CTRL_POLY_OFF:
187 case MIDI_CTRL_POLY_ON:
188 if ( chan != mc->base_channel )
189 return; /* ignored - not on base channel */
190 else
191 return; /* XXX ignored anyway - not implemented yet */
192 case MIDI_CTRL_NOTES_OFF:
193 mc->notify(mc->cookie, MIDICTL_NOTES_OFF, chan, 0);
194 return;
195 case MIDI_CTRL_LOCAL:
196 mc->notify(mc->cookie, MIDICTL_LOCAL, chan, ctlval[1]);
197 return;
198 case MIDI_CTRL_SOUND_OFF:
199 mc->notify(mc->cookie, MIDICTL_SOUND_OFF, chan, 0);
200 return;
201 case MIDI_CTRL_RESET:
202 reset_all_controllers(mc, chan);
203 return;
204 /*
205 * Control changes to be handled specially:
206 */
207 case MIDI_CTRL_RPN_LSB:
208 mc-> rpn &= ~0x7f;
209 mc-> rpn |= PN_SET | (0x7f & ctlval[1]);
210 mc->nrpn &= ~PN_SET;
211 return;
212 case MIDI_CTRL_RPN_MSB:
213 mc-> rpn &= ~0x7f<<7;
214 mc-> rpn |= PN_SET | (0x7f & ctlval[1])<<7;
215 mc->nrpn &= ~PN_SET;
216 return;
217 case MIDI_CTRL_NRPN_LSB:
218 mc->nrpn &= ~0x7f;
219 mc->nrpn |= PN_SET | (0x7f & ctlval[1]);
220 mc-> rpn &= ~PN_SET;
221 return;
222 case MIDI_CTRL_NRPN_MSB:
223 mc->nrpn &= ~0x7f<<7;
224 mc->nrpn |= PN_SET | (0x7f & ctlval[1])<<7;
225 mc-> rpn &= ~PN_SET;
226 return;
227 case MIDI_CTRL_DATA_ENTRY_LSB:
228 islsb = 1;
229 goto whichparm;
230 case MIDI_CTRL_DATA_ENTRY_MSB:
231 islsb = 0;
232 whichparm:
233 if ( 0 == ( (mc->rpn ^ mc->nrpn) & PN_SET ) )
234 return; /* exactly one must be current */
235 if ( mc->rpn & PN_SET ) {
236 key = mc->rpn;
237 c = RPN;
238 } else {
239 key = mc->nrpn;
240 c = NRPN;
241 }
242 key &= 0x3fff;
243 if ( 0x3fff == key ) /* 'null' parm# to lock out changes */
244 return;
245 enter14(mc, chan, c, key, islsb, ctlval[1]);
246 return;
247 case MIDI_CTRL_RPN_INCREMENT: /* XXX for later - these are a PITA to */
248 case MIDI_CTRL_RPN_DECREMENT: /* get right - 'right' varies by param */
249 /* see http://www.midi.org/about-midi/rp18.shtml */
250 return;
251 }
252
253 /*
254 * Channel mode, RPN, and NRPN operations have been ruled out.
255 * This is an ordinary control change.
256 */
257
258 key = ctlval[0];
259 c = classify(&key, &islsb);
260
261 switch ( c ) {
262 case CTL14:
263 enter14(mc, chan, c, key, islsb, ctlval[1]);
264 break;
265 case CTL7:
266 bkt = store_locate(mc->store, c, chan, key);
267 if ( !mc->accept_any_ctl_rpn ) {
268 if ( NULL == bkt )
269 break;
270 val = store_extract(bkt, c, chan, key);
271 if ( !(val&C7_SET) )
272 break;
273 }
274 store_update(mc->store, bkt, c, chan, key,
275 C7_SET | (0x7f & ctlval[1]));
276 mc->notify(mc->cookie, MIDICTL_CTLR, chan, key);
277 break;
278 case CTL1:
279 bkt = store_locate(mc->store, c, chan, key);
280 if ( !mc->accept_any_ctl_rpn ) {
281 if ( NULL == bkt )
282 break;
283 val = store_extract(bkt, c, chan, key);
284 if ( !(val&C1_SET) )
285 break;
286 }
287 store_update(mc->store, bkt, c, chan, key,
288 C1_SET | (ctlval[1]>63));
289 mc->notify(mc->cookie, MIDICTL_CTLR, chan, key);
290 break;
291 case RPN:
292 case NRPN:
293 break; /* won't see these - sop for gcc */
294 }
295 }
296
297 uint_fast16_t
298 midictl_read(midictl *mc, uint_fast8_t chan, uint_fast8_t ctlr,
299 uint_fast16_t dflt)
300 {
301 bucket *bkt;
302 uint_fast16_t key, val;
303 class c;
304 _Bool islsb;
305
306 key = ctlr;
307 c = classify(&key, &islsb);
308 switch ( c ) {
309 case CTL1:
310 bkt = store_locate(mc->store, c, chan, key);
311 if ( NULL == bkt ||
312 !(C1_SET&(val = store_extract(bkt, c, chan, key))) ) {
313 val = C1_SET | (dflt > 63);
314 store_update(mc->store, bkt, c, chan, key, val);
315 }
316 return (val & 1) ? 127 : 0;
317 case CTL7:
318 bkt = store_locate(mc->store, c, chan, key);
319 if ( NULL == bkt ||
320 !(C7_SET&(val = store_extract(bkt, c, chan, key))) ) {
321 val = C7_SET | (dflt & 0x7f);
322 store_update(mc->store, bkt, c, chan, key, val);
323 }
324 return val & 0x7f;
325 case CTL14:
326 _MIDICTL_ASSERT(!islsb);
327 return read14(mc, chan, c, key, dflt);
328 case RPN:
329 case NRPN:
330 break; /* sop for gcc */
331 }
332 return 0; /* sop for gcc */
333 }
334
335 uint_fast16_t
336 midictl_rpn_read(midictl *mc, uint_fast8_t chan, uint_fast16_t ctlr,
337 uint_fast16_t dflt)
338 {
339 return read14(mc, chan, RPN, ctlr, dflt);
340 }
341
342 uint_fast16_t
343 midictl_nrpn_read(midictl *mc, uint_fast8_t chan, uint_fast16_t ctlr,
344 uint_fast16_t dflt)
345 {
346 return read14(mc, chan, NRPN, ctlr, dflt);
347 }
348
349 static void
350 reset_all_controllers(midictl *mc, uint_fast8_t chan)
351 {
352 uint_fast16_t ctlr, key;
353 class c;
354 _Bool islsb;
355 bucket *bkt;
356
357 for ( ctlr = 0 ; ; ++ ctlr ) {
358 switch ( ctlr ) {
359 /*
360 * exempt by http://www.midi.org/about-midi/rp15.shtml:
361 */
362 case MIDI_CTRL_BANK_SELECT_MSB: /* 0 */
363 case MIDI_CTRL_CHANNEL_VOLUME_MSB: /* 7 */
364 case MIDI_CTRL_PAN_MSB: /* 10 */
365 continue;
366 case MIDI_CTRL_BANK_SELECT_LSB: /* 32 */
367 ctlr += 31; /* skip all these LSBs anyway */
368 continue;
369 case MIDI_CTRL_SOUND_VARIATION: /* 70 */
370 ctlr += 9; /* skip all Sound Controllers */
371 continue;
372 case MIDI_CTRL_EFFECT_DEPTH_1: /* 91 */
373 goto loop_exit; /* nothing more gets reset */
374 /*
375 * exempt for our own personal reasons:
376 */
377 case MIDI_CTRL_DATA_ENTRY_MSB: /* 6 */
378 continue; /* doesn't go to the store */
379 }
380
381 key = ctlr;
382 c = classify(&key, &islsb);
383
384 bkt = store_locate(mc->store, c, chan, key);
385 if ( NULL == bkt )
386 continue;
387 store_update(mc->store, bkt, c, chan, key, 0); /* no C*SET */
388 }
389 loop_exit:
390 mc->notify(mc->cookie, MIDICTL_RESET, chan, 0);
391 }
392
393 static void
394 enter14(midictl *mc, uint_fast8_t chan, class c, uint_fast16_t key,
395 _Bool islsb, uint8_t val)
396 {
397 bucket *bkt;
398 uint16_t stval;
399
400 bkt = store_locate(mc->store, c, chan, key);
401 stval = (NULL == bkt) ? 0 : store_extract(bkt, c, chan, key);
402 if ( !(stval&(C14MSET|C14LSET)) ) {
403 if ( !((NRPN==c)? mc->accept_any_nrpn: mc->accept_any_ctl_rpn) )
404 return;
405 }
406 if ( islsb )
407 stval = C14LSET | val | ( stval & ~0x7f );
408 else
409 stval = C14MSET | ( val << 7 ) | ( stval & ~0x3f80 );
410 store_update(mc->store, bkt, c, chan, key, stval);
411 mc->notify(mc->cookie, CTL14 == c ? MIDICTL_CTLR
412 : RPN == c ? MIDICTL_RPN
413 : MIDICTL_NRPN, chan, key);
414 }
415
416 static uint_fast16_t
417 read14(midictl *mc, uint_fast8_t chan, class c, uint_fast16_t key,
418 uint_fast16_t dflt)
419 {
420 bucket *bkt;
421 uint16_t val;
422
423 bkt = store_locate(mc->store, c, chan, key);
424 if ( NULL == bkt )
425 goto neitherset;
426
427 val = store_extract(bkt, c, chan, key);
428 switch ( val & (C14MSET|C14LSET) ) {
429 case C14MSET|C14LSET:
430 return val & 0x3fff;
431 case C14MSET:
432 val = C14LSET | (val & ~0x7f) | (dflt & 0x7f);
433 break;
434 case C14LSET:
435 val = C14MSET | (val & ~0x3f8) | (dflt & 0x3f8);
436 break;
437 neitherset:
438 case 0:
439 val = C14MSET|C14LSET | (dflt & 0x3fff);
440 }
441 store_update(mc->store, bkt, c, chan, key, val);
442 return val & 0x3fff;
443 }
444
445 /*
446 * Determine the controller class; ranges based on
447 * http://www.midi.org/about-midi/table3.shtml dated 1995/1999/2002
448 * and viewed 2 June 2006.
449 */
450 static class
451 classify(uint_fast16_t *key, _Bool *islsb) {
452 if ( *key < 32 ) {
453 *islsb = 0;
454 return CTL14;
455 } else if ( *key < 64 ) {
456 *islsb = 1;
457 *key -= 32;
458 return CTL14;
459 } else if ( *key < 70 ) {
460 *key -= 64;
461 return CTL1;
462 } /* 70-84 defined, 85-90 undef'd, 91-95 def'd */
463 return CTL7; /* 96-101,120- handled above, 102-119 all undef'd */
464 /* treat them all as CTL7 */
465 }
466
467 static void
468 notify_no_one(void *cookie, midictl_evt evt, uint_fast8_t chan, uint_fast16_t k)
469 {
470 }
471
472 #undef PN_SET
473 #undef C14MSET
474 #undef C14LSET
475 #undef C7_SET
476 #undef C1_SET
477
478 /*
479 * I M P L E M E N T A T I O N O F T H E S T O R E :
480 *
481 * MIDI defines a metric plethora of possible controllers, registered
482 * parameters, and nonregistered parameters: a bit more than 32k possible words
483 * to store. The saving grace is that only a handful are likely to appear in
484 * typical MIDI data, and only a handful are likely implemented by or
485 * interesting to a typical client. So the store implementation needs to be
486 * suited to a largish but quite sparse data set.
487 *
488 * For greatest efficiency, this could be implemented over the hash API.
489 * For now, it is implemented over libprop, which is not a perfect fit,
490 * but because that API does so much more than the hash API, this code
491 * has to do less, and simplicity is worth something.
492 *
493 * prop_numbers are uintmax_t's, which are wider than anything we store, and
494 * to reduce waste we want to fill them. The choice is to fill an entry
495 * with values for the same controller across some consecutive channels
496 * (rather than for consecutive controllers on a channel) because very few
497 * controllers are likely to be used, but those that are will probably be used
498 * on more than one channel.
499 */
500
501 #include <prop/proplib.h>
502 #include <sys/malloc.h>
503
504 #define KEYSTRSIZE 8
505 static void tokeystr(char s[static KEYSTRSIZE],
506 class c, uint_fast8_t chan, uint_fast16_t key);
507
508 static uint_fast8_t const packing[] = {
509 [CTL1 ] = 4*sizeof(uintmax_t)/sizeof(uint8_t),
510 [CTL7 ] = sizeof(uintmax_t)/sizeof(uint8_t),
511 [CTL14] = sizeof(uintmax_t)/sizeof(uint16_t),
512 [RPN ] = sizeof(uintmax_t)/sizeof(uint16_t),
513 [NRPN ] = sizeof(uintmax_t)/sizeof(uint16_t)
514 };
515
516 struct bucket {
517 union {
518 uintmax_t val;
519 uint8_t c7[sizeof(uintmax_t)/sizeof(uint8_t)];
520 uint16_t c14[sizeof(uintmax_t)/sizeof(uint16_t)];
521 } __packed un;
522 midictl_store *ms;
523 };
524
525 struct midictl_store {
526 prop_dictionary_t pd;
527 bucket bkt; /* assume any one client nonreentrant (for now?) */
528 };
529
530 static midictl_store *
531 store_init(void)
532 {
533 midictl_store *s;
534
535 s = _MIDICTL_MALLOC(sizeof *s, M_DEVBUF);
536 s->pd = prop_dictionary_create();
537 s->bkt.ms = s;
538 return s;
539 }
540
541 static void
542 store_done(midictl_store *s)
543 {
544 prop_object_release(s->pd);
545 _MIDICTL_FREE(s, M_DEVBUF);
546 }
547
548 static bucket *
549 store_locate(midictl_store *s, class c, uint_fast8_t chan, uint_fast16_t key)
550 {
551 char buf[8];
552 prop_number_t pn;
553
554 tokeystr(buf, c, chan, key);
555 pn = (prop_number_t)prop_dictionary_get(s->pd, buf);
556 if ( NULL == pn ) {
557 s->bkt.un.val = 0;
558 return NULL;
559 }
560 s->bkt.un.val = prop_number_integer_value(pn);
561 return &s->bkt;
562 }
563
564 static uint16_t
565 store_extract(bucket *b, class c, uint_fast8_t chan, uint_fast16_t key)
566 {
567 chan %= packing[c];
568 switch ( c ) {
569 case CTL1:
570 return 3 & (b->un.c7[chan/4]>>(chan%4)*2);
571 case CTL7:
572 return b->un.c7[chan];
573 case CTL14:
574 case RPN:
575 case NRPN:
576 break;
577 }
578 return b->un.c14[chan];
579 }
580
581 static void
582 store_update(midictl_store *s, bucket *b, class c, uint_fast8_t chan,
583 uint_fast16_t key, uint16_t value)
584 {
585 uintmax_t orig;
586 char buf[KEYSTRSIZE];
587 prop_number_t pn;
588 boolean_t success;
589 uint_fast8_t ent;
590
591 if ( NULL == b ) {
592 b = &s->bkt;
593 orig = 0;
594 } else
595 orig = b->un.val;
596
597 ent = chan % packing[c];
598
599 switch ( c ) {
600 case CTL1:
601 b->un.c7[ent/4] &= ~(3<<(ent%4)*2);
602 b->un.c7[ent/4] |= (3&value)<<(ent%4)*2;
603 break;
604 case CTL7:
605 b->un.c7[ent] = value;
606 break;
607 case CTL14:
608 case RPN:
609 case NRPN:
610 b->un.c14[ent] = value;
611 break;
612 }
613
614 if ( orig == b->un.val )
615 return;
616
617 tokeystr(buf, c, chan, key);
618
619 if ( 0 == b->un.val )
620 prop_dictionary_remove(s->pd, buf);
621 else {
622 pn = prop_number_create_integer(b->un.val);
623 _MIDICTL_ASSERT(NULL != pn);
624 success = prop_dictionary_set(s->pd, buf, pn);
625 _MIDICTL_ASSERT(success);
626 prop_object_release(pn);
627 }
628 }
629
630 static void
631 tokeystr(char s[static KEYSTRSIZE],
632 class c, uint_fast8_t chan, uint_fast16_t key)
633 {
634 snprintf(s, KEYSTRSIZE, "%x%x%x", c, chan/packing[c], key);
635 }
636
637 #if defined(_MIDICTL_MAIN)
638 void
639 dumpstore(void)
640 {
641 char *s = prop_dictionary_externalize(mc.store->pd);
642 printf("%s", s);
643 free(s);
644 }
645 #endif
646