=== AFX Sound V. 0.91 ===
=== Library Documentation ===

Contents

1. General
  1.1. Legal Stuff & Contacting the Programmer
  1.2. What's new in V. 0.91
  1.3. How to get the AFX putting out sound
  1.4. Package contents & Using the Lib

2. Interfacing essential sound functions
  2.1. Basics
  2.2. Constants, data structures, routines
    2.2.1. Initialisation / Deinitialisation
    2.2.2. Playing frequencies
    2.2.3. Notes and octaves
    2.2.4. Channels
  2.3. What you have to watch out for in two channel mode

3. Interfacing MIDI functions
  3.1. Basics
  3.2. Constants, data structures, routines
    3.2.1. Channel tracks & MIDI tracks
    3.2.2. MIDI state
    3.2.3. MIDI routines

4. Special Thanks

1. General

1.1. Legal Stuff & Contacting the Programmer

AFX Sound is freeware, feel free to copy and distribute it like you want. It also comes with the source this time, you can read and study or modify it for your own purposes.

NOTE: This library comes with absolutely no warranty, use is on own risk!!! Bugs or problems are not known at the moment, but if you encounter any, I would be pleased if you tell me. Also suggestions are welcome every time. My Email address(es) is(are):

AFX Sound's homepage is:

See it also in the UCF File Sharing Area.


1.2. What's new in V 0.91

There are only minor changes facing V. 0.9:

1.3. How to get the AFX putting out sound

Just plug a headphone or a speaker into the calc's COM port. You need no extra controllers or other special hardware for it.

There are different ways you can do this: You could get a 3.5mm to 2.5mm stereo jack adapter for example and plug a line phone/speaker-->adapter-->port. You also could use your SB62 (you got it with the AFX) and connect it anyhow. Be creative :-) If you think it's not loud enough, use a smaller speaker (an earphone maybe) or an amplifier.

Have fun :-)


1.4. Package contents and using the LIB

AFXSound is available for C and Pascal. What the package contains is this document, the C/Pascal sources and a compiled Borland TP 6.0 Unit (afxsound.tpu).

Using AFXSound with C/C++ Using AFXSound with Digital Mars Using AFXSound with TP 6.0 Using with other Pascal versions The following chapters will describe now the AFXSound library's interface:


2. Interfacing essential sound functions

2.1. Basics

AFXSound uses the COM port at a speed of 14400 Bps and allows sounds up to (14400/8)/s = 1.8kHz thus. Doing so the lib is able to create every audiofrequency in that range in a continuous spectrum. It can play all twelve notes of the musical scale in seven octaves in a really good quality (note: tones are adjusted at a' = 440Hz like used in real by music, too). Furthermore, you have TWO channels available, so you can play two differend frequencies simultaneously (see chapter 2.3. What you have to watch out for in two channel mode though)

But there's also a revers of the medal: when playing sounds, it will take most of the CPU resources cause the lib has to write permanently to the port. In fact, AFXSound's soundplay() procedure runs as the main thread, breaked only by interrupts (usually keyboard and timer) for your program's other processes (they won't disturb sound output when they don't take a too long time however). So things you can use AFXSound for are:
Have a look therefor to the MIDI routines described in chapter 3, too.


2.2. Constants, data structures, routines

2.2.1. Initialisation / Deinitialisation

procedure soundopen;  //open sound device
void soundopen();

procedure soundplay;  //play sounds
void soundplay();

procedure soundclose; //close sound device
void soundclose();

const soundstop: boolean = false; //exit soundplay
byte soundstop = 0;
First of all, prepare the calc for sound output via soundopen, unprepare via soundclose. When prepared, you can call soundplay routine that will run until either soundstop is set to 1 or soundclose is called (When setting soundstop 1, soundplay returns, but the calc remains being prepared for sound output).

Functions affecting the sound output like noteon will have an effect only while soundplay is running! (thus you have to control this functions via interrupt).

Example:
...
soundopen;
oldtimer := intvects[$1C];
intvects[$1C] := @mycontroller;
soundplay;
intvects[$1C] := oldtimer;
...

2.2.2. Playing frequencies

function preparefreq(freq: word): word; //prepare frequency for freqon
word preparefreq(word freq);

procedure freqon(freq: word; ch: byte); //play frequency, for example freqon(preparefreq(440))
void freqon(word freq, byte ch);
You play a frequency on a channel by freqon function. It acts like a note on event, and the frequency will be played until another frequency is played on that channel, noteoff/note_quite occurs, or soundplay exits.

Freqon's parameter freq is NOT an absolute frequency, but a prepared one. Use preparefreq to determine it. For example, freqon(440,0) will not play a sound of 440Hz on channel 0, but freqon(preparefreq(440),0) will.


2.2.3. Notes and octaves

const note_quite = $FFFF; //NoteOff event
      note_c     = 2939;
      note_cis   = 2774;  //note: you get note_xxx of octave
      note_d     = 2618;  //      n simply by note_xxx shr n
      note_dis   = 2471;
      note_e     = 2333;  //      frequencies are adjusted
      note_f     = 2202;  //      correctly, a' is 440Hz
      note_fis   = 2078;
      note_g     = 1961;
      note_gis   = 1851;
      note_a     = 1747;
      note_ais   = 1649;
      note_h     = 1557;

#define note_quite 0xFFFF
#define note_c     2939
...

const octave: byte = 3;   //current octave; key octave is 3
byte octave = 3;

procedure noteon(freq: word; ch: byte); //NoteOn  event
void noteon(word freq, byte ch);

procedure noteoff(ch: byte);            //NoteOff event
void noteoff(byte ch);
Note_xxx constants are prepared frequencies for the twelve notes C,Cis,...,H of octave 0 + the quite note (NoteOff event). You get the according value for octave n simply by shifting note_xxx right by the amount of n (thus, note_c shr 3 is note C in octave 3 for example).

AFXSound provides 7 octaves (you may use more, but the maximum frequency the lib can represent is 1.8kHz no matter on how high you screw them up), where 0 is the lowest and 6 the highest. Octave number 3 is the key octave.

You have the noteon routine that is quite similar to freqon, but shifts right it's parameter regarding current octave automatically, so you needn't care for this. Noteoff is just an alias for freqon(note_quite).

...
noteon(note_cis); //plays Cis in octave 3 when octave not changed until here
octave := 4;
noteon(note_g); //plays G in octave 4

2.2.4. Channels

type pchannel = ^tchannel;   //current channel data:
     tchannel = record
                  note: word; //current playing sound
                  cnt : word; //reserved for internal use
                  pat : byte; //current sound pattern
                end;

typedef struct { word note; //current playing sound
                 word cnt;  //reserved for internal use
                 byte pat;  //current sound pattern
               } tchannel;

typedef tchannel *pchannel;

const channels: array[0..1] of tchannel = ((note: $FFFF; cnt: 1; pat: $F),
                                           (note: $FFFF; cnt: 1; pat: $F));

tchannel channels[2] = {{0xFFFF,1,0xF},  //channel defualt values:
                        {0xFFFF,1,0xF}}; //quite + sound pattern 0xF
You have two channels 0 and 1 in AFXSound, you may use both simultaneously. What a channel contains is: the prepared frequency currently playing (note), an internal var (cnt) and the pattern of the sound currently played (pat).

Maybe you also could call the pattern an "instrument", but this would be overdone, as it consists of only one byte. However, you can form the sound characteristics with it a bit.


2.3. What you have to watch out for in two channel mode

You can't play each frequency you want simultaneously to each other proper. For example, when trying to play the same frequency f on both channels simultaneously, the result is undetermined (the resulting frequency f' is in the range f..2f however).

The reason for this is quite clear: the COM port can put out simple square waves only with two amplitudes: 1 and 0, so it doesn't sum up overlapping waves. Depending on both waves phase displacement, the result is either a wave with twice the frequency (phi = 0.5 lambda) or with the same freq again (phi = 0). (However, when causing NoteOn event with the same freq on both channels at the same time phi always will be 0).

Now this problem isn't with twice the same frequency only. Imagine you want to play 400Hz on ch0 and 405 on ch1. Both waves will have different lambdas, so there will be a phi and phi(t) always will oscillate (that means, you will hear two voices, but not at a constant frequency; frequencies are oscillating depending on how fast phi(t) oscillates).

But(!): when phi(t) oscillates just fast enough, you will not realize this anymore, and you will hear two voices at a constant frequency each :-)

Thus, when playing f at ch0 and f' at ch1 simultaneously: Having a look for this, you can use even two channel mode in a proper quality. Otherwise, you could use oscillating frequencies also for nice sound effects :-)


3. Interfacing MIDI functions

3.1. Basics

MIDI functions are based upon the essential sound functions. What they do simply is causing events (FreqOn, NoteOn, NoteOff, SetInstrument) automatically that are stored in a MIDI track. This is usefull, because it allows to play music in a program without having a care for the track's single events.


3.2. Constants, data structures, routines

3.2.1. Channel tracks and MIDI tracks

const rate_freeze  = 0; //infinite
      rate_highest = 1; //1/50s    //note: you may use even all other
      rate_high    = 2; //1/25s    //      sampling rates in the range
      rate_normal  = 3; //1/16.67s //      of 1..$FFFF
      rate_low     = 5; //1/10s
      rate_lower   = 8; //1/06.25s

#define rate_freeze 0
...

const freq_setinstrument = $8000 //SetInstrument event
#define freq_setinstrument 0x8000

//---Channel tracks + MIDI tracks-----------------------------------------------

type pchtrack = ^tchtrack;
     tchtrack = array[0..0] of record
                                 time: word; //time when event occurs
                                 freq: word; //prepared frequency or other event params
                               end;

typedef struct { word time; //time when event occurs
                 word freq; //prepared frequency or other event params
               } tchtrack[];

typedef tchtrack *pchtrack;

type pmidtrack = ^tmidtrack;
     tmidtrack = record
                   samples   : word;     //track time in samples
                   rate      : byte;     //sampling rate in 1/50s; see rate_xxx
                   ch1events : word;     //channel1 track's count of events
                   ch2events : word;     //channel1 track's count of events
                   ch1track  : pchtrack; //track for channel 1 (nil = none)
                   ch2track  : pchtrack; //track for channel 2 (nil = none)
                 end;

typedef struct { word samples;      //track time in samples
                 byte rate;         //sampling rate in 1/50s; see rate_xxx
                 word ch1events;    //channel1 track's count of events
                 word ch2events;    //channel1 track's count of events
                 pchtrack ch1track; //track for channel 1 (nil = none)
                 pchtrack ch2track; //track for channel 2 (nil = none)
               } tmidtrack;

typedef tmidtrack *pmidtrack;
Channeltrack (chtrack) is an array of single events that can occur on a channel. What an array field contains is: the time when the event shall occur (time) and the event param (freq). Time is given in samples number, so absolute time depends on the sampling rate. The param is the prepared frequency of an NoteOn event. If this is 0xFFFF (note_quite), it's used as an note_off event. Additionaly, you can have a SetInstrument event when setting freq to (freq_setinfstrument || pat), where pat is the byte containing the instrument (see tchannel structure).

A Miditrack consists of one or two channeltracks (ch1track, ch2track; they may be nil/NULL however), where ch1events and ch2events give the amount of events stored. Rate indicates the time used per sample in 1/50s (so rate = 1 is the fastest; 0 indicates to freeze the track), and samples the length of the track in seconds (note: this has nothing to do with the count of samples).

Example:
var mychanneltrack: array[0..9] of word = ( 0,freq_setinstrument or $AA,
                                            0,note_c shr 3,
                                           30,note_e shr 3,
                                           60,note_g shr 3,
                                           90,note_quite);

var mymidtrack: tmidtrack = (samples: 120; rate: rate_normal;
                             ch1events: 4; ch2events: 0;
                             ch1track: *mychanneltrack; ch2track: NULL);

tchtrack mychanneltrack = (( 0,freq_setinstrument || 0xAA),
                           ( 0,note_c >> 3),
                           (30,note_e >> 3),
                           (60,note_g >> 3),
                           (90,note_quite));

tmidtrack mymidtrack = (120,rate_normal,
                        4,0,
                        *mychanneltrack,NULL);

3.2.2. MIDI state

const mid_repeat = 1; //repeat when end of track has been reached
#define mid_repeat 1

type pmidstate = ^tmidstate;
     tmidstate = record
                   midi  : pmidtrack; //midi track to play
                   flags : byte;      //mid_xxx
                   srate : byte;      //sampling rate in 1/50s
                   ch1phys: byte;     //map midi channel 1 to physical channel
                   ch2phys: byte;     //map midi channel 2 to physical channel
                   rtick : byte;      //reserved - internal use
                   csmple: word;      //reserved - internal use (current sample)
                   ch1evnt: word;     //reserved - internal use (current channel 1 event)
                   ch2evnt: word;     //reserved - internal use (current channel 2 event)
                 end;

typedef struct { pmidtrack midi; //midi track to play
                 byte flags;     //mid_xxx
                 byte srate;     //sampling rate in 1/50s, see rate_xxx; may differ from midtrack.rate
                 byte ch1phys;   //map midi channel 1 to physical channel
                 byte ch2phys;   //map midi channel 2 to physical channel
                 byte rtick;     //reserved - internal use
                 word csmple;    //reserved - internal use (current sample)
                 word ch1evnt;   //reserved - internal use (current channel 1 event)
                 word ch2evnt;   //reserved - internal use (current channel 2 event)
               } tmidstate;

typedef tmidstate *pmidstate;
This indicates the state of a MIDI track curretnly playing. The only fields of this structure you have to care for are midi, flags, srate, ch1phys and ch2phys. The other ones are for internal use.


3.2.3. MIDI routines

procedure midiinit(mid: pmidstate; midtrack: pmidtrack); //init midi state according to midi track
function midiplay(mid: pmidstate): boolean;              //play midi track
procedure midijump(mid: pmidstate; smple: word);         //inner-track jump to certain sample
procedure miditrace(mid: pmidstate; smple: longint);     //inner-track relative jump to sample

void midiinit(pmidstate mid, pmidtrack midtrack); //init midi state according to midi track
byte midiplay(pmidstate mid);                     //play midi track
void midijump(pmidstate mid, word smple);         //inner-track jump to certain sample
void miditrace(pmidstate mid, longint smple);     //inner-track relative jump to sample
After discussing the MIDI data structres, here the routines right now:
Example:
...

var mychanneltrack: array[0..9] of word = ( 0,freq_setinstrument or $AA,
                                            0,note_c shr 3,
                                           30,note_e shr 3,
                                           60,note_g shr 3,
                                           90,note_quite);

var mymidtrack: tmidtrack = (samples: 120; rate: rate_normal;
                             ch1events: 4; ch2events: 0;
                             ch1track: @mychanneltrack; ch2track: NULL);

...

var mymidstate: tmidstate;

procedure mycontroller; interrupt;
begin
  midiplay(mymidstate);
  if keypressed then soundclose;
end;

...

soundopen;
midiinit(mymidstate,mymidtrack);
oldtimer := intvects[$1C];
intvects[$1C] := @mycontroller;
soundplay;
intvects[$1C] := oldtimer;
...

4. Special Thanks

Special Thanks go to:

4nic8