/*
 * BRLTTY - A background process providing access to the console screen (when in
 *          text mode) for a blind person using a refreshable braille display.
 *
 * Copyright (C) 1995-2024 by The BRLTTY Developers.
 *
 * BRLTTY comes with ABSOLUTELY NO WARRANTY.
 *
 * This is free software, placed under the terms of the
 * GNU Lesser General Public License, as published by the Free Software
 * Foundation; either version 2.1 of the License, or (at your option) any
 * later version. Please see the file LICENSE-LGPL for details.
 *
 * Web Page: http://brltty.app/
 *
 * This software is maintained by Dave Mielke <dave@mielke.cc>.
 */

#include "prologue.h"

#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/soundcard.h>

#include "log.h"
#include "io_misc.h"
#include "pcm.h"

#define PCM_OSS_DEVICE_PATH "/dev/dsp"

#ifndef SNDCTL_DSP_SPEED
#define SNDCTL_DSP_SPEED SOUND_PCM_WRITE_RATE
#endif /* SNDCTL_DSP_SPEED */

#ifndef SNDCTL_DSP_CHANNELS
#define SNDCTL_DSP_CHANNELS SOUND_PCM_WRITE_CHANNELS
#endif /* SNDCTL_DSP_CHANNELS */

struct PcmDeviceStruct {
  int fileDescriptor;
  int driverVersion;
  int sampleRate;
  int channelCount;
};

PcmDevice *
openPcmDevice (int errorLevel, const char *device) {
  PcmDevice *pcm;
  if ((pcm = malloc(sizeof(*pcm)))) {
    if (!*device) device = PCM_OSS_DEVICE_PATH;
    if ((pcm->fileDescriptor = open(device, O_WRONLY|O_NONBLOCK)) != -1) {
      /* Nonblocking if snd_seq_oss is loaded with nonblock_open=1.
       * There appears to be a bug in this case as write() always
       * returns the full count even though large chunks of sound are
       * missing. For now, therefore, force blocking output.
       */
      setBlockingIo(pcm->fileDescriptor, 1);

      pcm->driverVersion = 0X030000;
#ifdef OSS_GETVERSION
      if (ioctl(pcm->fileDescriptor, OSS_GETVERSION, &pcm->driverVersion) == -1)
        logMessage(errorLevel, "cannot get OSS driver version");
#endif /* OSS_GETVERSION */
      logMessage(LOG_DEBUG, "OPSS driver version: %06X", pcm->driverVersion);

      setPcmSampleRate(pcm, 8000);
      setPcmChannelCount(pcm, 1);
      return pcm;
    } else {
      logMessage(errorLevel, "cannot open PCM device: %s: %s", device, strerror(errno));
    }
    free(pcm);
  } else {
    logSystemError("PCM device allocation");
  }
  return NULL;
}

void
closePcmDevice (PcmDevice *pcm) {
  close(pcm->fileDescriptor);
  free(pcm);
}

int
writePcmData (PcmDevice *pcm, const unsigned char *buffer, int count) {
  return writeFile(pcm->fileDescriptor, buffer, count) != -1;
}

int
getPcmBlockSize (PcmDevice *pcm) {
  int fragmentCount = (1 << 0X10) - 1;
  int fragmentShift = 7;
  int fragmentSize = 1 << fragmentShift;
  int fragmentSetting = (fragmentCount << 0X10) | fragmentShift;
  ioctl(pcm->fileDescriptor, SNDCTL_DSP_SETFRAGMENT, &fragmentSetting);

  {
    int blockSize;
    if (ioctl(pcm->fileDescriptor, SNDCTL_DSP_GETBLKSIZE, &blockSize) != -1) return blockSize;
  }
  return fragmentSize;
}

int
getPcmSampleRate (PcmDevice *pcm) {
  return pcm->sampleRate;
}

int
setPcmSampleRate (PcmDevice *pcm, int rate) {
  if (ioctl(pcm->fileDescriptor, SNDCTL_DSP_SPEED, &rate) != -1) pcm->sampleRate = rate;
  return getPcmSampleRate(pcm);
}

int
getPcmChannelCount (PcmDevice *pcm) {
  return pcm->channelCount;
}

int
setPcmChannelCount (PcmDevice *pcm, int channels) {
  if (ioctl(pcm->fileDescriptor, SNDCTL_DSP_CHANNELS, &channels) != -1) pcm->channelCount = channels;
  return getPcmChannelCount(pcm);
}

typedef struct {
  PcmAmplitudeFormat internal;
  int external;
} AmplitudeFormatEntry;
static const AmplitudeFormatEntry amplitudeFormatTable[] = {
  {PCM_FMT_U8     , AFMT_U8    },
  {PCM_FMT_S8     , AFMT_S8    },
  {PCM_FMT_U16B   , AFMT_U16_BE},
  {PCM_FMT_S16B   , AFMT_S16_BE},
  {PCM_FMT_U16L   , AFMT_U16_LE},
  {PCM_FMT_S16L   , AFMT_S16_LE},
  {PCM_FMT_ULAW   , AFMT_MU_LAW},
  {PCM_FMT_ALAW   , AFMT_A_LAW},
  {PCM_FMT_UNKNOWN, AFMT_QUERY }
};

static PcmAmplitudeFormat
doPcmAmplitudeFormat (PcmDevice *pcm, int format) {
  if (ioctl(pcm->fileDescriptor, SNDCTL_DSP_SETFMT, &format) != -1) {
    const AmplitudeFormatEntry *entry = amplitudeFormatTable;
    while (entry->internal != PCM_FMT_UNKNOWN) {
      if (entry->external == format) return entry->internal;
      ++entry;
    }
  }
  return PCM_FMT_UNKNOWN;
}

PcmAmplitudeFormat
getPcmAmplitudeFormat (PcmDevice *pcm) {
  return doPcmAmplitudeFormat(pcm, AFMT_QUERY);
}

PcmAmplitudeFormat
setPcmAmplitudeFormat (PcmDevice *pcm, PcmAmplitudeFormat format) {
  const AmplitudeFormatEntry *entry = amplitudeFormatTable;
  while (entry->internal != PCM_FMT_UNKNOWN) {
    if (entry->internal == format) break;
    ++entry;
  }
  return doPcmAmplitudeFormat(pcm, entry->external);
}

void
pushPcmOutput (PcmDevice *pcm) {
  ioctl(pcm->fileDescriptor, SNDCTL_DSP_POST, 0);
}

void
awaitPcmOutput (PcmDevice *pcm) {
  ioctl(pcm->fileDescriptor, SNDCTL_DSP_SYNC, 0);
}

void
cancelPcmOutput (PcmDevice *pcm) {
  ioctl(pcm->fileDescriptor, SNDCTL_DSP_RESET, 0);
}
