#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#include <sys/ioctl.h>
#include <fcntl.h>
#include <sys/soundcard.h>

void Usage(FILE *output, const char *progname)
{
  fprintf(output,
          "Usage:\n\n%s [sounddevice]\n\n",
          progname);

  fprintf(output,
          "If sounddevice is not specified,\n"
          "then /dev/dsp is used by default.\n"
         );
} 
 
/* all helper functions should be inserted at this point */
#include <sys/stat.h> 

int open_device(FILE *errorlog, const char *device_name, int open_flags)
{ 
  struct stat device_info; 
  int audio_fd; 
 
  if (stat(device_name,&device_info) == -1) /* file does not exist */ 
    { 
      fprintf(errorlog, 
              "ERROR: device <%s> could not be opened: it does not exist.\n",
              device_name); 
      return -1;
    } 
 
  if ((device_info.st_mode & S_IFCHR) != S_IFCHR) /* is this a device ? */ 
    {
      fprintf(errorlog, 
              "ERROR: filename <%s> is not a devicefile.\n", 
              device_name); 
 
      return -1; 
    } 
     
  audio_fd = open(device_name, open_flags, 0);

  if (audio_fd == -1) // opening the device failed
    { 
      int reason_for_failure = errno;
      fprintf(errorlog, 
              "ERROR: device <%s> could not be opened with flags %d.\n",
              device_name, open_flags);

      fprintf(errorlog, 
              "       The reason given was:  <%s>.\n",
              strerror(reason_for_failure)); 
    }

  return audio_fd;
}

int decode_version(FILE *output, int version)
{
  int major_version = (version >> 16) & 0xff;
  int minor_version = (version >>  8) & 0xff;
  int revision      = (version      ) & 0xff;

  fprintf(output,
          "The OSS Version of this device-driver is %d.%d.%d\n",
          major_version,minor_version,revision);

  return (major_version << 16) | 
         (minor_version <<  8) |
          revision; 
}

int report_version(FILE *output, const char *devicename, int device_fd)
{
  int version = 0;

  if (ioctl(device_fd, OSS_GETVERSION, &version) == -1)
    {
      /* the device does not support this call */ 
 
      int reason_why_GET_VERSION_failed = errno;

      fprintf(output, 
              "The device <%s> reported the error %d (%s),\n"
              "while asking for its OSS driver version.\n",
              devicename,
              reason_why_GET_VERSION_failed,
              strerror(reason_why_GET_VERSION_failed));
      
      if (reason_why_GET_VERSION_failed == EINVAL) /* might be an old driver */
        return 0x0300000; /* make up a version number, reasonably recent */

      return 0;
    }
  else /* we should now be able to decode the version supported: */
    {
      return decode_version(output, version);
    }
}

typedef struct 
{
  int         scBitmask; /* the bitmask that corresponds to this capability */
  const char *scName;  /* a human readable representation for this capability */
} sound_capability;
  

void list_capabilities(FILE *output, int capability_mask)
{
static const sound_capability capability_list[] = 
        {
          { DSP_CAP_REVISION, "revision" },
          { DSP_CAP_DUPLEX,  "full duplex" },
          { DSP_CAP_REALTIME, "real-time processing" },
          { DSP_CAP_BATCH, "batching" },
          { DSP_CAP_COPROC, "co-processing" },
          { DSP_CAP_TRIGGER, "trigger" },
          { DSP_CAP_MMAP, "mmap()" },
          { 0, 0 }
        };
  
  int i;

  for (i = 0 ; capability_list[i].scBitmask != 0; ++i)
     {
       if (capability_mask & capability_list[i].scBitmask)
         {
           fprintf(output,
                   "The device has the %s capability (%x -> %x).\n",
                   capability_list[i].scName, 
                   capability_list[i].scBitmask,
                   capability_mask & capability_list[i].scBitmask
                  );
         }
     }
}

int report_capabilities(FILE *output, const char *devicename, int device_fd)
{
  int capability_mask = 0;

  if (ioctl(device_fd, SNDCTL_DSP_GETCAPS, &capability_mask) == -1)
    {
      /* the device does not support this call */ 
 
      int reason_why_DSP_GETCAPS_failed = errno;

      fprintf(output, 
              "The device <%s> reported the error %d (%s),\n"
              "while asking for its supported capabilities.\n",
              devicename,
              reason_why_DSP_GETCAPS_failed, 
              strerror(reason_why_DSP_GETCAPS_failed));

      return 0;
    }
  else /* we should now be able to list the capabilities supported: */
    {
      fprintf(output,
              "The device <%s> supports the following capabilities: (%d %x)\n",
              devicename, capability_mask, capability_mask);

      list_capabilities(output, capability_mask);
    }

  return capability_mask; 
}

typedef struct 
{
  int         sfBitmask; /* the bitmask that corresponds with this format */
  const char *sfName;  /* a human readable representation for this format */
} sound_format;

void list_formats(FILE *output, int format_mask)
{
  static sound_format format_list[] = 
        {
          { AFMT_MU_LAW, "mu-law" },
          { AFMT_A_LAW,  "a-law" },
          { AFMT_IMA_ADPCM, "IMA ADPCM" },
          { AFMT_U8, "8 bit unsigned" },
          { AFMT_S16_LE, "signed 16 bit, little endian" },
          { AFMT_S16_BE, "signed 16 bit, big endian" },
          { AFMT_S8, "8 bit signed" },
          { AFMT_U16_LE, "unsigned 16 bit, little endian" },
          { AFMT_U16_BE, "unsigned 16 bit, big endian" },
          { AFMT_MPEG, "MPEG audio" },
          { 0, 0 }
        };

  int i;

  for (i = 0 ; format_list[i].sfBitmask != 0; ++i)
     {
       if (format_mask & format_list[i].sfBitmask)
         {
           fprintf(output,
                   "The device supports %s audio.\n",
                   format_list[i].sfName);
         }
     }
}

int report_formats(FILE *output, const char *devicename, int device_fd)
{
  int format_mask = 0;

  if (ioctl(device_fd, SNDCTL_DSP_GETFMTS, &format_mask) == -1)
    {
      /* the device does not support this call */ 
 
      int reason_why_DSP_GETFMTS_failed = errno;

      fprintf(output, 
              "The device <%s> reported the error %d (%s),\n"
              "while asking for its supported formats.\n",
              devicename,
              reason_why_DSP_GETFMTS_failed, 
              strerror(reason_why_DSP_GETFMTS_failed));

      return 0;
    }
  else /* we should now be able to list the formats supported: */
    {
      fprintf(output,
              "The device <%s> supports the following formats: (%d %x)\n",
              devicename, format_mask, format_mask);

      list_formats(output, format_mask);
    }

  return format_mask;
}

int set_play_format(int device_fd, int play_format)
{
  int new_play_format = play_format;

  if (ioctl(device_fd, SNDCTL_DSP_SETFMT, &new_play_format) == -1)
    {
      return -1; /* format (or ioctl()) not supported by device */
    }

  return new_play_format; /* could be different from the one asked for! */
}

int set_channels(int device_fd, int channels)
{
  int new_channels = channels;

  if (ioctl(device_fd, SNDCTL_DSP_CHANNELS, &new_channels) == -1)
    {
      if (channels <= 2) /* maybe the previous call failed because of an old driver */
        { 
          /* we are now going to use the old call, and need to adjust our parameter  
           * accordingly. 
           */ 

          new_channels = channels - 1; 

          if (ioctl(device_fd, SNDCTL_DSP_STEREO, &new_channels) == -1)
            return -1;

          return new_channels + 1;  
          /* return type will be 0 or 1,  should be consistent with the 'new' driver case  
           */
        }
      else
        return -1; /* this number of channels (>2) is not supported by an old driver */
               
    }

  return new_channels; /* this will be the number of channels actually available. */
}

int set_sample_frequency(int device_fd, int sample_frequency)
{
  int new_sample_frequency = sample_frequency;

  if (ioctl(device_fd, SNDCTL_DSP_SPEED, &new_sample_frequency) == -1)
    {
      return -1; /* format (or ioctl()) not supported by device */
    }

  return new_sample_frequency; 
     /* could be different from the one asked for! */
}

int bytes_needed(int play_format,
                 int channels,
                 int sample_frequency,
                 int seconds)
{
  int bytes_per_channel = 0;

  switch (play_format)
        {
          case AFMT_MU_LAW:
          case AFMT_A_LAW:
          case AFMT_IMA_ADPCM:
          case AFMT_S16_LE:
          case AFMT_S16_BE:
          case AFMT_U16_LE:
          case AFMT_U16_BE:
          case AFMT_MPEG:
              return 0; /* format not supported by this program */

          case AFMT_U8:
          case AFMT_S8:
              bytes_per_channel = 1;
              break;
       }
  return seconds * sample_frequency * bytes_per_channel * channels;
}

int setup_buffer(unsigned char **buffer, size_t *buffersize,  
                 int play_format, 
                 int channels, 
                 int sample_frequency)
{
  unsigned char *walker = 0 ; /* used to walk through the buffer 
                               * we will allocate 
                               */
  int seconds           = 2;

  int needed = bytes_needed(play_format, 
                            channels, 
                            sample_frequency, 
                            seconds);
  int i;

  if ((needed == 0) || (!buffer) || (!buffersize))
    return 0;

  walker = malloc(needed);

  if (!walker)
    {
      *buffersize = 0;
      return 0;
    }

  *buffer     = walker; /* save the beginning of the buffer */
  *buffersize = needed;

  needed /= channels;

  for (i = 0 ; i < needed ; ++i)
     {
       int j;

       for (j = 0 ; j < channels; ++j)
          /* the following formula generates a sawtooth waveform */
          *walker++ = -1 * (j+1) * i & 0xff;
     }
  
  return 1;
}

int wait_till_finished(int device_fd)
{
  if (ioctl(device_fd, SNDCTL_DSP_SYNC, 0) == -1)
    {
      return -1; /* format (or ioctl()) not supported by device */
    }

  return 1; /* all sound has been played */
}

int reset_device(int device_fd)
{
  if (ioctl(device_fd, SNDCTL_DSP_RESET, 0) == -1)
    {
      return -1; /* format (or ioctl()) not supported by device */
    }

  return 1; /* sounddevice has been reset */
}

int set_play_params(int device_fd, 
                    int play_format, 
                    int channels, 
                    int sample_frequency)
{
  /* make sure we get back what we ask for: */

  return (set_play_format(device_fd, play_format) == play_format) &&
         (set_channels(device_fd, channels) == channels) &&
  (set_sample_frequency(device_fd, sample_frequency) == sample_frequency);
}

int main(int argc, const char *argv[])
{
  const char *device    = "/dev/dsp"; /* this is the standard name of the
                                       * sound device.
                                       */
                                      
  int         device_fd = -1;         /* we have not opened anything yet */
  
  int         version         = 0;    /* the version of the OSS driver    */
  int         capability_mask = 0;    /* what the device is capable of.   */
  int         format_mask     = 0;    /* what formats the device can play.*/

  int         channels         = 1;
  int         play_format      = AFMT_U8;
  int         sample_frequency = 48000; /* change this to 44100 or 22050 
                                         * if this high a frequency is not 
                                         * supported by your soundcard. 
                                         */

  unsigned char *buffer        = 0;
  size_t         buffersize    = 0;

  switch (argc)
        {
          case 2: device = argv[1];   /* device name was specified. */ 
                  if (strcmp(argv[1],"-?") == 0)
                    {
                      Usage(stdout,argv[0]); /* provide help */
                      exit(EXIT_SUCCESS); 
                    }
                  break;
          case 1: break;           /* no arguments, use standard device. */

          default: /* too many arguments, show help: */
              Usage(stderr,argv[0]);
              exit(EXIT_FAILURE);
        } 
 
  /* note: for playing we need to write to this device; for reading (recording) 
   * we would specify O_RDONLY. Writing is dangerous, so the open_device() call 
   * below should check whether the filename passed actually points to a device. 
   */
  
  device_fd = open_device(stderr, device, O_WRONLY); 

  if (device_fd == -1) /* if this is true,the open call failed and an
                        * error will have been given.
                        */
   {
     exit(1);
   }

  /* we have succesfully opened our device. now report what it can do: */

  version         = report_version(stdout, device, device_fd);
  capability_mask = report_capabilities(stdout,device,device_fd);
  format_mask     = report_formats(stdout,device,device_fd);

  if ((version == 0) ||
      (capability_mask == 0) ||
      (format_mask == 0))
    {
      /* remember that we were going to -write- to the device.
       * If this device happened to be a harddisk which got passed to
       * this program by mistake (or maybe the devicefile itself got
       * messed up by accident) we would be destroying the filesystem
       * and lose important data. We therefore try to dwell on the side
       * of caution and exit the program.
       */

      fprintf(stderr,
   "This device did not respond to one of the required audio functions.\n"
   "It may not be an audio output-device. Exiting, just in case.\n"
             );
      exit(1);
    } 
 
/* playing code will go here */

  /* ok, we are ready to play something now */

  if (!(format_mask & play_format))
    {
      fprintf(stderr,
              "This device is not capable of playing the desired format.\n");

      exit(1);
    }

  if (!set_play_params(device_fd, play_format, channels, sample_frequency))
    {
      fprintf(stderr,
              "This device is not capable of playing the combination of:\n"
              "Format   : %d\n"
              "Channels : %d\n"
              "Frequency: %d\n",
              play_format, channels, sample_frequency); 
 
      /* usually when this happens, the sample frequency is not supported;
       * try changing it to a lower value.  
       */ 

      exit(1);
    }

   fprintf(stdout,
           "This device is now set up to play the combination of:\n"
              "Format   : %d\n"
              "Channels : %d\n"
              "Frequency: %d\n",
              play_format, channels, sample_frequency);

   /* initialize a buffer with the desired playing format */

   if (setup_buffer(&buffer, &buffersize,  
                    play_format, channels, sample_frequency))
     { 
       /* code to play buffer goes here */       
 
       size_t bytes_written =  0;

       fprintf(stdout, 
               "Now writing %d bytes to the device.\n",
               buffersize);

       bytes_written =  write(device_fd, buffer, buffersize);

       fprintf(stdout,
               "Managed to write %d bytes to the device.\n",
               bytes_written);

       reset_device(device_fd); 

       free(buffer);
     }

   close(device_fd); 

  return 0;
}
