SKINI0.9

Synthesis toolKit Instrument Network Interface


*********************************
Too good to be true?
Have control and read it too?
A SKINI Haiku.
*********************************

This describes the 0.9 implementation of SKINI:

Synthesis toolKit Instrument Network Interface

for  the Synthesis Toolkit in C++ by Perry R. Cook 1996.

Profound thanks to Dan Trueman and Brad Garton for input on
this revision.  Thanks also to MIDI, the NeXT MusicKit, ZIPI
and all the creators and modifiers of these for good bases 
upon/from which to build and depart.

1)  MIDI Compatibility

    SKINI was designed to be MIDI compatible wherever possible,
    and extend MIDI in incremental, then maybe profound ways.  
    
    Differences from MIDI, and motivations, include:

        Text-based messages are used, with meaningful names 
        wherever possible.  This allows any language or system
        capable of formatted printing to generate SKINI. 
        Similarly, any system capable of reading in a string 
        and turning delimited fields into strings, floats, 
        and ints can consume SKINI for control.  More importantly,
        humans can actually read, and even write if they want,
        SKINI files and streams.  Use an editor and search/
        replace or macros to change a channel or control number. 
        Load a SKINI score into a spread sheet to apply 
        transformations to time, control parameters, MIDI 
        velocities, etc.  Put a monkey on a special typewriter
        and get your next great work.  Life's too short to debug
        bit/nybble packed variable length mumble messages.  Disk
        space gets cheaper, available bandwidth increases, music
        takes up so little space and bandwidth compared to video
        and grapics.  Live a little.   

        Floating point numbers are used wherever possible.
        Note Numbers, Velocities, Controller Values, and
        Delta and Absolute Times are all represented and
        scanned as ASCII double-precision floats.  MIDI byte
        values are preserved, so that incoming MIDI bytes
        from an interface can be put directly into SKINI
        messages.  60.0 or 60 is middle C, 127.0 or 127 is 
        maximum velocity etc.  But, unlike MIDI, 60.5 can
        cause a 50cent sharp middle C to be played.  As with
        MIDI byte values like velocity, use of the integer and 
        SKINI-added fractional parts is up to the implementor
        of the algorithm being controlled by SKINI messages.
        But the extra precision is there to be used or ignored.

2)  WHY SKINI?

    SKINI was designed to be extensable and hackable for a number
    of applications: imbedded synthesis in a game or VR simulation,
    scoring and mixing tasks, real-time and non-real time applications 
    which could benefit from a controllable sound synthesis, 
    JAVA controlled synthesis, or eventually maybe JAVA synthesis, 
    etc.  SKINI is not intended to be "the mother of scorefiles," 
    but since the entire system is based on text representations 
    of names, floats, and ints, converters from one scorefile 
    language to SKINI, or back, should be easily created.

    I am basically a bottom-up designer with an awareness of top-
    down design ideas, so SKINI above all reflects the needs of my 
    particular research and creative projects as they have arisen and
    developed.  SKINI09 represents a profound advance beyond SKINI08
    (the first version), but it is likely that SKINI1.0x will 
    reflect some changes.  Compatibility with prior scorefiles
    will be attempted, but there aren't that many scorefiles out
    there yet.  The one thing I will attempt to keep in mind is 
    enough consistency to allow the creation of a SKINI0x to SKINIx0
    file and stream converter object.  SKINI09 should be fully 
    compatible with SKINI08.
    
3)  SKINI MESSAGES

    A basic SKINI message is a line of text.  There are only three
    required fields, the message type (an ASCII name), the time (either
    delta or absolute), and the channel number.  Don't freak out and
    think that this is MIDI channel 0-15 (which is supported), because
    the channel number is scanned as a long int.  Channels could be socket
    numbers, machine IDs, serial numbers, or even unique tags for each
    event in a synthesis.  Other fields might be used, as specified in the
    SKINI09.tbl file.  This is described in more detail later.
    
    Fields in a SKINI line are delimited by spaces, commas, or
    tabs.  The SKINI parser only operates on a line at a time,
    so a newline means the message is over.  Multiple messages are
    NOT allowed directly on a single line (by use of the ; for 
    example in C).  This could be supported, but it isn't in 0.9.

    Message types include standard MIDI types like NoteOn, NoteOff, 
    ControlChange, etc.  MIDI extension message types (messages 
    which look better than MIDI but actually get turned into 
    MIDI-like messages) include LipTension, StringDamping, etc.
    NonMIDI message types include SetPath (sets a path for file
    use later), and OpenReadFile (for streaming, mixing, and applying 
    effects to soundfiles along with synthesis, for example).
    Other NonMIDI message types include Trilling, HammerOn, etc. (these
    translate to gestures, behaviors, and contexts for use by 
    intellegent players and instruments using SKINI).  Where possible
    I will still use these as MIDI extension messages, so foot
    switches, etc. can be used to control them in real time.

    All fields other than type, time, and channel are optional, and the 
    types and useage of the additional fields is defined in the file 
    SKINI09.tbl.  
    
    The other important file used by SKINI is SKINI09.msg, which is a
    set of #defines to make C code more readable, and to allow reasonably
    quick re-mapping of control numbers, etc..  All of these defined 
    symbols are assigned integer values.  For JAVA, the #defines could 
    be replaced by declaration and assignment statements, preserving 
    the look and behavior of the rest of the code.

4)  C Files Used To Implement SKINI09

    SKINI09.cpp is an object which can either open a SKINI file, and
    successively read and parse lines of text as SKINI strings, or 
    accept strings from another object and parse them.  The latter 
    functionality would be used by a socket, pipe, or other connection
    receiving SKINI messages a line at a time, usually in real time,
    but not restricted to real time.

    SKINI09.msg should be included by anything wanting to use the 
    SKINI09.cpp object.  This is not mandatory, but use of the __SK_blah_
    symbols which are defined in the .msg file will help to ensure 
    clarity and consistency when messages are added and changed.

    SKINI09.tbl is used only by the SKINI parser object (SKINI09.cpp).
    In the file SKINI09.tbl, an array of structures is declared and    
    assigned values which instruct the parser as to what the message
    types are, and what the fields mean for those message types.
    This table is compiled and linked into applications using SKINI, but
    could be dynamically loaded and changed in a future version of 
    SKINI.

5)  SKINI Messages and the SKINI Parser:

    The parser isn't all that smart, but neither am I.  Here are the
    basic rules governing a valid SKINI message:

    a)  If the first (non-delimiter (see c)) character in a SKINI 
        string is '/' that line is treated as a comment and echoed 
        to stdout.

    b)  If there are no characters on a line, that line is treated
        as blank and echoed to stdout.  Tabs and spaces are treated
        as non-characters.

    c)  Spaces, commas, and tabs delimit the fields in a SKINI 
        message line.  (We might allow for multiple messages per
        line later using the semicolon, but probably not.  A series 
        of lines with deltaTimes of 0.0 denotes simultaneous events.  
        For Readability, multiple messages per line doesn't help much, 
        so it's unlikely to be supported later).

    d)  The first field must be a SKINI message name.  (like NoteOn). 
        These might become case-insensitive in SKINI09+, so don't plan
        on exciting clever overloading of names (like noTeOn being
        different from NoTeON).  There can be a number of leading
        spaces or tabs, but don't exceed 32 or so.

    e)  The second field must be a time specification in seconds.  
        For real-time applications, this field can be 0.0.  A time 
        field can be either delta-time (most common and the only one 
        supported in SKINI0.8), or absolute time.  Absolute time 
        messages have an '=' appended to the beginning of the floating 
        point number with no space.  So 0.10000 means delta time of 
        100ms., while =0.10000 means absolute time of 100 ms.  Absolute
        time messages make most sense in score files, but could also be 
        used for (loose) synchronization in a real-time context.  Real 
        time messages should be time-ordered AND time-correct.  That is, 
        if you've sent 100 total delta-time messages of 1.0 seconds, and
        then send an absolute time message of =90.0 seconds, or if you
        send two absolute time messages of =100.0 and =90.0 in that
        order, things will get really fouled up.  The SKINI0.9 parser 
        doesn't know about time, however.  The RawWvOut device is the 
        master time keeper in the Synthesis Toolkit, so it should be 
        queried to see if absolute time messages are making sense.  
        There's an example of how to do that later in this document.  
        Absolute times are returned by the parser as negative numbers
        (since negative deltaTimes are not allowed).

    f)  The third field must be an integer channel number.  Don't go
        crazy and think that this is just MIDI channel 0-15 (which is 
        supported).  The channel number is scanned as a long int.  Channels
        0-15 are in general to be treated as MIDI channels.  After that
        it's wide open.  Channels could be socket numbers, machine IDs, 
        serial numbers, or even unique tags for each event in a synthesis.
        A -1 channel can be used as don't care, omni, or other functions
        depending on your needs and taste.

    g)  All remaining fields are specified in the SKINI09.tbl file.
        In general, there are maximum two more fields, which are either
        SK_INT (long), SK_DBL (double float), or SK_STR (string).  The
        latter is the mechanism by which more arguments can be specified
        on the line, but the object using SKINI must take that string
        apart (retrived by using getRemainderString()) and scan it.
        Any excess fields are stashed in remainderString.
    
6)  A Short SKINI File:

        /*  Howdy!!! Welcome to SKINI09, by P. Cook 1996

        NoteOn          0.000082 2 55 82
        NoteOff         1.000000 2 55 0
        NoteOn          0.000082 2 69 82
        StringDetune    0.100000 2    10
        StringDetune    0.100000 2    30
        StringDetune    0.100000 2    50
        NoteOn          0.000000 2 69 82
        StringDetune    0.100000 2    40
        StringDetune    0.100000 2    22
        StringDetune    0.100000 2    12
        //
        StringDamping   0.000100 2     0.0
        NoteOn          0.000082 2 55 82
        NoteOn          0.200000 2 62 82
        NoteOn          0.100000 2 71 82
        NoteOn          0.200000 2 79 82
        NoteOff         1.000000 2 55 82
        NoteOff         0.000000 2 62 82
        NoteOff         0.000000 2 71 82
        NoteOff         0.000000 2 79 82
        StringDamping  =4.000000 2     127.0
        NoteOn          0.000082 2 55 82
        NoteOn          0.200000 2 62 82
        NoteOn          0.100000 2 71 82
        NoteOn          0.200000 2 79 82
        NoteOff         1.000000 2 55 82
        NoteOff         0.000000 2 62 82
        NoteOff         0.000000 2 71 82
        NoteOff         0.000000 2 79 82

7)  The SKINI09.tbl File, How Messages are Parsed

    The SKINI09.tbl file contains an array of structures which
    are accessed by the parser object SKINI09.cpp.  The struct is:
    
        struct SKINISpec { char messageString[32];
                   long  type;
                   long data2;
                   long data3;
                 };

    so an assignment of one of these structs looks like:
        
        MessageStr$      ,type, data2, data3,                         
                                                                           
            type is the message type sent back from the SKINI line parser.
            data is either                                              
                 NOPE    : field not used, specifically, there aren't going                                           
                           to be any more fields on this line.  So if there
                           is is NOPE in data2, data3 won't even be checked
                 SK_INT  : byte (actually scanned as 32 bit signed long int)
                             If it's a MIDI data field which is required to
                             be an integer, like a controller number, it's 
                             0-127.  Otherwise) get creative with SK_INTs  
                 SK_DBL  : double precision floating point.  SKINI uses these
                           in the MIDI context for note numbers with micro   
                           tuning, velocities, controller values, etc.       
                 SK_STR  : only valid in final field.  This allows (nearly)  
                           arbitrary message types to be supported by simply 
                           scanning the string to EndOfLine and then passing 
                           it to a more intellegent handler.  For example,   
                           MIDI SYSEX (system exclusive) messages of up to   
                           256bytes can be read as space-delimited integers  
                           into the 1K SK_STR buffer.  Longer bulk dumps,    
                           soundfiles, etc. should be handled as a new       
                           message type pointing to a FileName, Socket, or 
                           something else stored in the SK_STR field, or 
                           as a new type of multi-line message.                                          
        
    Here's a couple of lines from the SKINI09.tbl file

 {"NoteOff"          ,        __SK_NoteOff_,               SK_DBL,  SK_DBL},
 {"NoteOn"           ,         __SK_NoteOn_,               SK_DBL,  SK_DBL},
 
 {"ControlChange"    ,  __SK_ControlChange_,               SK_INT,  SK_DBL},
 {"Volume"           ,  __SK_ControlChange_, __SK_Volume_        ,  SK_DBL},

 {"StringDamping"    ,  __SK_ControlChange_, __SK_StringDamping_ ,  SK_DBL},
 {"StringDetune"     ,  __SK_ControlChange_, __SK_StringDetune_  ,  SK_DBL},

    The first three are basic MIDI messages.  The first two would cause the
    parser, after recognizing a match of the string "NoteOff" or "NoteOn",
    to set the message type to 128 or 144 (__SK_NoteOff_ and __SK_NoteOn_
    are #defined in the file SKINI09.msg to be the MIDI byte value, without
    channel, of the actual MIDI messages for NoteOn and NoteOff).  The parser  
    would then set the time or delta time (this is always done and is
    therefore not described in the SKINI Message Struct).  The next two 
    fields would be scanned as double-precision floats and assigned to 
    the byteTwo and byteThree variables of the SKINI parser.  The remainder 
    of the line is stashed in the remainderString variable.  
    
    The ControlChange line is basically the same as NoteOn and NoteOff, but
    the second data byte is set to an integer (for checking later as to
    what MIDI control is being changed).  
    
    The Volume line is a MIDI Extension message, which behaves like a  
    ControlChange message with the controller number set explicitly to 
    the value for MIDI Volume (7).  Thus the following two lines would
    accomplish the same changing of MIDI volume on channel 2:

    ControlChange  0.000000 2 7 64.1
    Volume         0.000000 2   64.1

    I like the 2nd line better, thus my motivation for SKINI in the first
    place.
    
    The StringDamping and StringDetune messages behave the same as 
    the Volume message, but use Control Numbers which aren't specifically
    nailed-down in MIDI.  Note that these Control Numbers are carried
    around as long ints, so we're not limited to 0-127.  If, however,
    you want to use a MIDI controller to play an instrument, using 
    controller numbers in the 0-127 range might make sense.

8)  Objects using SKINI
    
    Here's a simple example of code which uses the SKINI object
    to read a SKINI file and control a single instrument.
    
        instrument = new Mandolin(50.0);
        score = new SKINI09(argv[1]);
        while(score->getType() > 0) {
            tempDouble = score->getDelta(); 
            if (tempDouble < 0)     {
                tempDouble = - tempDouble;
                tempDouble = tempDouble - output.getTime();
                if (tempDouble < 0) {
                   printf("Bad News Here!!!  Backward Absolute Time Required.\n");
                   tempDouble = 0.0;
                }
            }
            tempLong = (long) (tempDouble * SRATE);
            for (i=0;itick());
            }
            tempDouble3 = score->getByteThree();
            if (score->getType()== __SK_NoteOn_ )        {
                tempDouble3 *= NORM_7;
                if (score->getByteThree() == 0)  {
                    tempDouble3 = 0.5;
                    instrument->noteOff(tempDouble3);
                }
                else {
                    tempLong = (int) score->getByteTwo();
                    tempDouble2 = __MIDI_To_Pitch[tempLong];
                    instrument->noteOn(tempDouble2,tempDouble3);
                }
            }
            else if (score->getType() == __SK_NoteOff_) {
                tempDouble3 *= NORM_7;
                instrument->noteOff(tempDouble3);
            }
            else if (score->getType() == __SK_ControlChange_)        {
                tempLong = score->getByteTwoInt();
                instrument->controlChange(tempLong,temp3.0);
            }
            score->nextMessage();
        }

    When the score (SKINI09 object) object is created from the
    filename in argv[1], the first valid command line is read
    from the file and parsed.
    
    The score->getType() retrieves the messageType.  If this is 
    -1, there are no more valid messages in the file and the 
    synthesis loop terminates.  Otherwise, the message type is 
    returned.

    getDelta() retrieves the deltaTime until the current message
    should occur.  If this is greater than 0, synthesis occurs
    until the deltaTime has elapsed.  If deltaTime is less than
    zero, the time is interpreted as absolute time and the output
    device is queried as to what time it is now.  That is used to
    form a deltaTime, and if it's positive we synthesize.  If
    it's negative, we print an error and pretend this never 
    happened and we hang around hoping to eventually catch up.

    The rest of the code sorts out message types NoteOn, NoteOff
    (including NoteOn with velocity 0), and ControlChange.  The
    code implicitly takes into account the integer type of the
    control number, but all other data is treated as double float.

    The last line reads and parses the next message in the file.