forked from thestk/stk
-
Notifications
You must be signed in to change notification settings - Fork 0
/
SKINI.txt
388 lines (330 loc) · 18.5 KB
/
SKINI.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
This describes the latest (version 1.1) implementation of SKINI.
Synthesis toolKit Instrument Network Interface
for the Synthesis Toolkit in C++ by Perry R. Cook.
*********************************
* Too good to be true? *
* Have control and read it too? *
* A SKINI Haiku. *
*********************************
Profound thanks to Dan trueman, Brad Garton, and
Gary Scavone 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 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. SKINI 1.1 represents a profound advance beyond
versions 0.8 and 0.9 (the first versions), future SKINI's might
reflect some changes. Compatibility with prior scorefiles
will be attempted, but there aren't that many scorefiles out
there yet.
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
SKINItbl.h 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
version 1.1.
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 non-MIDI 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
SKINItbl.h.
The other important file used by SKINI is SKINImsg.h, 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 SKINI
Skini.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.
SKINImsg.h should be included by anything wanting to use the
Skini.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.
SKINItbl.h is used only by the SKINI parser object (Skini.cpp).
In the file SKINItbl.h, 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 read-ability, 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 future versions, 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.
A time field can be either delta-time (most common and the only one
supported in version 0.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
100 ms, 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 SKINI parser
doesn't know about time, however. The WvOut 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 SKINItbl.h 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 SKINI, by P. Cook 1999
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 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
7) The SKINItbl.h File, How Messages are Parsed:
The SKINItbl.h file contains an array of structures which
are accessed by the parser object Skini.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<n> 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
256 bytes 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 SKINItbl.h 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 SKINImsg.h 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 spec 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 spec 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.
Skini score;
Skini::Message message;
instrument = new Mandolin(50.0);
score.setFile( argv[1] );
while ( score.nextMessage( message ) != 0 ) {
tempDouble = message.time;
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 * Stk::sampleRate() );
for ( i=0; i<tempLong; i++ ) {
output.tick( instrument->tick() );
}
tempDouble3 = message.floatValues[1] * NORM_MIDI;
if ( message.type == __SK_NoteOn_ ) {
if ( tempDouble3 == 0.0 ) {
tempDouble3 = 0.5;
instrument->noteOff( tempDouble3 );
}
else {
tempLong = message.intValues[0];
tempDouble2 = Midi2Pitch[tempLong];
instrument->noteOn( tempDouble2, tempDouble3 );
}
}
else if ( message.type == __SK_NoteOff_ ) {
instrument->noteOff( tempDouble3 );
}
else if ( message.type == __SK_ControlChange_ ) {
tempLong = message.intValues[0];
instrument->controlChange( tempLong, tempDouble3 );
}
}
When a SKINI score is passed to a Skini object using the
Skini::setFile() function, valid messages are read from
the file and returned using the Skini::nextMessage() function.
A Skini::Message structure contains all the information parsed
from a single SKINI message. A returned message type of zero
indicates either an invalid message or the end of a scorefile.
The "time" member of a Skini::Message is 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, 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.