/*
 * 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 "prefs.h"
#include "log.h"
#include "pcm.h"
#include "notes.h"

char *opt_pcmDevice;

struct NoteDeviceStruct {
  PcmDevice *pcm;

  int blockSize;
  int sampleRate;
  int channelCount;
  PcmAmplitudeFormat amplitudeFormat;

  unsigned char *blockAddress;
  int blockUsed;

  PcmSampleMaker makeSample;
};

static int
pcmFlushBytes (NoteDevice *device) {
  int ok = writePcmData(device->pcm, device->blockAddress, device->blockUsed);
  if (ok) device->blockUsed = 0;
  return ok;
}

static int
pcmWriteSample (NoteDevice *device, int16_t amplitude) {
  PcmSample *sample = (PcmSample *)&device->blockAddress[device->blockUsed];
  PcmSampleSize size = device->makeSample(sample, amplitude);
  device->blockUsed += size;

  for (int channel=1; channel<device->channelCount; channel+=1) {
    for (int byte=0; byte<size; byte+=1) {
      device->blockAddress[device->blockUsed++] = sample->bytes[byte];
    }
  }

  if (device->blockUsed == device->blockSize) {
    if (!pcmFlushBytes(device)) {
      return 0;
    }
  }

  return 1;
}

static int
pcmFlushBlock (NoteDevice *device) {
  while (device->blockUsed)
    if (!pcmWriteSample(device, 0))
      return 0;

  return 1;
}

static NoteDevice *
pcmConstruct (int errorLevel) {
  NoteDevice *device;

  if ((device = malloc(sizeof(*device)))) {
    memset(device, 0, sizeof(*device));

    if ((device->pcm = openPcmDevice(errorLevel, opt_pcmDevice))) {
      device->blockSize = getPcmBlockSize(device->pcm);
      device->sampleRate = getPcmSampleRate(device->pcm);
      device->channelCount = getPcmChannelCount(device->pcm);
      device->amplitudeFormat = getPcmAmplitudeFormat(device->pcm);

      device->blockUsed = 0;
      device->makeSample = getPcmSampleMaker(device->amplitudeFormat);

      PcmSample sample;
      PcmSampleSize sampleSize = device->makeSample(&sample, 0);
      sampleSize *= device->channelCount;

      if (sampleSize && device->blockSize &&
          !(device->blockSize % sampleSize)) {
        if ((device->blockAddress = malloc(device->blockSize))) {
          logMessage(LOG_DEBUG, "PCM enabled: BlkSz:%d Rate:%d ChnCt:%d Fmt:%d",
                     device->blockSize, device->sampleRate, device->channelCount, device->amplitudeFormat);
          return device;
        } else {
          logMallocError();
        }
      } else {
        logMessage(LOG_ERR,
                   "PCM block size not multiple of sample size:"
                   " BlkSz:%d" " SmpSz:%u",
                   device->blockSize, sampleSize);
      }

      closePcmDevice(device->pcm);
    }

    free(device);
  } else {
    logMallocError();
  }

  logMessage(LOG_DEBUG, "PCM not available");
  return NULL;
}

static void
pcmDestruct (NoteDevice *device) {
  pcmFlushBlock(device);
  free(device->blockAddress);
  closePcmDevice(device->pcm);
  free(device);
  logMessage(LOG_DEBUG, "PCM disabled");
}

static int
pcmTone (NoteDevice *device, unsigned int duration, NoteFrequency frequency) {
  int32_t sampleCount = device->sampleRate * duration / 1000;

  logMessage(LOG_DEBUG, "tone: MSecs:%u SmpCt:%"PRId32 " Freq:%"PRIfreq,
             duration, sampleCount, frequency);

  if (frequency) {
    /* A triangle waveform sounds nice, is lightweight, and avoids
     * relying too much on floating-point performance and/or on
     * expensive math functions like sin(). Considerations like
     * these are especially important on PDAs without any FPU.
     */ 

    /* We need to know the maximum amplitude based on the currently set
     * volume percentage. This percentage then needs to be squared because
     * we perceive loudness exponentially.
     */
    const unsigned char fullVolume = 100;
    const unsigned char currentVolume = MIN(fullVolume, prefs.pcmVolume);
    const int32_t maximumAmplitude = INT16_MAX
                                   * (currentVolume * currentVolume)
                                   / (fullVolume * fullVolume);

    /* The calculations for triangle wave generation work out nicely and
     * efficiently if we map a full period onto a 32-bit unsigned range.
     */

    /* The two high-order bits specify which quarter wave a sample is for.
     *   00 -> ascending from the negative peak to zero
     *   01 -> ascending from zero to the positive peak
     *   10 -> descending from the positive peak to zero
     *   11 -> descending from zero to the negative peak
     * The higher bit is 0 for the ascending segment and 1 for the
     * descending segment. The lower bit is 0 when going from a peak to
     * zero and 1 when going from zero to a peak.
     */
    const uint8_t magnitudeWidth = 32 - 2;

    /* The amplitude is 0 when the lower bit of the quarter wave indicator
     * is 1 and the rest of the (magnitude) bits are all 0.
     */
    const uint32_t zeroValue = UINT32_C(1) << magnitudeWidth;

    /* We need to know how many steps to make from one sample to the next.
     * stepsPerSample = stepsPerWave * wavesPerSecond / samplesPerSecond
     *                = stepsPerWave * frequency / sampleRate
     *                = stepsPerWave / sampleRate * frequency
     */
    const uint32_t stepsPerSample = (NoteFrequency)UINT32_MAX 
                                  / (NoteFrequency)device->sampleRate
                                  * frequency;

    /* The current value needs to be a signed value so that the >> operator
     * will extend its sign bit. We start by initializing it to the value
     * that corresponds to the start of the first logical quarter wave
     * (the one that ascends from zero to the positive peak).
     */
    int32_t currentValue = zeroValue;

    /* Round the number of samples up to a whole number of periods:
     * partialSteps = (sampleCount * stepsPerSample) % stepsPerWave
     *
     * With stepsPerWave being (1 << 32), we simply let the product
     * overflow. The modulus corresponds to the remaining 32 low bits:
     * partialSteps = (uint32_t)(sampleCount * stepsPerSample)
     *
     * missingSteps = stepsPerWave - partialSteps
     *              = (uint32_t) -partialSteps

     * extraSamples = missingSteps / stepsPerSample
     */
    sampleCount += (uint32_t)(sampleCount * -stepsPerSample) / stepsPerSample;

    while (sampleCount > 0) {
      /* Convert the current 32-bit unsigned linear value to a 31-bit
       * triangular amplitude by inverting its low-order 31 bits if its
       * high-order (sign) bit is set.
       */
      int32_t amplitude = currentValue ^ (currentValue >> 31);

      /* Convert the 31-bit amplitude from unsigned to signed. */
      amplitude -= zeroValue;

      /* Convert the amplitude's magnitude from 30 bits to 16 bits. */
      amplitude >>= magnitudeWidth - 16;

      /* Adjust the 17-bit signed amplitude (sign bit + 16-bit value) by
       * the currently set volume (15-bit value):
       * (16-bit value) * (15-bit value) + (sign bit) = 32-bit signed value
       */
      amplitude *= maximumAmplitude;

      /* Convert the signed amplitude from 32 bits to 16 bits. */
      amplitude >>= 16;

      if (!pcmWriteSample(device, amplitude)) break;
      currentValue += stepsPerSample;
      sampleCount -= 1;
    }
  } else {
    /* generate silence */
    while (sampleCount > 0) {
      if (!pcmWriteSample(device, 0)) break;
      sampleCount -= 1;
    }
  }

  return (sampleCount > 0) ? 0 : 1;
}

static int
pcmNote (NoteDevice *device, unsigned int duration, unsigned char note) {
  return pcmTone(device, duration, getNoteFrequency(note));
}

static int
pcmFlush (NoteDevice *device) {
  int ok = pcmFlushBlock(device);
  if (ok) pushPcmOutput(device->pcm);
  return ok;
}

const NoteMethods pcmNoteMethods = {
  .construct = pcmConstruct,
  .destruct = pcmDestruct,

  .tone = pcmTone,
  .note = pcmNote,
  .flush = pcmFlush
};
