About a year ago,
I wrote about how I thought the use of unique IDs to provide persistent references to MIDI devices in CoreMIDI was a terrible idea. To summarize, theyre a mechanism that solves an exceptional case which gets thrown into a prominent role of an API. They achieve nothing that a manufacturer name, model name, and system exclusive device ID tuple cannot. The latter makes much more sense to the user, and solves further problems that unique IDs do not. I was busy finishing some work on MyJazzBand then. But today Ive worked out some more of my design of a better API.
First, we need a function to query the MIDI system for a list of devices of a given type, say:
CFPropertyListRef Devices(int types, int options = 0);
Possible types are devices correspond to source endpoints, destination endpoints, and endpoint pairs in the sample code
/Developer/Examples/CoreAudio/PublicUtility/CAMIDIEndpoints.cpp. Notice that it is
common practice to refer to source and destination endpoints by the containing devices names. Endpoint pairs roughly correspond to Sysex devices in
my previous design. Other possible types might be external devices, virtual sources, and virtual destinations. To support specialized applications, it may be necessary to further distinguish devices that can and cannot respond to sysex messages, ones to which a NameConfig property can and cannot be attached, etc. The function protocol is general enough to be expanded this way.
What kind of value does the function
Devices return? An array of external handles of devices. These external handles are analogous to (string) pathnames for accessing files. We use them to open a device to obtain a (run-time) device reference to pass to functions that perform actions on that device, such as sending MIDI messages to it. But since our API must handle renaming and reorganization of the MIDI setup, the external handles need to be slightly more complex than string names. If I were using unique IDs, these external handles will be name-unique ID pairs.
Each external handle will be a CoreFoundation dictionary with at least a value for the key
DisplayName. And since it is a CF property list, such an external handle can easily be saved to and restored from files. The display name will be what is normal displayed as name of the device. The display names of the external handles in the array returned by
Devices can be used in popup buttons to let the user select a destination/source/sysex/etc. device. Determination of a display name for a device will follow
Apples recommendations, with perhaps a few enhancements. For example, source and destination endpoints connected to multiple devices will use names that are concatenations of the device names, but these will be
alphabetically ordered before they are concatenated. Doing so will make it possible to resolve names again correctly when the MIDI setup changes.
Most external handles will also have values for the keys
Manufacturer and
Model. Some will have values for the key
Sysex Device ID, if these have been specified in Audio MIDI setup. The latter will be useful for multiple devices of the same manufacturer and model in the setup. Endpoints connected to multiple devices will have a value for the key
Devices. This value will be an array of dictionaries of Manufacturer, Model, and possibly Sysex Device ID. Virtual sources and destinations will have none of these, but only display names. Ports of MIDI interfaces not connected to any external devices in Audio MIDI Setup will also only have display names.
Second, we need an algorithm for resolving names: given an external handle, determine whether the ones in a given array (one that the function
Devices returns) matches it. An exact match of all values in the dictionary will be tried first. If that fails, we try to match Manufacturer and Model if this value pair uniquely identifies the device in the array, and Manufacturer, Model, and Sysex Device ID otherwise.
Once we can resolve names, we be build functions thatll operate on devices, such as send MIDI message to it. An example of a set of functions for sending and receiving sysex messages is
one I designed a while ago. The API will look a little now as I have a few new ideas on how to redo this. Also using the SWIG and CF property list code Ive been working on, the new library will be callable from different languages. Ill at least be interested in making sure the Python and Scheme (specifically Chicken) bindings work.
I converted yesterdays Unicode test into
an Xcode project. Now the Python and Chicken extensions can be built by simply selecting and building the Python Extension and Chicken Extension targets in this project, respectively. Shell script targets (named Python Swig and Chicken Swig) handle the invocation of SWIG to generate the function wrappers. The Chicken Swig script performs an additional step of compiling the Scheme wrapper into C.
Files generated by SWIG (and the C version of the Scheme wrapper) are placed in the directories
build/python and
build/chicken. The dynamic libraries for the extensions are placed in
build/{python,chicken}/Debug or
build/{python,chicken}/Release, depending on the active build configuration. Therefore the entire
build directory can be removed to manually clean the project.
Unicode, Chicken, the Utf8 Egg, Bytevector...
In the implementation of the Chicken-CoreFoundation property list typemaps,
CFStrings are converted to and from Scheme strings. The utf8 egg (extension library/package) enables Scheme strings in Chicken to be UTF-8 encoded. Heres a sample interaction showing how to use UTF-8 strings in Chicken with a few SWIG-wrapped functions and my newly-written typemaps.
~/Documents/SWIG/unicode-test/chicken$ csi
______ __ __ __
| | |--.|__|.----.| |--.-----.-----.
| ---| || || __|| <| -__| |
|______|__|__||__||____||__|__|_____|__|__|
Version 2, Build 0 - macosx-unix-gnu-ppc - [ dload ]
(c)2000-2005 Felix L. Winkelmann
#;1> (load-library 'example "simple.bundle")
; loading library example ...
#t
#;2> (use syntax-case)
; loading /usr/local/lib/chicken/syntax-case.so ...
; loading /usr/local/lib/chicken/syntax-case-chicken-macros.scm ...
#;3> (use utf8)
; loading /usr/local/lib/chicken/utf8.so ...
; loading /usr/local/lib/chicken/utf8-lolevel.so ...
; loading /usr/local/lib/chicken/byte-string.so ...
#;4> (import utf8)
; visiting /usr/local/lib/chicken/utf8.scm ...
#;5> (define x "鎮江排骨")
#;6> x
"鎮江排骨"
#;7> (string-length x)
4
#;8> (string->list x)
(#\u93ae #\u6c5f #\u6392 #\u9aa8)
#;9> (show x)
\u93ae\u6c5f\u6392\u9aa8
#;10> (show (vector x))
<CFArray 0x1100770 [0xa0728150]>{type = immutable, count = 1, values = (
0 : <CFString 0x1100ef0 [0xa0728150]>{contents = "\u93ae\u6c5f\u6392\u9aa8"}
)}
#;11> (noop x)
"鎮江排骨"
#;12> (begin (print (xml x)) #f)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<string>鎮江排骨</string>
</plist>
#f
#;13> ^D
~/Documents/SWIG/unicode-test/chicken$
Objects of type
CFData are converted to and from Chicken byte vectors, which are supported by the unit
lolevel. Heres a sample interaction to illustrate their use.
~/Documents/SWIG/unicode-test/chicken$ csi -quiet
#;1> (load-library 'example "simple.bundle")
; loading library example ...
#t
#;2> (use lolevel)
; loading library lolevel ...
#;3> (define x (byte-vector 1 2 3 4 5))
#;4> x
#<byte-vector>
#;5> (show x)
<CFData 0x1100770 [0xa0728150]>{length = 5, capacity = 5, bytes = 0x0102030405}
#;6> (noop x)
#<byte-vector>
#;7> (byte-vector->list (noop x))
(1 2 3 4 5)
#;8> (begin (print (xml x)) #f)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<data>
AQIDBAU=
</data>
</plist>
#f
#;9> ^D
~/Documents/SWIG/unicode-test/chicken$
In a new
unicode-test project I put together today, the Python and Chicken versions of the typemaps are now chosen automatically depending on the target language specified to SWIG. Therefore both the Python and Chicken extensions are built from a single set of source files containing the functions to be wrapped and their interface specifications (i.e.,
example.c and
example.i). To add support for another programming language, simply implement a new set of typemaps for it. Of course when this is done, support for the new language will be available for any sets of functions to be wrapped.
Unicode, Python, CFString, CFData, ...
The MIDI library will need a way to pass byte-array arguments and return values, for example for sysex data, so
Ive added support for
CFData to the SWIG typemaps. So now, objects of type
CFString are converted to and from Python Unicode objects, while those of type
CFData are converted to and from Python strings. Strings
are used in Python to represent arrays of bytes (in addition to ASCII strings), and null characters are allowed in the middle of a string.
The code is also updated to perform error checking using the Try-Catch and Throw mechanism of
cexcept.
To use Unicode in Python in Terminal.app, make sure that the display character set encoding is set to Unicode (UTF-8) in Window Settings.... Heres a sample interaction:
~/Documents/SWIG/unicode-test$ python
Python 2.3.5 (#1, Mar 20 2005, 20:38:20)
[GCC 3.3 20030304 (Apple Computer, Inc. build 1809)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import example
>>> x = 'abc\0def'
>>> example.show(x)
<CFData 0x30b210 [0xa0728150]>{length = 7, capacity = 7, bytes = 0x61626300646566}
>>> example.noop(x)
'abc\x00def'
>>> y = unicode('麻婆豆腐', 'utf-8')
>>> y
u'\u9ebb\u5a46\u8c46\u8150'
>>> print y.encode('utf-8')
麻婆豆腐
>>> example.show(y)
\u9ebb\u5a46\u8c46\u8150
>>> example.show([y])
<CFArray 0x309020 [0xa0728150]>{type = immutable, count = 1, values = (
0 : <CFString 0x307000 [0xa0728150]>{contents = "\u9ebb\u5a46\u8c46\u8150"}
)}
>>> example.noop(y)
u'\u9ebb\u5a46\u8c46\u8150'
>>> print example.xml(y).encode('utf-8')
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<string>麻婆豆腐</string>
</plist>
>>> ^D