=== AFX Sound V. 0.91 ===
=== Library Documentation ===
Contents
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):
mailofmarco"at"gmx.de (my default one) and
marco5327"at"yahoo.de (alternative)
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:
- Works also with Digital Mars compilers right now (see chapter 1.4.)
- A new C demo is available in Exmpls folder (thanks to 4nic8 who wrote it!)
- Minor bug fixed in snddemo.pas and mididemo.pas
- Modified soundopen() procedure
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++Just include afxsound.c into your program.
Using AFXSound with Digital MarsInclude afxsound_dm.c instead of afxsound.c
Using AFXSound with TP 6.0Copy AFXSound.tpu into your unit directory and note down "afxsound" in the program's uses clause
Using with other Pascal versionsCompile AFXSound.pas a unit
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:
- Background music for low performance games such as Tetris
- Music in intros and menus
- Maybe short sound effects even for high performance games
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:
- Avoid f = f'!
- Also avoid f and f' being multiples of each other (f = n*f' or n*f = f' with n is an integer)
- Ensure that both differ from each other and their multiples for about 10 or 15% at least
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.
- midi: this is a pointer to a midi track those state is represented
- flags: combination of mid_xxx (currently, there's mid_repeat only)
- srate: sampling rate the midi is to be played at (note: this may differ from rate field of tmidtrack structure)
- ch1phys,ch2phys: indicate the physical channels 1,0 where logical channels 1,0 are mapped to (for example you can map logical channel 1 to physical one 0; this may be sefull when playing different midi tracks synchronously)
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:
- midiinit: connects a midistate with a miditrack and initialises the state
- midiplay: this plays a midi represented by a state. Note: you have to call this routine 50 times a sec by timer, but not as an interrupt directly!
- midijump: set's the pointer of a midi state to a certain position (and continues playing there)
- miditrace: like midijump, but jump is relative
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;
...
Special Thanks go to:
4nic8
For testing the C version of AFXSound, bugreprot and making the SWEETSOUND demo.