                                   SndLib

                 Bill Schottstaedt (bil@ccrma.stanford.edu)

Contents

Introduction
Headers
Data
Hardware
Examples
     SndInfo
     SndPlay
     SndRecord
     AudInfo
     SndSine
     Other Examples
How to Make Sndlib and the examples
Current Status

Introduction

The sound library is a collection of sound file and audio hardware handlers
written in C and running currently on SGI (either audio library), NeXT
(NeXTStep and OpenStep), Sun, Be, OSS (Linux and others), Mac, HPUX,
MkLinux, and Windoze systems. It provides relatively straightforward access
to many sound file headers and data types, and most of the features of the
audio hardware.

The following files make up sndlib: io.c (read and write sound file data),
headers.c (read and write sound file headers), audio.c (read and write sound
hardware ports), and sound.c (provide slightly higher level access to the
preceding files). The overall header file is sndlib.h. (The stub file
lispcall.c takes care of a function used in C-to-Lisp communication).

Headers

Sound files have built-in descriptors known as headers (we're in computer
land, so no header is considered a kind of header). The following functions
return the information in the header. In each case the argument to the
function is the full file name of the sound file.

  int sound_samples (char *arg)          /* samples of sound according to header (can be incorrect) */
  int sound_datum_size (char *arg)       /* bytes per sample */
  int sound_data_location (char *arg)    /* location of first sample (bytes) */
  int sound_chans (char *arg)            /* number of channels (samples are interleaved) */
  int sound_srate (char *arg)            /* sampling rate */
  int sound_header_type (char *arg)      /* header type (aiff etc) */
  int sound_data_format (char *arg)      /* data format (alaw etc) */
  int sound_original_format (char *arg)  /* unmodified data format specifier */
  char *sound_comment (char *arg)        /* comment if any */
  int sound_comment_start (char *arg)    /* comment start (bytes) if any */
  int sound_comment_end (char *arg)      /* comment end (bytes) */
  int sound_length (char *arg)           /* true file length (for error checks) */
  int sound_fact_samples (char *arg)     /* compression scheme data */
  int sound_distributed (char *arg)      /* is header scattered around in sound file */
  int sound_write_date (char *arg)       /* bare (uninterpreted) file write date */
  int sound_type_specifier (char *arg)   /* original header type identifier */
  int sound_align (char *arg)            /* more compression data */
  int sound_bits_per_sample(char *arg)   /* bits per sample */
  int bytes_per_sample(int format)       /* bytes per sample */

The following can be used to provide user-understandable descriptions of the
header type and the data format:

  char *sound_type_name(int type)          /* "AIFF" etc */
  char *sound_format_name(int format)      /* "16-bit big endian linear" etc */

In all cases if an error occurs, -1 is returned; for information about the
error use:

  int audio_error(void)                    /* returns error code indicated by preceding audio call */
  char *audio_error_name(int err)          /* gives string decription of error code */

Header data is cached internally, so the actual header is read only if it
hasn't already been read, or the write date has changed. Loop points are
also available, if there's interest.

Data

The following functions provide access to sound file data:

  int open_sound_input (char *arg)
  int open_sound_output (char *arg, int srate, int chans, int data_format, int header_type, char *comment)
  int close_sound_input (int fd)
  int close_sound_output (int fd, int bytes_of_data)
  int read_sound (int fd, int beg, int end, int chans, int **bufs)
  int write_sound (int fd, int beg, int end, int chans, int **bufs)
  int seek_sound (int fd, long offset, int origin)
  void float_sound(char *charbuf, int samps, int charbuf_format, float *buffer)

open_sound_input opens arg for reading. Most standard uncompressed formats
are readable. This function returns the associated file number, or -1 upon
failure.

close_sound_input closes an open sound file. Its argument is the integer
returned by open_sound_input.

open_sound_output opens arg, setting its sampling rate to be srate, number
of channels to chans, data format to data_format (see sndlib.h for these
types: snd_16_linear, for example, means 16-bit 2's complement big endian
fractions), header type to header_type (AIFF for example; the available
writable header types are AIFF_sound_file, RIFF_sound_file ('wave'),
NeXT_sound_file, and IRCAM_sound_file), and comment (if any) to comment. The
header is not considered complete without an indication of the data size,
but since this is rarely known in advance, it is supplied when the sound
file is closed. This function returns the associated file number.

close_sound_output first updates the file's header to reflect the final data
size bytes_of_data, then closes the file. The argument fd is the integer
returned by open_sound_output.

read_sound reads data from the file indicated by fd, placing data in the
array obufs as 32-bit integers in the host's byte order. chans determines
how many arrays of ints are in obufs, which is filled by read_sound from its
index beg to end with zero padding if necessary. See the sndplay example
below if this is not obvious.

write_sound writes data to the file indicated by fd, starting for each of
chans channels in obufs at beg and ending at end.

seek_sound moves the read or write position for the file indicated by fd to
offset given the origin indication (both treated as in lseek). The new
actual position attained is returned. In both cases (the returned value and
offset), the output datum size is considered to be 2, no matter what it
really is. That is, use byte positions as if you were always reading and
writing 16-bit data, and seek_sound will compensate if its actually 32-bit
floats or whatever.

float_sound takes a buffer full of sound data in some format (charbuf_format
and returns the data as a buffer full of floats.

Hardware

The following functions provide access to audio harware. If an error occurs,
they return -1, and the audio_error functions can be used to find out what
went wrong.

  int initialize_audio(void)
  void save_audio_state(void)
  void restore_audio_state(void)
  void describe_audio_state(void)
  char *report_audio_state(void)
  int open_audio_output(int dev, int srate, int chans, int format, int size)
  int open_audio_input(int dev, int srate, int chans, int format, int size)
  int write_audio(int line, char *buf, int bytes)
  int close_audio(int line)
  int read_audio(int line, char *buf, int bytes)
  int read_audio_state(int dev, int field, int chan, float *val)
  int write_audio_state(int dev, int field, int chan, float *val)
  int audio_systems(void)
  char *audio_system_name(int system)
  void setup_dsps(int cards, int *dsps, int *mixers) /* OSS only */

initialize_audio takes care of any necessary intialization.

save_audio_state saves the current audio hardware state.

restore_audio_state restores the audio hardware to the last saved state.

describe_audio_state prints to stdout a description of the current state of
the audio hardware. report_audio_state returns the same description as a
string.

audio_systems returns the number of separate and complete audio systems
(soundcards essentially) that are available. audio_system_name returns some
user-recognizable name for the given card.

open_audio_input opens an audio port to read sound data (i.e. a microphone,
line in, etc). The input device is dev (see sndlib.h for details; when in
doubt, use DEFAULT_DEVICE). The input sampling rate is srate or as close as
we can get to it. The number of input channels (if available) is chans. The
input data format is format (when in doubt, use the macro
COMPATIBLE_FORMAT). And the input buffer size (if settable at all) is size
(bytes). This function returns an integer to distinguish its port from
others that might be in use. In this and other related functions, the device
has an optional second portion that refers to the soundcard or system for
that device. AUDIO_SYSTEM(n) refers to the nth such card, so (DAC_DEVICE |
AUDIO_SYSTEM(1)) is the 2nd card's dac (the default is system 0, the first
card).

open_audio_output opens an audio port to write date (i.e. speakers, line
out, etc). The output device is dev (see sndlib.h). Its sampling rate is
srate, number of channels chans, data format format, and buffer size size.
This function returns the associated line number of the output port.

close_audio closes the port (input or output) associated with line.

read_audio reads sound data from line. The incoming bytes bytes of data are
placed in buf. If no error was returned from open_audio_input, the data is
in the format requested by that function with channels interleaved.

write_audio writes bytes bytes of data in buf to the output port associated
with line. This data is assumed to be in the format requested by
open_audio_output with channels interleaved.

read_audio_state and write_audio_state are complicated. They get and set the
audio hardware state. The audio hardware is treated as a set of "systems"
(sound cards) each of which has a set of "devices" (dacs, adcs, etc), with
various "fields" that can be read or set (gain, channels active, etc). For
example, a microphone is called the MICROPHONE_DEVICE, and its hardware gain
setting (if any) is called the AMP_FIELD. All gains are considered to be
linear between 0.0 and 1.0, so to set the microphone's first channel
amplitude to .5 (that is, the gain of the signal before it reaches the
analog-to-digital converter),

  float vals[1];
  vals[0]=0.5;
  write_audio_state(MICROPHONE_DEVICE,AMP_FIELD,0,vals);

Similarly

  read_audio_state(MICROPHONE_DEVICE,AMP_FIELD,0,vals);
  amp=vals[0];

returns the current gain in the float array vals. read_audio_state can also
return a description of the currently available audio hardware.

If a requested operation is not implemented, -1 is returned, and AUDIO_ERROR
is set to CANT_READ or CANT_WRITE. If an error occurs during the requested
operation, -1 is returned, and AUDIO_ERROR is set to READ_ERROR or
WRITE_ERROR. If some operation cannot be performed on the current hardware,
-1 is returned and AUDIO_ERROR tries to indicate what portion of the
requested operation is impossible (SRATE_NOT_AVAILABLE,
FORMAT_NOT_AVAILABLE, and so on).

Systems

Each separate sound card is called a system, accessible via the device
argument through the macro AUDIO_SYSTEM(n). The count starts at 0 which is
the default. The function audio_systems returns how many such cards are
available. (Currently it returns more than one only on Linux systems with
multiple sound cards).

Devices

Each audio system has a set of available devices. To find out what is
available on a given system

  #define LIST_MAX_SIZE 32;
  float device_list[LIST_MAX_SIZE];
  read_audio_state(AUDIO_SYSTEM(0),DEVICE_FIELD,LIST_MAX_SIZE,device_list);

The list of available devices is returned in the device_list array, with the
number of the devices as device_list[0]. The set of device identifiers is in
sndlib.h (LINE_IN_DEVICE for example). Two special devices are MIXER_DEVICE
and DAC_FILTER_DEVICE. The latter refers to the low-pass filter often
associated with a DAC. The former refers to a set of analog gain and tone
controls often associated with a sound card. The individual gains are
accessed through the various fields (described below).

Fields

The field argument in read-audio-state and write-audio-state selects one
aspect of the given card's devices' controls. The simplest operations
involve AMP_FIELD and SRATE_FIELD. The latter gets or sets the sampling rate
of the device, and the former gets or sets the amplitude (between 0.0 and
1.0) of the specified channel of the device. The value to be set or returned
is in the 0th element of the vals array. An example of reading the current
microphone gain is given above. The meaning of the field argument can depend
on which device it is applied to, so there is some complexity here. The
channel argument usually selects which channel we are interested in, but in
some cases it instead tells read-audio-state how big a returned list can
get. A brief description of the fields:

AMP_FIELD       gain or volume control (0.0 to 1.0)
SRATE_FIELD     sampling rate
CHANNEL_FIELD   active channels

BASS_FIELD, TREBLE_FIELD    mixer's tone control
LINE_FIELD      mixer's line-in gain control
MIC_FIELD       mixer's microphone gain control
similarly for IMIX_FIELD, IGAIN_FIELD, RECLEV_FIELD, PCM_FIELD, PCM2_FIELD,
              OGAIN_FIELD, LINE1_FIELD, LINE2_FIELD, LINE3_FIELD, SYNTH_FIELD

FORMAT_FIELD    return list of usable sound formats (e.g. snd_16_linear)
DEVICE_FIELD    return list of available devices (e.g. MICROPHONE_DEVICE)

Due to minor problems in the OSS (Linux) multi-card support, you sometimes
need to set up the map of dsps and mixers by hand. setup_dsps, called after
audio_initialize, sets the number of cards and the device numbers of the
main dsp (0 for /dev/dsp0), and mixer.

Examples

In the following examples I've omitted the usual garrulous C-header gab and
other inessential stuff. The full program code is available as noted below.

SndInfo

This program prints out a description of a sound file (sndinfo.c).

int main(int argc, char *argv[])
{
  int fd,chans,srate,samples;
  float length;
  time_t date;
  char *comment;
  char timestr[64];
  fd = clm_open_read(argv[1]); /* see if it exists */
  if (fd != -1)
    {
      close(fd);
      date = sound_write_date(argv[1]);
      srate = sound_srate(argv[1]);
      chans = sound_chans(argv[1]);
      samples = sound_samples(argv[1]);
      comment = sound_comment(argv[1]);
      length = (float)samples / (float)(chans * srate);
      strftime(timestr,64,"%a %d-%b-%y %H:%M %Z",localtime(&date));
      fprintf(stdout,"%s:\n  srate: %d\n  chans: %d\n  length: %f\n",
              argv[1],srate,chans,length);
      fprintf(stdout,"  type: %s\n  format: %s\n  written: %s\n  comment: %s\n",
              sound_type_name(sound_header_type(argv[1])),
              sound_format_name(sound_data_format(argv[1])),
              timestr,comment);
    }
  else
    fprintf(stderr,"%s: %s\n",argv[1],strerror(errno));
  return(0);
}

SndPlay

This code plays a sound file (sndplay.c):

int main(int argc, char *argv[])
{
  int fd,afd,i,j,n,k,chans,srate,frames,outbytes;
  int **bufs;
  short *obuf;
  fd = open_sound_input(argv[1]);
  if (fd != -1)
    {
      initialize_audio();
      chans = sound_chans(argv[1]);
      srate = sound_srate(argv[1]);
      frames = sound_samples(argv[1])/chans;
      outbytes = BUFFER_SIZE * chans * 2;
      bufs = (int **)calloc(chans,sizeof(int *));
      for (i=0;i<chans;i++) bufs[i] = (int *)calloc(BUFFER_SIZE,sizeof(int));
      obuf = (short *)calloc(BUFFER_SIZE * chans,sizeof(short));
      afd = open_audio_output(DEFAULT_DEVICE,srate,chans,COMPATIBLE_FORMAT,outbytes);
      if (afd != -1)
        {
          for (i=0;i<frames;i+=BUFFER_SIZE)
            {
              read_sound(fd,0,BUFFER_SIZE-1,chans,bufs);
              for (k=0,j=0;k<BUFFER_SIZE;k++,j+=chans)
                for (n=0;n<chans;n++) obuf[j+n] = bufs[n][k];
              write_audio(afd,(char *)obuf,outbytes);
            }
          close_audio(afd);
        }
      close_sound_input(fd);
      for (i=0;i<chans;i++) free(bufs[i]);
      free(bufs);
      free(obuf);
    }
  else
    fprintf(stderr,"%s: %s ",argv[1],audio_error_name(audio_error()));
  return(0);
}

SndRecord

This code records a couple seconds of sound from a microphone. Input formats
and sampling rates are dependent on available hardware, so in a "real"
program, you'd use read_audio_state to find out what was available, then
float-sound to turn that data into a stream of floats. You'd also provide,
no doubt, some whizzy user interface to turn the thing off. (sndrecord.c)

int main(int argc, char *argv[])
{
  int fd,afd,i,err;
  short *ibuf;
#if MACOS
  argc = ccommand(&argv);
#endif
  afd = -1;
  fd = open_sound_output(argv[1],22050,1,snd_16_linear,NeXT_sound_file,"created by sndrecord");
  if (fd != -1)
    {
      ibuf = (short *)calloc(BUFFER_SIZE,sizeof(short));
      afd = open_audio_input(MICROPHONE_DEVICE,22050,1,snd_16_linear,BUFFER_SIZE);
      if (afd != -1)
        {
          for (i=0;i<10;i++) /* grab 10 buffers of input */
            {
              err = read_audio(afd,(char *)ibuf,BUFFER_SIZE*2);
              if (err != NO_ERROR) {fprintf(stderr,audio_error_name(audio_error())); break;}
              write(fd,ibuf,BUFFER_SIZE*2);
            }
          close_audio(afd);
        }
      else
        fprintf(stderr,audio_error_name(audio_error()));
      close_sound_output(fd,BUFFER_SIZE*10*2);
      free(ibuf);
    }
  else
    fprintf(stderr,"%s: %s ",argv[1],strerror(errno));
  return(0);
}

AudInfo

This program describes the current audio harware state (audinfo.c):

int main(int argc, char *argv[])
{
  describe_audio_state();
  return(0);
}

SndSine

This program writes a one channel NeXT/Sun sound file containing a sine wave
at 440 Hz.

int main(int argc, char *argv[])
{
  int fd,i,k,frames;
  float phase,incr;
  int *obuf[1];
  fd = open_sound_output(argv[1],22050,1,snd_16_linear,NeXT_sound_file,"created by sndsine");
  if (fd != -1)
    {
      frames = 22050;
      phase = 0.0;
      incr = 2*PI*440.0/22050.0;
      obuf[0] = (int *)calloc(BUFFER_SIZE,sizeof(int));
      k=0;
      for (i=0;i<frames;i++)
        {
          obuf[0][k] = (int)(3276.8 * sin(phase)); /* amp = .1 */
          phase += incr;
          k++;
          if (k == BUFFER_SIZE)
            {
              write_sound(fd,0,BUFFER_SIZE-1,1,obuf);
              k=0;
            }
        }
      if (k>0) write_sound(fd,0,k-1,1,obuf);
      close_sound_output(fd,22050*c_snd_datum_size(snd_16_linear));
      free(obuf[0]);
    }
  return(0);
}

Other Examples

The primary impetus for the sound library was the development of Snd and
CLM, both of which are freely available.

  ------------------------------------------------------------------------

How to Make Sndlib and the examples

The Sndlib files can be used as separate modules or made into a library. The
following sequence, for example, builds the sndplay program from scratch on
an SGI:

cc -c io.c -O -DSGI
cc -c headers.c -O -DSGI
cc -c audio.c -O -DSGI
cc -c lispcall.c -O -DSGI
cc -c sound.c -O -DSGI
cc sndplay.c -o sndplay -O -DSGI audio.o io.o headers.o lispcall.o sound.o -laudio -lm

To make a library out of the sndlib files, first compile them as above,
then:

ld -r audio.o io.o headers.o lispcall.o sound.o -o sndlib.a
cc sndplay.c -o sndplay -O -DSGI sndlib.a -laudio -lm

The full sequence in Linux:

cc -c io.c -O -DLINUX
cc -c audio.c -O -DLINUX
cc -c headers.c -O -DLINUX
cc -c lispcall.c -O -DLINUX
cc -c sound.c -O -DLINUX
cc sndplay.c -o sndplay -O -DLINUX audio.o io.o headers.o lispcall.o sound.o -lm

ld -r audio.o io.o headers.o lispcall.o sound.o -o sndlib.a
cc sndplay.c -o sndplay -O -DLINUX sndlib.a -lm

And on a NeXT:

cc -c io.c -O -DNEXT
cc -c audio.c -O -DNEXT
cc -c headers.c -O -DNEXT
cc -c lispcall.c -O -DNEXT
cc -c sound.c -O -DNEXT
cc sndplay.c -o sndplay -O -DNEXT audio.o io.o headers.o lispcall.o sound.o

ld -r audio.o io.o headers.o lispcall.o sound.o -o sndlib.a
cc sndplay.c -o sndplay -O -DNEXT sndlib.a

Some similar sequence should work on a Sun (-DSOLARIS) or in HP-UX (-DHPUX).
On a Mac or in Windoze, you need to make a project in CodeWarrior or Watcom
or whatever that includes all the basic sndlib .c and .h files (io.c,
audio.c headers.c, lispcall.c, sound.c, sndlib.h) as source files. Add the
main program you're interested in (say sndplay.c), and "Make" the project.
On a Mac, when the project is "Run", a dialog pops up asking for the
arguments to the program (in this case the name of the file to be played, as
a quoted string). In Windoze, you can run the program from a DOS shell. On a
Be, you can either build a project or use a makefile. The C compiler's name
is mwcc. The tricky part here is that you have to find and include
explicitly the Be audio library, libmedia.so -- look first in
beos/system/lib. The Snd package includes make information for the sndlib
examples as well, so if you have Snd, you can say:

make sndplay

and so on.

  ------------------------------------------------------------------------

Current Status

       System       SndSine  SndInfo Audinfo     SndPlay    SndRecord
 NeXT 68k          ok       ok       ok       ok           ok
 NeXT Intel        ok       ok       ok       interruptionsruns (*)
 SGI old and new ALok       ok       ok       ok           ok
 OSS (Linux et al) ok       ok       ok       ok           ok
 Be                ok       ok       ok       ok           ok
 Mac               ok       ok       ok       ok           ok
 Windoze           ok       ok       ok       ok           not written
 Sun               broken   ok       broken   ok           bus error
 HPUX              untested untested untested untested     untested
 MkLinux           ok       ok       unready  unready      unready

(*) I can't find a microphone.

headers supported read/write
     NeXT/Sun/DEC/AFsp
     AIFF/AIFC
     RIFF (Microsoft wave)
     IRCAM (old style)
     no header
headers supported read-only
     8SVX (IFF), IRCAM Vax float, EBICSF, INRS, ESPS,
     SPPACK, ADC (OGI), NIST-SPHERE, AVR, VOC,
     Sound Tools, Turtle Beach SMP, SoundFont 2.0,
     Sound Designer I and II, PSION, MAUD, Kurzweil 2000,
     Tandy DeskMate, Gravis Ultrasound, ASF,
     Comdisco SPW, Goldwave sample, omf, quicktime
     Sonic Foundry, SBStudio II, Delusion digital,
     Digiplayer ST3, Farandole Composer WaveSample,
     Ultratracker WaveSample, Sample Dump exchange,
     Yamaha SY85, SY99, and TX16, Covox v8, SPL, AVI,

Incomplete: OMF, AVI, ASF, QuickTime, SoundFont 2.0.
Not handled: Esignal, ILS, HTK, DVSM, SoundEdit.
Handled by Snd: Mus10, IEEE text, HCOM, various compression schemes.
