The Audio Device On The Amiga ----------------------------- May, 1986, somebody asks: "Why couldn't they have made audio easier to use?" Is it really so difficult, I ask myself. After all, it seems pretty thoroughly documented. But then again, why are so few people using audio so far if it is supposed to be so good on the Amiga. But I'm really busy trying to document other things so I leave audio to the experts. As help is requested, it appears that the designer of the audio device is advising people in person. I guess that whoever needs help is getting directly to him. Fast forward to November, 1986. I am not at Amiga any more. I'm consulting for a couple of other places and doing the Programmers' Guide To The Amiga in whatever spare time I have left. Comes the time to do an audio chapter. Hmmm, the ROM Kernel stuff describes a lot of features, but demonstrates only a few of them. And it goes directly to the hardware. I don't want to do that! My editor recommends another Amiga book that seems to have an "inside look" at the audio. So I got it and, oh no, it goes directly to the hardware too! What I want to do is queue up several sounds, and have the the audio system play them sequentially while my task goes on to do something else. Going directly to the hardware meant the audio device will neither count cycles for me nor queue sounds for automatic play. Sound queueing could be done using the audio device command called CMD_WRITE, but at first I found no examples that used the CMD_WRITE command. Later, after several false starts (and after completing the audio chapter and most of the rest of the book) I finally discovered an example program on Usenet (reposted from BIX) that used CMD_WRITE. Some pieces of that program, along with what I developed subsequent to the audio chapter, appear here. I simply wanted my main program to look like this: main() { /* ... (program-stuff) ... */ InitAudio(); /* initialize everything */ channel = GetChannel(-1); if(channel != -1) { PlayNote(channel, waveform, note_no, volume, duration); /* ... (more PlayNotes) ... */ } /* do non-audio things here */ FreeChannel(channel); FinishAudio(); /* close everything down */ } THAT seemed to make it simple, and it seemed to be what I wanted (and what others had been asking for). The details of device access and message passing are buried in a subroutine somewhere, where a person need not deal with it. After I created some of these routines I tested them on a few cases and they seem to be what people wanted, so here they are. The functions you see above are provided in this article, with other support functions for the audio device. By examining the source code I've provided, you will see just how to communicate with the audio device and you may be able to add your own enhancements to these techniques. At the end of the article, you'll find a list of some of the enhancements I expect to install by the time this article actually appears in print, as well as other features that people have requested. Note: If not otherwise stated, all parameters passed to the routines and passed back as return values are LONG integers (32-bits). Sometimes a pointer (also 32-bits) is used, and is shown as such. The Functions, Explained ------------------------ 1. gotchannel = GetChannel(channel); channel - any number from 0 to 3, corresponding to a specific hardware channel on the Amiga. If you ask for channel number -1, it means get ANY channel that is available. The function GetChannel returns the channel number, or returns -1 if none are available to you. 2. PlayNote(channel,waveform,note_no,volume,duration,priority,message); channel - a channel that you already own. If you don't own it, the note simply will not play. waveform - a pointer to the start of a waveform table that contains 256 samples of a single wave of your sound. Sample values range from -128 to +127. The waveform table also includes copies of the same waveform, each having fewer and fewer samples in the table (128 samples, 64, 32 and so on). This waveform table lets us stay within the allowable limits of the Amiga audio hardware. In particular, period values of 127 through 500 are the values that lets the Amiga output the best quality audio. To get an output that is of a high frequency, since the period values are limited, each wave of the waveform must be output more quickly. Thus the table with several copies of the waveform, each having different numbers of samples. See the source code for MakeWaves to see how the tables are built. Note Number - Notes are numbered from 0 to 95, structured as 8 octaves of 12 notes each. Each octave has its own waveform table entry having a length appropriate to that octave. Volume - Takes a value from 0 to 64 where 0 is minimum. Duration - specified in 1000ths of a second. Five Hundred thousandths of a second, for example, is one-half of a second. The audio device accepts a command to output a specific number of cycles of a waveform. I calculate the frequency (in cycles per second) from the note number, then multiply by Duration and divide by 1000, yielding the correct number of cycles for that frequency. Thus all notes play for the correct time. Priority - [NOT IMPLEMENTED YET]. If priority is 0, just queue the note. If less than 0, flush all current requests for this channel and start this note only. If greater than 0, do not flush... the priority value is only going to be used to identify the note number to you when the note begins to play. Message - [NOT IMPLEMENTED YET]. Audiotools can send you a message that contains an identifier of your choice (the priority value) to let your task know that this note has just begun to play. On receiving the message, your task must reply to it so that the audiotools can reuse or deallocate the message memory. 3. FreeChannel(channel) - frees a channel that you own to let another task (or your own task, later) use the channel. 4. InitAudio() 5. FinishAudio() - These functions take care of the background work, such as opening and closing the audio device. By using these routines, you do not have to deal with the audio device at all. You need not allocate and initialize message blocks and so on. All of this is built into the support routines and associated global variables. By using the routines, though, you add some additional overhead to accessing the audio device. If you are designing a high performance audio routine, you just may have to lock the channels and go directly to the hardware. For that kind of thing, you have the ROM Kernel examples to guide you. But for the rest of us, who just need to BEEP at somebody, these routines make the access just a bit simpler (and provide a functional jumping off point for further audio development). You could also get rid of the subroutine call overhead by copying appropriate portions of code directly into your main program. Note: PlayNote is asynchronous. This means that it queues up a note to be played by the audio device and then returns to the calling program immediately. (It does not wait for the note to be finished before it returns to the caller). ALL other functions in this article are synchronous. That is, the function is performed entirely before your program goes on to do something else. What The Sample (main) Program Does ----------------------------------- Using the above functions, main simply plays a few notes through each channel, in each of the waveforms: sawtooth, triangle and square waves. All 4 channels are active at the same time. When all notes have completed, the program exits. Support Functions ----------------- This audio library has the following support functions. 6. error = StopChannel(channel) 7. error = StartChannel(channel) - stop or start a specific channel. If a CMD_WRITE arrives at a stopped channel, it queues and waits for the channel to be started. A return value of 0 means no error. A return of -1 indicates low memory. Any other value is a direct return from io_Error. See devices/audio.h for meanings of other return values. StopChannel terminates any CMD_WRITE currently in progress. 8. error = FlushChannel(channel) - If there are CMD_WRITE's lined up to be played, return them all to the caller (flush input). 9. error = ResetChannel(channel) - reset it to its default values. Also means flush a channel's input queue. 10. stereopair = GetStereoPair(pair) - In the Amiga hardware, audio channels 0 and 3 are connected to the Left audio output; audio channels 1 and 2 are connected to the Right audio output. Thus to get a stereo pair, you need left-channel and one right-channel. Specify pair as -1 for ANY stereo pair, or specify 0 (for audio channels 0 and 1), 1 (for 0 and 2), 2 (for 1 and 3) or 3 (for 2 and 3). Return value is the pair that was available, or -1 if no stereo pair is available. FreeChannel(stereopair) works just as FreeChannel(channel). 11. error = SetPV(channel, period, volume) - set the period and volume of a note that is playing currently. Note that there is only a limited range available for the period (roughly 127 to 500) so is it more likely that you would use PlayNote instead since PlayNote can modify the waveform pointer as well as the other parameters. Internal Functions ------------------ The following internal functions are used by the library functions shown earlier. Your programs may, at times, need these functions as well. These functions create, initialize and free audio device message blocks. (I call them IOBs, for I/O Blocks). 12. iob = GetIOB() - allocate or assign an IOAudio structure for use. Returns a value of 0 if system is too low on memory. If no IOB is available from a specified pool of IOB's, then dynamically allocate an IOB and pass back its address. Note: for more advanced system functions suggested by the users group members (shown later in this article), this structure may need to be extended to hold additional parameters. For now, though, the ExtIOB structure is identical to the normal IOAudio structure (created for now by a define statement). This allows us to define an extended version of the structure later, with little if any change to existing functions. 13. ReEmployIOB() - look at audio channel reply ports and see if any IOB's have returned (are now unemployed) and can therefore be reassigned or deallocated. 14. FreeIOB() - return a finished IOB to the free IOB pool or deallocate a dynamically allocated one. 15. InitBlock(iob,channel) - Initialize an IOB for communication with a specific channel, default command is CMD_WRITE. iob is a pointer to an IOAudio structure. Channel is the specific channel for which this block is to be initialized (allocation key is the critical item). 16. ExpandWave(waveform_pointer) - Takes a pointer to a waveform buffer that contains one cycle of a waveform, in 256 consecutive bytes, and expands the table to add the same waveform sampled 128 times at twice the sampling interval, 64 times at 4 times the sampling interval, 32 times/16 times/8 times and so on. NOTE: the wave tables MUST be in CHIP memory otherwise the audio device will be unable to play the notes!!! ExpandWave is associated with MakeWaves() that creates three tables total, one containing a sawtooth wave, one a triangle wave, and the third contains a square wave. ExpandWave completes the table entries for each waveform. All of the waveforms are left in contiguous memory after the first wave, in order of decending sizes (256, 128, 64, ...) MakeWaves, ExpandWave, SetPV and PlayNote are paraphrased versions of similar routines found in a posting to BIX by Steven A. Bennett. Thanks, Steven, for the inspiration on this project. Steven's posted article also provided the waveform and period tables I've used, as well as the excellent explanation of the period value calculation that I've quoted (slightly modified) below. As you examine the source code provided, you'll see that the audio device requires a period value rather than a frequency value. The period table contains the period value corresponding to the frequencies of the normal scale (12 notes per octave... see ABasiC manual, page 138). You could calculate period yourself from the formula: period = Clock / ( samples-per-wave * frequency) Clock rate is 3,579,545 cycles per second So if you are playing a wave table that contains 32 samples, and your selected output frequency is to be middle-A (440 hz), of the piano the period value must be 3579545 / (32 * 440) = 254.229. : "But the audio device only accepts a whole number, so your result has to be rounded down to the nearest integer which can result in a maximum frequency error of about .25%, assuming one uses the octave for frequency between period 226 and period 428. (This comes out to be less than a twentieth step at the shortest period)." : "Period values of less than 127 are illegal, as there aren't enough cycles set aside for audio DMA for anything less. period values of greater than 500 or so aren't recommended as the anti-aliasing filter isn't of much use then, and actually could cause a possible high pitched overtone, which I'm sure nobody wants. Thus I am only going to use SetPV to handle a single octave's range." : "Changes of octave are accomplished by doubling or halving the number of samples in one cycle of the waveform, so one must, therefore, call PlayNote() instead of SetPV()." Additional Information About Internal Functions ----------------------------------------------- For GetIOB, you can control how many structures are allocated for IOAudio use. How many audio ioblocks should the system have available for queueing up notes? If you want to queue up a whole song by using a whole bunch of PlayNote commands and go away to do something else, it could take a lot of memory! Once the system runs out of these preallocated structures, it must dynamically allocate and free memory... this can cause fragmentation of memory space. You might want to send parts of the song at a time instead of the whole song. Depending on the variable AUDBUFFERS, defined when the program is compiled, GetIOB either returns the address of a buffer in global memory space, named "global" (in the name field of the I/O message, node area) or named "dynamic" if GetIOB runs out of AUDBUFFERS global blocks to use. The number of dynamic blocks is limited only by the available system memory (FAST memory, that is non-CHIP memory is used for the I/O blocks). NOTE: To be able to use only a standard sized IOAudio structure for the message passing, I assigned the message mn_Length field to identify the global blocks. (As of 1.1 and 1.2, the mn_Length field is still available for anybody to decide what meaning it has). To be perfectly safe, as well as to handle the advanced functions that people have requested, an extended audio block should probably be used... with a LONG quantity appended to it as the identifier in place of using mn_Length, as well as a few other fields. This change is very likely to be made for the disk version of the tools. (The structure ExtIOB will be used as an extended version of IOAudio). The Audio Tools And A Test Program ---------------------------------- Here are the listings that implement the tools described above. Following the listings are some improvements that have been suggested. Whatever I've been able to implement of those improvements, as well as this code, appears on the disk mentioned at the end of the article. I hope these help you to better understand the audio system of the Amiga. ======================================== ======================================== What People Have Suggested -------------------------- I did a chalk-talk at a developers' group meeting, and showed them what I was working on for this article. They suggested the following additions to what you see described above. 0. Add examples that use the stereo pair; check that all error conditions are properly reported ("bullet-proofing"). (This is numbered "0" because it just has-to-be-done). 1. Implement the Priority and Message fields of PlayNote, just as described. 2. Add a PlaySong function that can take a pointer to a data structure that describes a song, with some of the parameters that PlayNote takes, and just play the song automatically. 3. Add a PlayWave function to handle sampled sounds, such as: PlayWave(channel,sample_addr,copy,period,repeats,priority,message) Global variables would be expanded to include a separate ReplyPort for the sampled sounds. A copy (TRUE/FALSE) parameter would specify (if TRUE) that the sampled_wave should be copied (into chip memory) before queueing it to be played. (Only CHIP memory waveforms can be played anyway). If FALSE, it would assume that sample_addr is in chip memory and that you will not change the contents of memory before the note has completed playing. 4. Add a PlayFreq function that takes a frequency value instead of a note number so that oriental music, for example, not based on the same scale we use for a piano, could be played. PlayFreq would take exactly the same parameters as PlayNote but substitute "frequency" for "note_no". It would calculate which is the longest waveform that can be used for the selected frequency and still leave the period value within the appropriate range of 127 to 500. 5. Add an implied-rest between notes so that it would sound more natural and avoid having to explicitly encode such rests into a song structure. 6. Add the ability to specify a slew rate for either volume or frequency or both so that notes instead of going directly from one setting to another can slide to the new setting at a specified rate. Or perhaps better still, add full ADSR capabilities. This last one is a little tricky. It could require software interrupts or perhaps even breaking into the audio interrupt vector itself. It will also require a data structure larger than the basic IOAudio structure to hold these new variables. This article is basically a report on current progress on a continuing project whose goal is to develop a freely distributable set of license-free routines that make it easier to use Amiga audio. I welcome suggestions as to additional enhancements that might be useful, or code samples that implement such enhancements.