Interface Manual: Software Requirements
Levels of Complexity
The complexity of the software needed to communicate with the Chroma is dependent upon the kind of communication desired. The most important factor is whether or not the software can wait for input from the Chroma. A simple system can be designed in which all communication is essentially half-duplex, meaning that when the computer is expecting information from the Chroma it is doing nothing else. This precludes recording and playing concurrently. In fact, it precludes doing much more with information arriving from the Chroma than storing it for later processing.
In order to allow more processing to occur in response to information arriving from the Chroma (as opposed to processing that is totally independent of what the Chroma might be sending), it is advisable to make the input system interrupt driven. This would allow information to be taken into the computer as soon as the Chroma sends it, where it would be queued until it could be processed.
If it is desired to record and process information from the Chroma while doing a large amount of unrelated processing (such as communicating with other instruments or a display terminal), some form of multi-tasking is necessary. A generalized multi-tasking operating system would be nice, but hardly necessary. The Chroma firmware is itself structured as two concurrent tasks, as the Chroma has plenty of stuff to do besides wait around for commands to arrive on the interface.
If it is important that outputting information to the Chroma be fast, a queue can be provided for outgoing information. and an interrupt can be used to move bytes from the queue onto the port. This is also done in the Chroma.
A Simple System
The simplest form of communication doesn't require any fancy software support. It is only necessary to wait for the output port to be empty before outputting each byte, and wait for the input port to be full before inputting each byte. BASIC peeks and pokes are sufficient for handling programming information, although most interpreted versions of BASIC aren't really fast enough to implement a decent sequencer. Note also that it would be advisable to include some method of getting out of the loop that waits for input from the Chroma (such as pressing a key on the computer terminal) to prevent communications problems from hanging the computer. A loop with a timeout might be appropriate when the computer requests specific information from the Chroma. Unless the Chroma is doing an autotune or cassette operation, it should respond to any command within a couple milliseconds.
A System With Interrupt Driven Input
This kind of system is what most people will probably be interested in playing with. The Hardware Requirements section of this manual shows an interface circuit that includes provisions for interrupting the processor when either the input port is full or the output port is empty. The output interrupt is less important, so the gates needed for this can be left out if not desired. The purpose behind making the input interrupt driven is that it keeps the real-time constraints of the Chroma from extending into the bulk of the computer software. This is because it allows rapid bursts of information from the Chroma to be handled as long as the computer can keep up with the average rate of information flow. The software necessary to do interrupt driven input consists or three procedures. The initialization procedure sets up the queue pointers and enables the interrupt. The interrupt handler pulls bytes off the port and stuffs them in the queue. The input procedure pulls bytes out of the queue for processing. These algorithms are presented below. Note that the interrupt handler must be written so that it returns with the interrupt masked in the event that the queue is full, and the input routine must, upon removing a byte from the queue, reenable the interrupt.
Procedure to Initialize Interface — called upon start-up
- set head and tail queue pointers to zero
- enable input interrupt
- input byte into tail of queue
- advance queue tail pointer
- if input queue full, disable interrupt
- wait for queue to be not empty
- remove byte from head of queue
- disable interrupt
- advance queue head pointer
- enable interrupt
- return byte
A fourth procedure might be provided to check to see if anything is in the queue without actually waiting in a loop.
A Fully Interrupt Driven Dual Task System
This is really a description or the way the Chroma handles its end of the interface. The algorithms described below show how the interface software might be written to allow inputting information to be handled as a separate, parallel task, without the use of a multi-tasking operating system. The purpose of multi-tasking is to allow a computer to take turns doing more than one thing, giving the appearance that it is doing them simultaneously.
When a computer has more than one task to perform, some mechanism must be provided for deciding which task should be handled at any given instant. In the Chroma, there are two tasks, one controlling the synthesizer and one responding to commands from the interface. Deciding which task should be performed is simple. If a byte is available from the interface, it is processed. If no byte is available, one complete cycle of the synthesizer firmware is performed (lasting about 1.25msec).
In order to implement two parallel tasks, it is necessary to save all the information representing the state of one task while running the other task. If a task is suspendable at only one point, and under the same conditions every time, no state information is required; the task is nothing more than a procedure that is called whenever there is work to do. If the task is to be suspendable in more than one place under different conditions, this information must be saved, which usually means using separate stacks.
The algorithms presented below treat the synthesizer task as the "background" task and the external input task as a "priority" task. The synthesizer task checks the input queue every now and then and, if a byte is found, causes the external input task to be resumed. The external input task is initialized so that the arrival of the first byte causes the task to "return" to the beginning of the command interpreter, which interprets the byte as a command code.
Procedure to Initialize Interface
— called upon start-up
- set input and output head and tail pointers to zero
- set non-responding flag — this gets reset when first byte arrives
- create external input process state image
- return address must point into command interpreter, as if command interpreter had called input procedure for an op-code
- unmask input interrupt — output interrupts remain masked
Input and Output Interrupt Handler
— handles both interrupts arriving on one line, with a round-robin
- repeat forever
- if input interrupt pending
- clear non-responding flag
- input byte and put into tail of input queue
- advance input queue tail pointer
- if input queue full
- mask input interrupt
- else if output interrupt pending
- output byte from head of output queue
- advance output queue head pointer
- if output mask empty
- mask output interrupt
- else return from interrupt
- if input interrupt pending
Procedure to Dispatch External Input Process
— called every 1msec or so by the main process
- if input queue not empty
- remove byte from head of input queue
- advance input queue head pointer
- unmask input interrupt — in case the queue was full
- save background task state
- restore external input process task state
- return byte to external input task
Procedure to Input One Byte
— called by external input process
- if input queue not empty within 100usec
- remove byte from head of input queue
- advanced input queue head pointer
- if input interrupt disabled — meaning queue was full
- unmask input interrupt
- return byte
- if input queue still empty after 100usec
- save external input task state
- restore background task state
- return to background task
Procedure to Output One Byte
— called by either main or external input process
- if non-responding flag set
- discard byte
- if output queue empty and output port empty within 100usec
- output byte directly
- else if output queue has room in it
- put byte in tail of output queue
- advance output queue tail pointer
- unmask output interrupt — in case the queue was empty
- else if output queue full
- set non-responding flag
- mask output interrupts
- set input and output queue head and tail pointers to zero
- reinitialize external input task state
With two hardware-prioritized interrupt lines, the interrupt handler could be split in two. Note also that the 100usec waits may actually speed up data transfers by increasing the likelihood that a multi-byte data transfer can be handled without re-interrupting for each byte. The non-responding flag is a mechanism used in the Chroma to handle the case of a crashed computer at the other end of the interface. It is cleared by incoming bytes and set if the output doesn't respond within a reasonable time.
Computer software to aid in programming the Chroma does not require any timing circuitry. However, if you intend to record and play back music with the Chroma, time measurement becomes very important. The Chroma is pretty good about playing music that arrives over the interface without any noticeable time lag. However, if you intend to use an interrupt driven input system, you must make sure that the delay between a byte's acceptance by the interrupt handler and its ultimate processing doesn't cause timing errors in the music. The easiest way to assure this is to let the interrupt handler record the time each byte arrives. Each byte in the input queue will therefore be accompanied by time information.
The timing resolution can be fixed, although music processing is easier if all events are timestamped according to a metronome that counts in some subdivision of the beat such as 48 ticks per beat. The obvious way to do this is to use a variable rate hardware timer, but these suffer from the disadvantage that they cost money and have less resolution at high speeds than at low speeds.
The easier way to keep variable-rate time is to use a fixed rate interrupt (many computers already have one built in) running at something faster than the fastest metronome speed, and then use a phase-increment algorithm to determine the actual software metronome rate. This is done by using a 16-bit integer and a 16-bit fraction for the metronome, and using a 16-bit fractional increment to set the metronome time. On every timer interrupt, the increment fraction is added to the metronome fraction. Thus, the rate at which the integer counts is directly proportional to the fractional increment, not inversely proportional as is the case with programmable hardware dividers.
Once the information is pulled from the queue for processing, the time bytes associated with command operands can be eliminated, leaving only the time bytes associated with each command code byte. The absolute time measurements might also be converted to relative time between events, if that is more appropriate to the processing that is to be performed.
Utilizing The Command Language Of The Chroma
The structure of a music recording and playback system is further impacted by the fact that the communication in each direction upon the interface is independent, yet the information flowing in each direction is not. To clarify, consider the case of the Performance Switch Off command. This command is normally sent to the Chroma as a signal that you are finished recording and no more information is to be accepted. But it is entirely possible, given the amount of buffering that the information must suffer, that further performance information will be transmitted in the milliseconds after this command has been issued. Even though the Chroma does in fact stop transmitting when it sees the Performance Switch Off command, there is no guarantee as to how long this will take. The difficulty is handled by the fact that all such mode change commands are echoed by the Chroma. Thus, the stream of data coming back from the Chroma will include "flags" that frame the information so that the computer knows when the Chroma is done transmitting. The correct way to start and stop recording from the Chroma is to keep a status flag that is set by receipt of a Performance Switch On command and cleared by receipt of a Performance Switch Off command. When recording is to commence or terminate, the computer should send the appropriate command, but the state of the status flag, controlled by the echoed commands, should start and stop the actual recording process.
The Restore command is actually the command most likely to be used to terminate recording. This command is provided as a convenience, making it unnecessary to explicitly restore the instrument definitions that were in effect when the recording started. Note, though, that the first few Chromas built do not echo the Performance, Panel and Pressure Switch Off commands when the Restore command is received, but current Chromas do. Refer to Appendix A.
When recording, with the panel, performance and/or pressure switches on, the Chroma will transmit "commands" with instrument codes 0 and 1. In order to allow playing and recording at the same time, instruments that are used for playback should be assigned higher numbers. If the interface is sending commands to instruments 0 and 1, there is nothing to prevent the performance controls and panel controls to send commands to these instruments at the same time. This shouldn't cause a problem, but it won't sound very good either. Note that the commands that are sent by the Chroma during recording are all in exactly the form (except for instrument number) that they should be transmitted back to the Chroma during playback. No other information will be sent by the Chroma unless it is explicitly requested.