|
|
1.1 ! root 1: .so ../ADM/mac ! 2: .XX music 477 "Computer Music Under the 10th Edition UNIX System" ! 3: .nr dP 1 ! 4: .nr dV 1.5p ! 5: .de FG ! 6: .ce ! 7: Figure \\$1. \\$2 ! 8: .SP .5 ! 9: .. ! 10: .TL ! 11: Computer Music under the 10th Edition ! 12: .UX ! 13: System ! 14: .AU ! 15: T. J. Killian ! 16: .AI ! 17: .MH ! 18: .AB ! 19: We describe an evolving computer music system that draws upon many of ! 20: the novel facilities in the Research ! 21: .UX ! 22: system, as well as the standard repertoire of ! 23: familiar tools. ! 24: The Teletype 5620 bitmap display serves both as the user's ! 25: terminal and real-time controller. The ! 26: .I mux ! 27: window system is used to download a ! 28: .SM MIDI ! 29: interface driver that ! 30: services other windows (by direct code sharing) and ! 31: host processes (which write on the driver's control stream). ! 32: We presently support several ! 33: .SM MIDI ! 34: devices: the Yamaha ! 35: .SM DX7 , ! 36: .SM TX816 , ! 37: .SM FB01 , ! 38: and ! 39: .SM SPX90 . ! 40: .PP ! 41: Window programs include a piano-roll-style score facility and a ! 42: virtual keyboard. Host programs include a music compiler, ! 43: .I m , ! 44: that converts an ! 45: .SM ASCII ! 46: score notation into ! 47: .SM MIDI ! 48: events; ! 49: it is based on ! 50: .I lex ! 51: and ! 52: .I yacc, ! 53: making it very easy to modify in response to user needs. ! 54: There are also several filters that perform simple transformations ! 55: (e.g., time and pitch translation) on ! 56: .SM MIDI ! 57: files. The latter are ! 58: .SM ASCII , ! 59: so that text filters (especially ! 60: .I awk , ! 61: .I sed , ! 62: and ! 63: .I sort) ! 64: can be used for sound manipulation. ! 65: .AE ! 66: .2C ! 67: .NH ! 68: Introduction \- the ! 69: .SM MIDI ! 70: standard. ! 71: .PP ! 72: Computer-music applications have taken off in recent years, largely ! 73: due to the introduction of the ! 74: .SM MIDI ! 75: (Musical Instrument Digital Interface) ! 76: standard |reference(midi standard). ! 77: .SM MIDI ! 78: has made it possible, with very modest hardware, ! 79: to interface a computer to synthesizers and other equipment, with broad ! 80: compatibility among manufacturers. ! 81: A full discussion of the ! 82: .SM MIDI ! 83: standard is out of place here; we will simply ! 84: give some of its essential characteristics. ! 85: .PP ! 86: .SM MIDI ! 87: uses an asynchronous serial protocol at 31.25 Kbaud to transmit \%8-bit ! 88: data bytes over a \%5-mA current loop (thus, to the programmer, it looks like an ! 89: .SM RS232 ! 90: line). Data flow on any given cable is unidirectional. ! 91: .I Status ! 92: and ! 93: .I data ! 94: bytes are distinguished by the high-order bit being set or clear, respectively. ! 95: A status byte encodes a ! 96: .I function, ! 97: and (usually) a ! 98: .SM MIDI ! 99: .I "channel number" ! 100: in the range 1 to 16. ! 101: Bytes are grouped logically into ! 102: .I messages ! 103: that consist of a status byte and (except as noted below) 1 or 2 data bytes. ! 104: For example, the \%3-byte message (\c ! 105: .CW 0x90,\ 0x3c,\ 0x40 ) ! 106: says, ``On channel 1, ! 107: turn on note number 60 (middle C) with a volume of 64 (mf).'' ! 108: In most cases the size of a message is determined by its status byte, so ! 109: the latter may be omitted if it is the same as the status byte most ! 110: recently transmitted. ! 111: .PP ! 112: The messages most commonly used in performance, such as those that ! 113: select a synthesizer's pre-programmed voices, or ! 114: turn notes on and off, are fixed by the ! 115: .SM MIDI ! 116: standard. An escape mechanism ! 117: is provided to allow control sequences ! 118: peculiar to the equipment of a given manufacturer. This ! 119: .I "system exclusive" ! 120: message has the form (\c ! 121: .CW 0xf0, ! 122: .I "ident, data," ! 123: \&..., ! 124: .CW 0xf7 ");" ! 125: .I ident ! 126: is a manufacturer's code assigned by the standards committee; the ! 127: .I data ! 128: is arbitrary (and can be of any length). ! 129: .PP ! 130: A device typically has three ! 131: .SM MIDI ! 132: sockets, labeled ! 133: .SM IN , ! 134: .SM OUT , ! 135: and ! 136: .SM THRU . ! 137: Messages received at the ! 138: .SM IN ! 139: socket are acted upon in real time if the device's ! 140: .SM MIDI ! 141: channel number matches that of the message. ! 142: In any case, the data are buffered directly to the ! 143: .SM THRU ! 144: socket to allow daisy-chaining. ! 145: An instrument such as a ! 146: keyboard may send an independent stream of messages to its ! 147: .SM OUT ! 148: socket. A typical ! 149: .SM MIDI ! 150: performance consists of (mostly) note-on and note-off messages ! 151: emitted by the ``performer'' with proper timing. ! 152: .PP ! 153: There are several problems with ! 154: .SM MIDI , ! 155: the most serious of which ! 156: is probably its limited bandwidth. Chords consisting of, say, ten or twelve ! 157: notes become noticeably arpeggiated, and changes in timbre that might ! 158: require a long system-exclusive message cannot be fitted into fast passages. ! 159: Lack of flow control makes this problem even worse. ! 160: .SM MIDI ! 161: is also tightly bound to the twelve-tone Western scale; it is sometimes possible ! 162: to work around this, again at the expense of bandwidth. ! 163: .NH ! 164: The ! 165: .SM MIDI ! 166: device driver. ! 167: .PP ! 168: Mark Kahrs and I built a ! 169: .SM MIDI ! 170: interface board that plugs into the parallel ! 171: .SM I/O ! 172: port of the Teletype 5620 bitmap terminal |reference(kahrs 5620). ! 173: The 5620 has a \%32-bit ! 174: microprocessor with 1 Mb of memory, and runs the ! 175: .I mux ! 176: window system |reference(blit bstj). ! 177: Using the 5620 as the real-time controller, we are able ! 178: to place a synthesizer under the control of a host computer ! 179: (in this case, a ! 180: .SM VAX ! 181: 750), without a large amount of systems programming. ! 182: .PP ! 183: The ! 184: .SM MIDI ! 185: device driver (\c ! 186: .I midiblt ) ! 187: is down-loaded into a ! 188: .I mux ! 189: window, where it takes over interrupt handling on the parallel ! 190: .SM I/O ! 191: port. Three types of interrupts are handled: ! 192: .SM UART ! 193: transmitter and receiver ready, ! 194: and a \%5-msec clock. ! 195: .SM MIDI ! 196: data are organized into three queues. Incoming messages are time-stamped ! 197: and placed on the ! 198: .I receiver ! 199: queue, where they are available to other software. At clock interrupt, the ! 200: .I scheduler ! 201: queue is examined for messages with time less than or equal to the current ! 202: time; such messages are moved from the scheduler queue to the ! 203: .I transmitter ! 204: queue, from which they are sent to the hardware as quickly as possible. ! 205: .PP ! 206: There is a sharp division between routines that control the ! 207: .SM UART ! 208: directly ! 209: (and handle single characters) ! 210: and those that manipulate ! 211: .SM MIDI ! 212: messages. For example, the ! 213: .SM MIDI ! 214: transmitter is ! 215: a finite-state machine that understands things like elided status bytes; it ! 216: is called by a lower-level routine and produces the next byte to be sent by ! 217: the ! 218: .SM UART . ! 219: .PP ! 220: Routines that empty the receiver queue and fill the scheduler queue are ! 221: available from outside the driver. ! 222: .I Midiblt ! 223: places their entry points in a name table maintained by ! 224: .I mux ; ! 225: since there is no memory mapping in the 5620, they are immediately available ! 226: to other programs down-loaded in the terminal. ! 227: This code-sharing mechanism is used to implement a virtual-keyboard program, ! 228: .I jx7 , ! 229: which provides the functionality of a one-fingered mouse-pianist, with slide ! 230: controls for volume, vibrato, pitch bend, etc. ! 231: .NH ! 232: Communication with the host. ! 233: .PP ! 234: Host communication takes place via the ! 235: .I streams ! 236: mechanism |reference(latest streams). Briefly, each ! 237: window is associated with a host control stream managed by ! 238: .I mux ; ! 239: when a program down-loaded in the terminal does a read or write, ! 240: .I mux ! 241: performs the appropriate operation on the stream associated with the window. ! 242: The program at the other end of the stream can be the shell ! 243: (this is how multiple virtual terminals are implemented), or a special-purpose ! 244: program (as in the case of a text editor). ! 245: .I Midiblt ! 246: falls into the second category. It mounts ! 247: its stream under the name \&\c ! 248: .CW .MIDI ! 249: in the ! 250: user's home directory, in effect ! 251: creating a character-special file for the ! 252: .SM MIDI ! 253: driver. The terminal side of ! 254: .I midiblt ! 255: reads characters as they appear from the host, assembles them into ! 256: .SM MIDI ! 257: messages, and places the messages on the scheduler queue. (The host side of ! 258: .I midiblt ! 259: does not look at this data; error correction and flow control are performed by ! 260: .I mux ). ! 261: .PP ! 262: We have followed a time-honored ! 263: .UX ! 264: tradition by formatting the ! 265: .SM MIDI ! 266: file in ! 267: .SM ASCII . ! 268: Such a file consists of a series of lines, one per ! 269: .SM MIDI ! 270: message. ! 271: Each line is a sequence of blank-separated decimal numbers, viz.: event time ! 272: (msec), status byte, and data. (Backslash-newline can be used ! 273: to break long system-exclusive messages.) ! 274: In this form, the entire panoply of ! 275: .UX ! 276: text-processing tools can be ! 277: brought to bear in rough-and-ready fashion. ! 278: .PP ! 279: On the other hand, this format is not well ! 280: suited for direct transmission to the 5620, since bandwidth is at a premium. ! 281: The program ! 282: .I midi , ! 283: which compresses the data, is used; it replaces (and has similar semantics to) ! 284: .CW "cat >.MIDI" . ! 285: In addition, ! 286: .I midi ! 287: attaches itself to the ! 288: .I "process group" ! 289: associated with the \&\c ! 290: .CW .MIDI ! 291: stream. This allows the ! 292: .I midiblt ! 293: window to send signals to the ! 294: .I midi ! 295: process so that, e.g., if the user does a reset from the ! 296: .I midiblt ! 297: menu, there is proper coordination between the host and the driver. ! 298: .PP ! 299: The ! 300: .CW "midi | midiblt" ! 301: pattern is repeated in the programs ! 302: .I score ! 303: and ! 304: .I scoreblt , ! 305: which produce a pitch vs. time graph. ! 306: .I Scoreblt ! 307: manages a terminal window and draws the display. Through the code-sharing ! 308: mechanism mentioned earlier, it has access to ! 309: .I thinkblt ! 310: which drives a HP ThinkJet dot-matrix printer. ! 311: .NH ! 312: Synthesizer control. ! 313: .PP ! 314: The system described was first used to run a Yamaha ! 315: .SM DX7 ! 316: and, later, a ! 317: .SM TX816 . ! 318: Both produce sound via ! 319: .SM FM ! 320: synthesis |reference(chowning), a voice being described ! 321: by around 100 (digital) parameters, all settable by system-exclusive ! 322: .SM MIDI ! 323: messages. The ! 324: .SM DX7 ! 325: has internal memory for 32 pre-loaded voices that can be ! 326: selected by number (via the ! 327: .SM MIDI ! 328: .I "program change" ! 329: message). It is a keyboard instrument with additional controls for ! 330: editing voice parameters (the current voice, whether internal or downloaded, ! 331: is always copied into an editing buffer). ! 332: Although the ! 333: .SM DX7 ! 334: is limited to playing in one voice at a time, ! 335: up to 16 simultaneous notes can be produced. The ! 336: .SM TX816 ! 337: is a rack-mounted ! 338: unit consisting of eight ! 339: .SM TF1 "'s." ! 340: Each ! 341: .SM TF1 ! 342: is essentially a ! 343: .SM DX7 ! 344: without keyboard, and ! 345: can be set to a different ! 346: .SM MIDI ! 347: channel, so that ! 348: orchestral and multi-track effects are possible. ! 349: .PP ! 350: A number of C programs are used for synthesizer configuration. ! 351: .I Txchan ! 352: assigns channel numbers to the ! 353: .SM TF1 "'s." ! 354: .I Mecho ! 355: is used to send ``constant'' data (such as to select an internal voice, or ! 356: alter single parameters of a voice). ! 357: .I Dxvoice ! 358: downloads voice data from a library file on the host. Figure 1 is an example ! 359: of a shell script that sets up the ! 360: .SM TX816 ! 361: with five instruments, one of which ! 362: (the harpsichord) uses two ! 363: .SM TF1 "'s" ! 364: in parallel. The violin voice needs to be ! 365: downloaded; the rest are already stored in the proper ! 366: .SM TF1 "'s" ! 367: (they came from the factory this way). ! 368: Percent signs delimit comments to ! 369: .I mecho . ! 370: .1C ! 371: .KF top ! 372: .P1 ! 373: txchan 1 1 3 4 5 6 7 8 ! 374: mecho init \e ! 375: prog -c1 28 % harpsichord chan 1 % \e ! 376: -c4 3 % reeds chan 4 % \e ! 377: -c6 24 % flute chan 6 % \e ! 378: -c8 3 % bass pipes chan 8 % \e ! 379: dx -c8 p144 24 % move up an octave % \e ! 380: parm -c4 p4 127 % foot control reeds % \e ! 381: -c6 p2 127 % breath control flute % \e ! 382: -c8 p4 127 % foot control pipes % ! 383: dxvoice -c3 -v2 tx816.8 # solo violin chan 3 ! 384: .P2 ! 385: .FG 1 "Setting up the TX816" ! 386: .KE ! 387: .2C ! 388: .PP ! 389: This scheme is complete in that it allows access to any ! 390: .SM MIDI ! 391: or ! 392: .SM DX ! 393: parameter, ! 394: but it is not altogether satisfactory. The user must know, for example, that ! 395: voice 3 is ``reeds'' in the fourth unit, but ``bass pipes'' in the eighth. ! 396: Simply assigning names to numbered parameters does not reduce the complexity, ! 397: however. Ideally, one would like an interactive ``orchestration editor'' ! 398: supported by a large database. ! 399: .1C ! 400: .KF bottom ! 401: .P1 ! 402: main() ! 403: { ! 404: int c, x, y, z, magic = 13, size = 64; ! 405: int mask = 2*size-1; ! 406: for (c = z = 0; c <= mask; c++, z += magic) ! 407: for (newfile(), y=0; y < size; y++) ! 408: if ((x = (y ^ z) & mask) < size) ! 409: play((x+y), (x-y)); ! 410: } ! 411: .P2 ! 412: .FG 2 "Munching squares" ! 413: .KE ! 414: .2C ! 415: .NH ! 416: Musical examples in C and the shell. ! 417: .PP ! 418: The first piece to be played on our system was written by Cynthia P. Killian ! 419: using a combination of C and the Bourne shell. The composer took ! 420: raw material produced by the ``munching squares'' algorithm and manipulated ! 421: it by splicing and dubbing techniques. Figure 2 shows the heart of the algorithm. ! 422: .I X ! 423: and ! 424: .I y ! 425: trace out short diagonal segments, which are rotated by 45 degrees and passed to ! 426: .I play . ! 427: The resulting vertical coordinate is mapped onto a tone row, and the length of ! 428: the segment (as determined by a sequence of points at the same height) determines ! 429: the length of time the tone is held. ! 430: .I Newfile ! 431: breaks the output into separate small files (\c ! 432: .CW smunch.\fI?? ) ! 433: for convenience. The ! 434: latter are then processed by shell scripts like that shown in Figure 3. ! 435: The unfamiliar commands in the script are either shell scripts or trivial C ! 436: programs. ! 437: .I Sed ! 438: and ! 439: .I "sort\ \-n" ! 440: form the basis for cutting and pasting. Operators that do a lot of arithmetic ! 441: (e.g, ! 442: .I retro , ! 443: which time-reverses its input) are written in C. The shell syntax for ! 444: operator precedence and the semantics of the ! 445: .UX ! 446: filter are extremely ! 447: well matched to this application. ! 448: .1C ! 449: .KF ! 450: .P1 ! 451: (divider <smunch.06 3 4; divider <smunch.07 1 2 ! 452: divider <smunch.07 1 2 | ! 453: invert 0 1120 0 | retro) | ttrans 0 280 >tmp$$ ! 454: (cat tmp$$ ! 455: divider <smunch.08 7 8) | ttrans `endtime <tmp$$` -1260 ! 456: rm tmp$$ ! 457: .P2 ! 458: .FG 3 "Shell script for processing munching squares" ! 459: .KE ! 460: .KF bottom ! 461: .P1 ! 462: /* Inventio 4 (BWV 775) */ ! 463: 8 = 120 /* tempo: 120 eighth notes/min */ ! 464: 3 / 8 /* time signature */ ! 465: { ! 466: treble : 1 < 6(16) | 6(16) > ! 467: | F60: d3 e f g a b@ | c# b@ a g f e | ! 468: bass : 1 < 4. | 4. > ! 469: | F60: r | r | ! 470: ! 471: treble < 3(8) | 3(8) | 6(16) > ! 472: | f a d4 | g3 c4# e | d e f g a b@ | ! 473: bass < 6(16) | 6(16) | 3(8) > ! 474: | d2 e f g a b@ | c# b@ a g f e | f a d3 | ! 475: \&... ! 476: } ! 477: .P2 ! 478: .FG 4 "The beginning of a Bach invention" ! 479: .KE ! 480: .2C ! 481: .NH ! 482: The M language. ! 483: .PP ! 484: The need for a closer tie with standard musical notation led to the development ! 485: of the M language. We will try to give a feel for the language with a short ! 486: example shown in Figure 4. ! 487: .PP ! 488: An M file consists of ``front matter'' followed by text enclosed ! 489: in matching curly braces. The comment convention is similar to C's, ! 490: except that comments nest. ! 491: The music is divided into ! 492: .I lines ! 493: formed by a ! 494: .I "voice name" , ! 495: an optional colon and ! 496: .SM MIDI ! 497: channel number, a ! 498: .I "rhythm list" , ! 499: and corresponding ! 500: .I "note list" . ! 501: Voice names are arbitrary alphanumeric strings. ! 502: Within rhythm and note lists, ! 503: measures are delimited by barlines. ! 504: A rhythm list consists of ! 505: .I "time values" ! 506: (\c ! 507: .CW \%1 "\&\ =\ whole note," ! 508: .CW \%2 "\&\ =\ half note," ! 509: .CW \%4 "\&\ =\ quarter note," ! 510: .CW \%4. "\&\ =\ dotted quarter note," ! 511: etc.) and ! 512: .I rests, ! 513: possibly with repeated groups (\c ! 514: .CW "3(16 16r)" ! 515: means ! 516: .\".fp 9 MU Musicpi ! 517: .\"\f(MU\N'140'\fP\ \f(MU\N'24'\fP ! 518: .\"\f(MU\N'140'\fP\ \f(MU\N'24'\fP ! 519: .\"\f(MU\N'140'\fP\ \f(MU\N'24'\fP). ! 520: .fp 8 MU Sonata ! 521: \f(MU\N'120'\fP\ \f(MU\N'197'\fP ! 522: \f(MU\N'120'\fP\ \f(MU\N'197'\fP ! 523: \f(MU\N'120'\fP\ \f(MU\N'197'\fP ). ! 524: A note list consists of ! 525: .I notes , ! 526: .I rests , ! 527: and ! 528: .I modifiers . ! 529: Notes specify ! 530: .I "pitch class" ! 531: and, optionally, ! 532: .I "octave number" . ! 533: Pitch class is indicated in standard letter notation, with accidentals ! 534: .CW # , ! 535: .CW @ ! 536: (flat), and ! 537: .CW = ! 538: (natural). The octave is given by a number appearing ! 539: after the letter name, e.g., ! 540: .CW c3 ! 541: is middle c, and ! 542: .CW c3# , ! 543: .CW c#3 ! 544: are a half-tone above. ! 545: The octave number changes between b and c, so a half-tone below middle c is ! 546: .CW b2 . ! 547: A missing octave number defaults to that of the previous note in the same voice. ! 548: Rests have time value only and are indicated by ! 549: .CW r . ! 550: .PP ! 551: Modifiers are used to set ``environmental'' parameters; in the example, ! 552: the modifier ! 553: .CW F60 ! 554: specifies a ``key force'' (volume) of 60 units. ! 555: .PP ! 556: Not shown in the example are notations for ties and more complicated rhythms. ! 557: Ties map particularly easily onto ! 558: .SM MIDI ! 559: events. Observe that a note usually ! 560: generates two ! 561: .SM MIDI ! 562: events, note-on and note-off. A note with a tie going out ! 563: (e.g., ! 564: .CW \%c- ")," ! 565: simply loses its note-off event, and one with a tie coming in ! 566: (e.g., ! 567: .CW _c ")" ! 568: loses its note-on event. ! 569: Time values may contain multiplicative expressions, e.g., ! 570: .CW 3(3*4) ! 571: is a triplet to a quarter note. ! 572: Finally, values that are inconvenient to ! 573: generate otherwise can be specified with numerator and denominator, e.g., ! 574: .CW 9/17 . ! 575: .PP ! 576: Also not shown in the example are possibilities for grouping in the ! 577: note list. Parentheses group a ! 578: .I sequence , ! 579: and square brackets group a ! 580: .I chord . ! 581: E.g., all of the notes in ! 582: .CW "[c\ e\ g]" ! 583: are sounded simultaneously, and ! 584: groupings may be nested, so that ! 585: .CW "[(c\ d\ c) (e\ g\ e) (g\ b\ g)]" ! 586: has three ! 587: sequences running in parallel to make a \%I-V-I progression. ! 588: .PP ! 589: To facilitate machine generation of M code, an alternative rhythm ! 590: notation is allowed: the rhythm list can be omitted, and time values prefixed ! 591: to the corresponding notes. This is readable to humans, but less convenient ! 592: for keyboard input (see below). ! 593: .PP ! 594: M is based on ! 595: .I lex ! 596: and ! 597: .I yacc , ! 598: and its continuing development depends heavily on them. ! 599: M is intended to be used by musicians who are not computer scientists, and ! 600: who can't always pass on the merits of a feature without testing it. Hence ! 601: the ability to experiment is most important. ! 602: .I Lex ! 603: in particular has been justly criticized on performance grounds, given that ! 604: lexical analyzers are fairly easy to write by hand. But this is the case ! 605: only for a static language, and M is still changing rapidly, particularly in ! 606: the area of dynamics control. ! 607: .PP ! 608: M produces a ! 609: .SM MIDI ! 610: file on its standard output, so that ! 611: .CW "m bach\ |\ midi" ! 612: is an example of a common invocation. ! 613: It also has options to restrict the output ! 614: to certain voices or a range of measures, as a debugging aid. ! 615: .NH ! 616: The M keyboard interface. ! 617: .PP ! 618: It is possible to generate the notes of an M program by playing at the ! 619: .SM DX7 ! 620: keyboard. First the note-on and note-off events are collected by ! 621: .I midiblt ! 622: and written into a file on the host. This file is then converted into a ! 623: list of note names by ! 624: .I unmidi . ! 625: We have used the conventions that the ! 626: .SM DX7 "'s" ! 627: .SM YES ! 628: button generates a newline, and the ! 629: .SM NO ! 630: button produces the comment ! 631: .CW "/*?*/" . ! 632: This gives the user some control over the format and the ability to flag errors. ! 633: We also use the ``portamento'' pedal to generate a barline. ! 634: .I Unmidi ! 635: has an ``output key'' option to direct it towards desired enharmonic spellings. ! 636: The output from ! 637: .I unmidi ! 638: is fed to an ! 639: .I awk ! 640: script which adds voice names. Now only the rhythm ! 641: is missing. Since it is separate from the notes, it ! 642: can be typed in quickly by making a second pass over the score. ! 643: .NH ! 644: Conclusions. ! 645: .PP ! 646: Twelve-tone pieces such as the one in section 5, and serialized pieces ! 647: particularly, would be easier and faster to develop with the aid of specialized ! 648: tools. These could range anywhere from a library of C routines to a ! 649: complete language implementation. We expect to begin on a modest scale ! 650: with the former and enlist the efforts of interested composers. ! 651: .PP ! 652: It is clear that we have barely scratched the surface of ! 653: an immensely challenging class ! 654: of problems. The ! 655: .UX ! 656: system, originally crafted as a home for programmers, ! 657: has proven remarkably robust, flexible, and downright hospitable as a base ! 658: for a very different application. ! 659: .NH ! 660: References. ! 661: .LP ! 662: |reference_placement
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.