Xeen supports a variety of audio drivers to output wave sound and MIDI music. Each of these drivers is stored in the game's primary resource CC file. These sound drivers are also responsible for providing timing functions to the game engine.
- 1 Driver Filenames
- 2 Music Driver Functions
- 3 Playing Music and Sound Effects
- 4 Sound Driver Functions
Each driver file is named according to a standard: the filename begins with a prefix which indicates the kind of sound card the driver is for, and the filename ends with the letters "MUS" for a music (MIDI) driver or "SND" for a wave sound driver.
Internally, the game uses the terms "speak", "sound", and "music" somewhat interchangeably. "Sound" and "music" generally refer to the MIDI music driver, while "speak" commands refer to the wave sound driver.
|Roland Canvas / GS||CANxxx|
|Covox Sound Master 2||COVXSND||Speech only|
|Sound Blaster Pro||PROxxx|
|Roland LAPC-1 / MT-32||ROLxxx|
|Wave Blaster||WAVEMUS||MIDI only|
These driver files consist entirely of standard x86 executable code, and work by exposing "jump" commands at specified locations. The drivers are loaded in to memory when the game starts, and the game jumps to a specific offset in the driver to perform any given function. For example, to initialize the music driver, the game will jump to seg:0000 where "seg" is the segment the driver has been loaded to.
The entry points listed below exist in all drivers. The initialization routines use the following variables, all of which (except seg_music) are read directly from the configuration file:
- soundAddr is the address of the sound card (default is 0x220)
- musicAddr is the address of the music card (default is 0x388)
- irq is the sound card's IRQ (default is 7)
- volumeSet is a boolean indicating whether or not the game may set the sound card's default volume levels (only used by some drivers)
- seg_music is the segment address that the driver has been loaded in to
- soundDMA is the DMA setting of the sound card (default is 1)
Music Driver Functions
This function takes four uint16 arguments: musicAddr, volumeSet, initString, soundAddr.
initString is a pointer to a theoretical string of MIDI commands that will be passed to the MPU on startup. In practice, it is always 0, and should be safely ignored.
Shuts down the driver.
The "song" function is called with a uint16 command argument, and any number of further arguments depending on the command being executed.
Command: 0000h Argument: none
Stops the music. Called when the music flag is flipped off in the control panel.
Command: 0001h Argument: none
Restarts the currently playing music to the beginning of the file
Command: 0100h Argument: Volume Level (uint16)
This function is called with two volume levels: 48 and 95, which indicate the probable range of accepted values is 0 to 100. This function is called in the in-game cutscenes, specifically the Sphinx, Golem, Reaper, and Dwarf events.
Set "Something" (Song playing?)
Command: >0001h but <=00FFh (00CF used everywhere, apparently) Argument: none
Sets the "Something" returned by command FFE0h to the command value.
Return "Something" (Song playing?)
Command: FFE0h Argument: none
Returns a byte variable of as yet unknown use. Suspected to be set true when a sound is playing. Can be set by the above Set command.
Command: >0100h Argument: ignored (uint16), stop (uint16)
The command value is actually the segment the song has been loaded to in memory. It abuses the segmented nature of real mode by starting the music right at the start of a segment, so the offset 0 is the start of the file. Takes two arguments. The first is ignored, the second is a boolean value which tells the music player that it needs to stop the current note on a channel before starting the next one. Both arguments are always set to 0 anyway.
This is an external link to the music interrupt function which is triggered 72.8 times per second. There should never be a need to call this directly.
Two zero bytes that do nothing.
"Set Instrument" Function
An external link to a "Set Instrument" function similar to the one called in the course of music playback. Two uint16 arguments are channel and instrument number. This is not called anywhere in any known code, and may be a holdover from some debugging functions.
Playing Music and Sound Effects
In order to play music, the "Song" function must be called with the segment the .M file has been loaded into. The function will set the offset to 0, set the "musicPlaying" boolean to true, and count down timer to 0. Similar in the call to "FX" function, setting the "fxPlaying" flag to true, and the respective count down timer to 0.
At the start of every interrupt, first check boolean variable "musicPlaying". If true, there's currently a .M file loaded, and the interrupt will attempt to process it.
Assuming music is playing, it then decrements and checks the music delay timer. If it is zero, it proceeds to read the next command from the file and execute it. It will then loop back to the top, reading and executing commands until it encounters command 0x1, which sets the delay timer, and exits the interrupt.
If the "musicPlaying" flag is false, or the music delay timer is not yet zero, it proceeds to the FX execution code.
In the FX execution code, it checks "fxPlaying" flag. If false, it simply exits the interrupt as there is no music and no FX commands to execute.
If the "fxPlaying" flag is true, it decrements and checks the FX delay timer. Similar to the music delay timer, if it is zero, it proceeds to read and execute commands, starting from where it last left off.
If the FX delay timer is not yet zero, it exits the interrupt.
These are commands used to control the music and sound effects (not to be confused with the digital sound voices)
Music (.M) file Commands
Commands from the .M file are played sequentially unless otherwise stated.
|Command||Use Channel||Data Bytes||FM/MIDI||Description|
|0x0||no||none||both||Records current position and next instruction into some sort of stack for later use (?) and skips to a new position|
|0x1||yes||none||both||Sets the count down timer delay, performs some cleanup, and exits the interrupt. If "channel" == 0, read the next byte and set the delay to that, else it uses the "channel" value.|
|0x2||yes||26||both||Defines an instrument that can be played. The "channel" nibble is actually the instrument number, to be referenced later to assign instruments to channels. Detail below.|
|0x3||no||none||none||This does nothing but return to the top for the next instruction.|
|0x4||yes||2||MIDI||sets the pitch wheel for the given channel. Only the first data byte is relevant: 128 is center and will produce the normal tone. The second data byte is ignored. FM simply skips 2 bytes.|
|0x5||no||2||none||This does nothing but skip two bytes and return to the top for the next instruction.|
|0x6||yes||1||MIDI||Changes the pan (left/right) of the selected channel. FM music is mono, so simply reads the byte and returns to the top for the next instruction. 128 is center.|
|0x7||no||none||none||This does nothing but return to the top for the next instruction.|
|0x8||yes||none||both||This fades a note off in MIDI. May not behave exactly the same in FM.|
|0x9||yes||2||both||Starts channel playing a specific note. The first data byte is the note, the second is the fade in rate. Note details below. Fade in rate is used only for MIDI.|
|0xA||yes||2||both||Sets volume for selected channel as a percentage of max volume. If MIDI and first byte is 0, the second byte is the volume, else this command is ignored. If FM and first byte is 5, the second byte is the volume, else this command is ignored. Each driver|
|0xB||no||X||MIDI||Sends an unknown number of bytes from the file directly to the MIDI port, terminating when 0xF7 is read. FM simply loops the file, ignoring bytes until 0xF7 is read.|
|0xC||yes||1||both||Sets channel to play the given instrument number, as defined in instruction 0x2 above. Data byte must not exceed 0x0F (15)|
|0xD||yes||none||FM||Clears (sets to zero) something. Unknown purpose: byte_111[channel]|
|0xE||yes||3||FM||Sets some things. Unknown purpose: byte_160[channel] = data, word_169[channel] = data . data (read two bytes to AX and xchang), byte_111[channel] = 1, byte_17B[channel] = 0xFF. MIDI simply advances file by 3.|
|0xF||yes||none||both||Depending on "channel" selected, may pop position from the stack (see instruction 0x0), or reset the file to start for looping.|
FX Commands are stored in internal structures as a sequence of bytes, similar (but different) to the .M music files, though obviously much shorter. This means there are a finite number of effects that can be played, all hard-coded directly in the driver. As a result, each driver also has a slightly different set of commands, though mostly similar. More investigation may be needed for this, though it should be sufficient as a black box now.
|Command||Use Channel||Data Bytes||FM/MIDI||Description|
|0x0||no||none||FM||Records current position and next instruction into some sort of stack for later use and skips to a new position, as 0x0 above. FM only this time; MIDI ignores. May be useless code.|
|0x1||yes||0 or 1||both||Sets the count down timer delay, performs some cleanup, and exits the interrupt. If "channel" == 0, read the next byte and set the delay to that, else it uses the "channel" value.|
|0x2||yes||1 or 11||both||records a byte, but in different ways for MIDI and FM. Also skips one data byte for MIDI and 11 for FM. Needs more investigation.|
|0x3||yes||2||both||Sets volume for selected channel as a percentage of max volume from first data byte. Same as 0xA in above music commands.|
|0x4||yes||none||MIDI||Resets something. word_10E[channel] = 0. Unknown purpose. FM ignores. Needs more investigation.|
|0x5||yes||0 or 4||MIDI||Stores next two words. FM ignores, but does not advance four data bytes. Needs more investigation.|
|0x6||yes||1||both||Changes the pan (left/right) of the selected channel for MIDI. Does something slightly different for FM. Needs more investigation.|
|0x7||yes||none||FM||Turns the selected channel off. Ignored for MIDI.|
|0x8||yes||1||both||Does something? Appears to stop selected note. Needs more investigation|
|0x9||yes||1 or 2||both||Starts a note playing on selected channel. First data byte is the note. For MIDI, second data byte is the fade in rate. FM doesn't count a second byte. Needs more investigation.|
|0xA||no||none||none||This does nothing but return to the top for the next instruction.|
|0xB||no||X||MIDI||Sends an unknown number of bytes from the file directly to the MIDI port, same as 0xB in above music commands. IGNORED COMPLETELY BY FM! Does not advance in file like other call for FM. Needs more investigation.|
|0xC||yes||1||both||Sets channel to play the given instrument number, as defined in instruction 0x2 above. Data byte must not exceed 0x0F (15). Same as above music command.|
|0xD||yes||none||FM||Clears (sets to zero) something. Unknown purpose: byte_111[channel]. Same as above music command.|
|0xE||yes||3||FM||Sets some things. Same as above music command EXCEPT MIDI IS NOT ADVANCED. MIDI simply ignores the command entirely. Needs more investigation.|
|0xF||yes||none||both||Changes position in file if channel == 0xF, else ends the interrupt. Needs more investigation.|
Sound Driver Functions
This function takes four uint16 arguments: soundAddr, seg_music, irq, soundDMA.
Shuts down the driver.