2.10. The sound lowlevel interface

This file is intended for programmers who would like to port the sound output routines to their platform. It gives a short outline what services have to be provided.

You should also have a look at the exisiting files, SOUNDLOW.CC, SOUNDMOD.CC and e.g. SOUNDLNX.CC for Linux or SOUNDWIN.CC for Windows and their respective header files to get an idea about how these things really work.

2.10.1. Files

The main include file for a lowlevel sound driver is iodev.h. It has all definitions for the system-independent functions that a sound driver uses. The sound driver also needs to include soundlow.h for the definitions of the base classes bx_sound_lowlevel_c, bx_soundlow_waveout_c, bx_soundlow_wavein_c and bx_soundlow_midiout_c.

Additionally, every output driver will have an include file, which should be included on top of soundmod.cc to allow the emulator to use that driver. The code to initialize the object for the selected drivers can be found in that file, so a soundcard emulation does not need to include the specific driver headers.

To actually make the emulator use any specific driver as the default, BX_SOUND_LOWLEVEL_NAME has to be set to the name of the respective driver.

Note that if your class contains any system-specific statements, include-files and so on, you should enclose both the include-file and the CC-file in an #if defined (OS-define) construct. Also don't forget to add your file to the list of lowlevel sound object files (SOUNDLOW_OBJS) in the file configure.in and to regenerate the configure script,

2.10.2. Defines and strutures

#define BX_SOUNDLOW_WAVEPACKETSIZE  19200

#define BX_SOUNDLOW_OK   0
#define BX_SOUNDLOW_ERR  1

typedef struct {
  Bit16u samplerate;
  Bit8u  bits;
  Bit8u  channels;
  Bit8u  format;
  Bit16u volume;
} bx_pcm_param_t;

const bx_pcm_param_t default_pcm_param = {44100, 16, 2, 1};

The maximum size of a wave data packet, the return values of the lowlevel functions, the structure for the PCM parameters and the default parameter set are also important for the sound driver development. They can be found in the main include file soundlow.h.

All lowlevel sound methods called from the device code have to return either BX_SOUNDLOW_OK if the function was successful, or BX_SOUNDLOW_ERR if not. If any of the initialization functions fail, the device emulation should disable the affected feature.

2.10.3. Classes

The following classes are involved with the sound lowlevel interface:

2.10.4. The base class bx_sound_lowlevel_c

class bx_sound_lowlevel_c : public logfunctions {
public:
  bx_sound_lowlevel_c();
  virtual ~bx_sound_lowlevel_c();

  virtual bx_soundlow_waveout_c* get_waveout() {return NULL;}
  virtual bx_soundlow_wavein_c* get_wavein() {return NULL;}
  virtual bx_soundlow_midiout_c* get_midiout() {return NULL;}

protected:
  bx_soundlow_waveout_c *waveout;
  bx_soundlow_wavein_c *wavein;
  bx_soundlow_midiout_c *midiout;
};

The base class for sound lowlevel support is derived from the logfunctions class to make the Bochs logging capabilities available in the sound driver code. The constructor of this base class only initializes all pointers to NULL and the destructor deletes the objects if necessary.

2.10.5. The waveout base class bx_soundlow_waveout_c

class bx_soundlow_waveout_c : public logfunctions {
public:
  bx_soundlow_waveout_c();
  virtual ~bx_soundlow_waveout_c();

  virtual int openwaveoutput(const char *wavedev);
  virtual int set_pcm_params(bx_pcm_param_t *param);
  virtual int sendwavepacket(int length, Bit8u data[], bx_pcm_param_t *src_param);
  virtual int get_packetsize();
  virtual int output(int length, Bit8u data[]);
  virtual int closewaveoutput();

  virtual int register_wave_callback(void *, get_wave_cb_t wd_cb);
  virtual void unregister_wave_callback(int callback_id);

  virtual bool mixer_common(Bit8u *buffer, int len);
protected:
  void convert_pcm_data(Bit8u *src, int srcsize, Bit8u *dst, int dstsize, bx_pcm_param_t *param);
  void start_mixer_thread(void);

  bx_pcm_param_t emu_pcm_param, real_pcm_param;
  int cvt_mult;

  int cb_count;
  struct {
    void *device;
    get_wave_cb_t cb;
  } get_wave[BX_MAX_WAVE_CALLBACKS];
  int pcm_callback_id;
};

The base class for wave output support is also derived from the logfunctions class. In addition to wave output methods used from sound devices, it contains everything required for the mixer thread feature (register PCM sources, convert data formats, start mixer).

The constructor should not allocate the output devices. This should be done in openwaveoutput().

This table shows the waveout class methods, where are they called from and if a platform / library specific implementation is required.

Table 2-4. Waveout methods

MethodCalled fromPlatform code
openwaveoutput()Sound init codeRequired
set_pcm_params()openwaveoutput() and sendwavepacket()Required
sendwavepacket()Sound device emulationOptional
get_packetsize()Mixer threadOptional
output()Mixer threadRequired
closewaveoutput()Sound device emulationOptional
register_wave_callback()openwaveoutput() and sound device emulationOptional
unregister_wave_callback()class destructor and sound device emulationOptional
mixer_common()Mixer threadOptional
convert_pcm_data()InternalNo
start_mixer_thread()InternalNo

2.10.5.1. int openwaveoutput(const char *wavedev)

openwaveoutput() is called when the sound output subsystem initializes. It should do the following:

  • Set up the default PCM parameters for output.

  • Open the given device, and prepare it for wave output.

  • Register the callback function for the PCM buffer queue (sendwavepacket() adds the output to the queue and the mixer thread gets it from there).

  • Start the mixer thread, unless the sound library has it's own one (e.g. SDL).

openwaveoutput() will only be called once, whereas set_pcm_params() is called whenever the PCM samplerate has been changed.

The parameters are the following:

  • wavedev is the wave output device selected by the user. It is strictly system-dependent. Some sound libraries currently ignore this value and use the default one instead. The value is that of the waveout=device configuration parameter of the sound bochsrc option.

Note that only one wave output device will be used at any one time. wavedev may not have the same value throughout one session, but it will be closed before it is changed.

2.10.5.2. int set_pcm_params(bx_pcm_param_t *param)

This function should called from openwaveoutput() to initialize the output device with the default parameters and from sendwavepacket() whenever the samplerate has been changed in the emulated sound device. It should do the following:

  • Open the wave output device, unless openwaveoutput() did that already.

  • Prepare the device for data and set the device parameters to those given in the function call.

The parameters are the following:

  • param is a pointer to a structure containing the set of parameters required to set up a sound device for PCM output.

The members of the structure bx_pcm_param_t are these:

  • samplerate is the desired frequency of the output. Because of the capabities of the soundcards, it can have any value between 5000 and 48,000.

  • bits is either 8 or 16, denoting the resolution of one sample.

  • channels is the number of channels (2 for stereo output, or 1 for mono output.

  • format is a bit-coded value (see below).

  • volume is the output volume to be used by the mixer code. The 16 bit value consists of two 8 bit values for each channel.

Table 2-5. format bits

Bit numberMeaning
0 (LSB)

0: unsigned data

1: signed data

1..6 Type of codec (see below)
7

0: no reference byte

1: with reference byte

8..x reserved (0)

Table 2-6. codecs

ValueMeaning
0 PCM (raw data)
1 reserved
2 2-bit ADPCM (Creative Labs format)
3 2.4-bit (3-bit) ADPCM (Creative Labs format)
4 4-bit ADPCM (Creative Labs format)

Other codecs are not supported by the SB hardware. In fact, most applications will translate their data into raw data, so that in most cases the codec will be zero.

The number of bytes per sample can be calculated from this as (bits / 8) * channels.

2.10.5.3. int sendwavepacket(int length, Bit8u data[], bx_pcm_param_t *src_param)

This function is called whenever a data packet of at most BX_SOUNDLOW_WAVEPACKETSIZE is ready at the soundcard emulation. It should then do the following:

  • Add this wave packet to the waveout buffer chain after converting to 16 bit signed little endian. If the samplerate has been changed set_pcm_params() should be called to update the sound hardware settings.

Parameters:

  • length is the number of data bytes in the data stream. It will never be larger than BX_SOUNDLOW_WAVEPACKETSIZE.

  • data is the array of data bytes.

  • src_param is a pointer to a structure containing the PCM parameters (see above).

The order of bytes in the data stream is the same as that in the Wave file format:

Table 2-7. wave output types

Output typeSequence of data bytes
8 bit mono Sample 1; Sample 2; Sample 3; etc.
8 bit stereo Sample 1, Channel 0; Sample 1, Channel 1; Sample 2, Channel 0; Sample 2, Channel 1; etc.
16 bit mono Sample 1, LSB; Sample 1, MSB; Sample 2, LSB; Sample 2, MSB; etc.
16 bit stereo Sample 1, LSB, Channel 0; Sample 1, MSB, Channel 0; Sample 1, LSB, Channel 1; Sample 1, MSB, Channel 1; etc.

Typically 8 bit data will be unsigned with values from 0 to 255, and 16 bit data will be signed with values from -32768 to 32767, although the soundcard emulations are not limited to this. site.

2.10.5.4. int get_packetsize()

This function is called from the mixer thread to retrieve the size of a wave data packet based on the current samplerate. By default the packet size is big enough to send output for 0.1 seconds. If the host sound driver / library uses a different value, this value should be returned with this method.

2.10.5.5. int output(int length, Bit8u data[])

This function is called from the mixer thread to send the mixed PCM output to the host sound hardware.

Parameters:

  • length is the number of data bytes in the data stream. It will never be larger than the value returned from get_packetsize.

  • data is the array of data bytes.

2.10.5.6. int closewaveoutput()

This function is currently only called from the soundcard emulation if the "file" driver is used. This makes the runtime change of the output file possible. By default this method does nothing and the wave output device is closed in the destructor of the specific class.

2.10.5.7. int register_wave_callback(void *arg, get_wave_cb_t wd_cb)

This function is called from openwaveoutput() to register the function to retrieve data from the PCM output buffer chain. Other sound emulation devices (e.g. OPL3, PC speaker) can register a function to poll the data from the device emulation. The return value is the ID of the registered function and it is usually used to unregister the source.

Parameters:

  • arg is the pointer to the device emulation object.

  • wd_cb is the pointer to a static function that returns wave data from the device emulation. This function is usually called from the mixer_common() method.

2.10.5.8. void unregister_wave_callback(int callback_id)

This function is usually called from the destructor of the sound emulation device to unregister it's registered function to poll PCM data. If the driver / library doesn't use the default mixer thread, a specific implementation of this method my be required.

Parameter:

  • callback_id is the ID of the function to unregister.

2.10.5.9. bool mixer_common(Bit8u *buffer, int len)

This is the main wave output mixing function. It is called from the mixer thread, it polls the wave data from all registered sources and it mixes the data using a simple algorithm (addition and clipping). The return value indicates whether or not wave data is available for output.

Parameters:

  • buffer is the output buffer for the wave data.

  • len is the maximum length of the output buffer.

2.10.5.10. void convert_pcm_data(Bit8u *src, int srcsize, Bit8u *dst, int dstsize, bx_pcm_param_t *param)

This function converts the PCM data sent from the sound device emulation to the 16 bit stereo signed little endian format. It should be called in sendwavepacket() after allocating the output buffer in the buffer queue. Future versions might also perform resampling here.

Parameters:

  • src is the buffer containing data sent from the sound emulation.

  • srcsize is the amount of wave data to be converted.

  • dst is the buffer for the converted wave data.

  • dstsize is the size of the destination buffer.

  • param is a pointer to the struture containing the format parameters of the source data.

2.10.5.11. void start_mixer_thread()

This function starts the mixer thread and it should be called in openwaveoutput() unless the sound driver / library has it's own way to do this (e.g. SDL). This function also initializes the mutex required for locking the mixer thread when adding data to the buffer chain or unregistering a source.

2.10.6. The wavein base class bx_soundlow_wavein_c

class bx_soundlow_wavein_c : public logfunctions {
public:
  bx_soundlow_wavein_c();
  virtual ~bx_soundlow_wavein_c();

  virtual int openwaveinput(const char *wavedev, sound_record_handler_t rh);
  virtual int startwaverecord(bx_pcm_param_t *param);
  virtual int getwavepacket(int length, Bit8u data[]);
  virtual int stopwaverecord();

  static void record_timer_handler(void *);
  void record_timer(void);
protected:
  int record_timer_index;
  int record_packet_size;
  sound_record_handler_t record_handler;
};

The base class for wave input support is also derived from the logfunctions class. It contains the framework for wave input (recording) support. The base class is used by the "dummy" sound driver and returns silence to let the input mechanism of the soundcard emulation work. The soundcard emulator object needs to implement a callback function to notifies the emulation about available data. This function usually calls the driver method to get the wave data packet. The driver objects has a periodic timer with an interval of 0.1 emulated seconds that is active during recording. The timer handler processes the wave data recorded with platform or library specific function and finally notifies the emulator.

The constructor of the base class only initializes the timer ID. OS specific implementations should initialize other required members here.

The destructor of the base class only calls stopwaverecord(). OS specific implementations should close the input device here if necessary.

2.10.6.1. int openwaveinput(char *device, sound_record_handler_t rh)

openwaveinput() is called when the sound emulation first receives a sound recording command. It should do the following:

  • Open the given device, and prepare it for wave input

or

  • Store the device name so that the device can be opened in startwaverecord().

In addition to this the record handler value should be stored and the record timer should be registered. This is the definition of record handler callback function:

typedef Bit32u (*sound_record_handler_t)(void *arg, Bit32u len);

openwaveinput() will only be called once, whereas startwaverecord() is called for every new wave input command to the soundcard emulation. If feasible, it could be useful to open and/or lock the input device in startwaverecord() as opposed to openwaveinput() to ensure that it can be used by other applications while Bochs doesn't need it.

The parameters are the following:

  • device is the wave device selected by the user. It is strictly system-dependent. The value is that of the wavein=device configuration parameter of the sound bochsrc option.

  • rh is a pointer to the record handler method of the sound emulation. When sound recording is active, this handler is called periodicly to notify the sound emulation about newly available data.

Note that only one wave input device will be used at any one time. device may not have the same value throughout one session, but it will be closed before it is changed.

2.10.6.2. int startwaverecord(bx_pcm_param_t *param)

This method receives a pointer to the required PCM parameters (samplerate, data format) as the argument and it should set up the input device for recording, calculate the size of the recording packet for 0.1 second and start the record timer.

2.10.6.3. int getwavepacket(int length, Bit8u data[])

This method is called from the record handler method of the sound emulation device to retrieve the recorded wave data packet.

2.10.6.4. int stopwaverecord()

This method is called to stop the wave recording. It deactivates the timer that calls the method to perform the recording.

2.10.7. The midiout base class bx_soundlow_midiout_c

class bx_soundlow_midiout_c : public logfunctions {
public:
  bx_soundlow_midiout_c();
  virtual ~bx_soundlow_midiout_c();

  virtual int openmidioutput(const char *mididev);
  virtual int midiready();
  virtual int sendmidicommand(int delta, int command, int length, Bit8u data[]);
  virtual int closemidioutput();
};

The base class for MIDI output support is also derived from the logfunctions class.

OS specific implementations should initialize required members in the constructor.

The destructor of the base class only calls closemidioutput(). OS specific implementations should close the input device here if necessary.

2.10.7.1. int openmidioutput(char *device)

  • openmidioutput() is called when the first midi output starts. It is only called if the midi output to the driver is active (midimode 1). It should prepare the given MIDI hardware for receiving midi commands.

Description of the parameters:

  • mididev is a system-dependent variable. The value is that of the midiout=device configuration parameter of the sound bochsrc option.

  • Note that only one midi output device will be used at any one time. device may not have the same value throughout one session, but it will be closed before it is changed.

2.10.7.2. int midiready()

midiready() is called whenever the applications asks if the midi queue can accept more data.

Return values:

  • BX_SOUNDLOW_OK if the midi output device is ready.

  • BX_SOUNDLOW_ERR if it isn't ready.

Note: midiready() will be called a few times before the device is opened. If this is the case, it should always report that it is ready, otherwise the application (not Bochs) will hang.

2.10.7.3. int sendmidicommand(int delta, int command, int length, Bit8u data[])

sendmidicommand()is called whenever a complete midi command has been written to the emulator. It should then send the given midi command to the midi hardware. It will only be called after the midi output has been opened. Note that if at all possible it should not wait for the completion of the command and instead indicate that the device is not ready during the execution of the command. This is to avoid delays in the program while it is generating midi output.

Description of the parameters:

  • delta is the number of delta ticks that have passed since the last command has been issued. It is always zero for the first command. There are 24 delta ticks per quarter, and 120 quarters per minute, thus 48 delta ticks per second.

  • command is the midi command byte (sometimes called status byte), in the usual range of 0x80..0xff. For more information please see the midi standard specification.

  • length is the number of data bytes that are contained in the data structure. This does not include the status byte which is not replicated in the data array. It can only be greater than 3 for SysEx messages (commands 0xF0 and 0xF7)

  • data[] is the array of these data bytes, in the order they have in the standard MIDI specification. Note, it might be NULL if length==0.

2.10.7.4. int closemidioutput()

closemidioutput() is called before shutting down Bochs or when the emulator gets the stop_output command through the emulator port. After this, no more output will be necessary until openmidioutput() is called again, but midiready() might still be called. It should do the following:

  • Wait for all remaining messages to be completed

  • Reset and close the midi output device