/* libSoX effect: gain/norm/etc.   (c) 2008-9 robs@users.sourceforge.net
 *
 * This library is free software; you can redistribute it and/or modify it
 * 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.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser
 * General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#define LSX_EFF_ALIAS
#include "sox_i.h"
#include <ctype.h>

typedef struct {
  sox_bool      do_equalize, do_balance, do_balance_no_clip, do_limiter;
  sox_bool      do_restore, make_headroom, do_normalize, do_scan;
  double        fixed_gain; /* Valid only in channel 0 */

  double        mult, reclaim, rms, limiter;
  off_t         num_samples;
  sox_sample_t  min, max;
  FILE          * tmp_file;
} priv_t;

static int create_gain(sox_effect_t * effp, int argc, char * * argv)
{
  priv_t * p = (priv_t *)effp->priv;
  char const * q;
  for (--argc, ++argv; argc && **argv == '-' && argv[0][1] &&
      !isdigit((int)argv[0][1]) && argv[0][1] != '.'; --argc, ++argv)
    for (q = &argv[0][1]; *q; ++q) switch (*q) {
      case 'n': p->do_scan = p->do_normalize = sox_true; break;
      case 'e': p->do_scan = p->do_equalize = sox_true; break;
      case 'B': p->do_scan = p->do_balance = sox_true; break;
      case 'b': p->do_scan = p->do_balance_no_clip = sox_true; break;
      case 'r': p->do_scan = p->do_restore = sox_true; break;
      case 'h': p->make_headroom = sox_true; break;
      case 'l': p->do_limiter = sox_true; break;
      default: lsx_fail("invalid option `-%c'", *q); return lsx_usage(effp);
    }
  if ((p->do_equalize + p->do_balance + p->do_balance_no_clip + p->do_restore)/ sox_true > 1) {
    lsx_fail("only one of -e, -B, -b and -r may be given");
    return SOX_EOF;
  }
  if (p->do_normalize && p->do_restore) {
    lsx_fail("only one of -n and -r may be given");
    return SOX_EOF;
  }
  if (p->do_limiter && p->make_headroom) {
    lsx_fail("only one of -l and -h may be given");
    return SOX_EOF;
  }
  do {NUMERIC_PARAMETER(fixed_gain, -HUGE_VAL, HUGE_VAL)} while (0);
  p->fixed_gain = dB_to_linear(p->fixed_gain);
  return argc? lsx_usage(effp) : SOX_SUCCESS;
}

static int start_gain(sox_effect_t * effp)
{
  priv_t * p = (priv_t *)effp->priv;

  if (effp->flow == 0) {
    if (p->do_restore) {
      if (!effp->in_signal.mult || *effp->in_signal.mult >= 1) {
        lsx_fail("can't reclaim headroom");
        return SOX_EOF;
      }
      p->reclaim = 1 / *effp->in_signal.mult;
    }
    effp->out_signal.mult = p->make_headroom? &p->fixed_gain : NULL;
    if (!p->do_equalize && !p->do_balance && !p->do_balance_no_clip)
      effp->flows = 1; /* essentially a conditional SOX_EFF_MCHAN */
  }
  p->mult = 0;
  p->max = 1;
  p->min = -1;
  if (p->do_scan) {
    p->tmp_file = lsx_tmpfile();
    if (p->tmp_file == NULL) {
      lsx_fail("can't create temporary file: %s", strerror(errno));
      return SOX_EOF;
    }
  }
  if (p->do_limiter)
    p->limiter = (1 - 1 / p->fixed_gain) * (1. / SOX_SAMPLE_MAX);
  else if (p->fixed_gain == floor(p->fixed_gain) && !p->do_scan)
    effp->out_signal.precision = effp->in_signal.precision;
  return SOX_SUCCESS;
}

static int flow_gain(sox_effect_t * effp, const sox_sample_t * ibuf,
    sox_sample_t * obuf, size_t * isamp, size_t * osamp)
{
  priv_t * p = (priv_t *)effp->priv;
  size_t len;

  if (p->do_scan) {
    if (fwrite(ibuf, sizeof(*ibuf), *isamp, p->tmp_file) != *isamp) {
      lsx_fail("error writing temporary file: %s", strerror(errno));
      return SOX_EOF;
    }
    if (p->do_balance && !p->do_normalize)
      for (len = *isamp; len; --len, ++ibuf) {
        double d = SOX_SAMPLE_TO_FLOAT_64BIT(*ibuf, effp->clips);
        p->rms += sqr(d);
        ++p->num_samples;
      }
    else if (p->do_balance || p->do_balance_no_clip)
      for (len = *isamp; len; --len, ++ibuf) {
        double d = SOX_SAMPLE_TO_FLOAT_64BIT(*ibuf, effp->clips);
        p->rms += sqr(d);
        ++p->num_samples;
        p->max = max(p->max, *ibuf);
        p->min = min(p->min, *ibuf);
      }
    else for (len = *isamp; len; --len, ++ibuf) {
      p->max = max(p->max, *ibuf);
      p->min = min(p->min, *ibuf);
    }
    *osamp = 0; /* samples not output until drain */
  }
  else {
    double mult = ((priv_t *)(effp - effp->flow)->priv)->fixed_gain;
    len = *isamp = *osamp = min(*isamp, *osamp);
    if (!p->do_limiter) for (; len; --len, ++ibuf)
      *obuf++ = SOX_ROUND_CLIP_COUNT(*ibuf * mult, effp->clips);
    else for (; len; --len, ++ibuf) {
      double d = *ibuf * mult;
      *obuf++ = d < 0 ? 1 / (1 / d - p->limiter) - .5 :
                d > 0 ? 1 / (1 / d + p->limiter) + .5 : 0;
    }
  }
  return SOX_SUCCESS;
}

static void start_drain(sox_effect_t * effp)
{
  priv_t * p = (priv_t *)effp->priv;
  double max = SOX_SAMPLE_MAX, max_peak = 0, max_rms = 0;
  size_t i;

  if (p->do_balance || p->do_balance_no_clip) {
    for (i = 0; i < effp->flows; ++i) {
      priv_t * q = (priv_t *)(effp - effp->flow + i)->priv;
      max_rms = max(max_rms, sqrt(q->rms / q->num_samples));
      rewind(q->tmp_file);
    }
    for (i = 0; i < effp->flows; ++i) {
      priv_t * q = (priv_t *)(effp - effp->flow + i)->priv;
      double this_rms = sqrt(q->rms / q->num_samples);
      double this_peak = max(q->max / max, q->min / (double)SOX_SAMPLE_MIN);
      q->mult = this_rms != 0? max_rms / this_rms : 1;
      max_peak = max(max_peak, q->mult * this_peak);
      q->mult *= p->fixed_gain;
    }
    if (p->do_normalize || (p->do_balance_no_clip && max_peak > 1))
      for (i = 0; i < effp->flows; ++i) {
        priv_t * q = (priv_t *)(effp - effp->flow + i)->priv;
        q->mult /= max_peak;
      }
  } else if (p->do_equalize && !p->do_normalize) {
    for (i = 0; i < effp->flows; ++i) {
      priv_t * q = (priv_t *)(effp - effp->flow + i)->priv;
      double this_peak = max(q->max / max, q->min / (double)SOX_SAMPLE_MIN);
      max_peak = max(max_peak, this_peak);
      q->mult = p->fixed_gain / this_peak;
      rewind(q->tmp_file);
    }
    for (i = 0; i < effp->flows; ++i) {
      priv_t * q = (priv_t *)(effp - effp->flow + i)->priv;
      q->mult *= max_peak;
    }
  } else {
    p->mult = min(max / p->max, (double)SOX_SAMPLE_MIN / p->min);
    if (p->do_restore) {
      if (p->reclaim > p->mult)
        lsx_report("%.3gdB not reclaimed", linear_to_dB(p->reclaim / p->mult));
      else p->mult = p->reclaim;
    }
    p->mult *= p->fixed_gain;
    rewind(p->tmp_file);
  }
}

static int drain_gain(sox_effect_t * effp, sox_sample_t * obuf, size_t * osamp)
{
  priv_t * p = (priv_t *)effp->priv;
  size_t len;
  int result = SOX_SUCCESS;

  *osamp -= *osamp % effp->in_signal.channels;

  if (p->do_scan) {
    if (!p->mult)
      start_drain(effp);
    len = fread(obuf, sizeof(*obuf), *osamp, p->tmp_file);
    if (len != *osamp && !feof(p->tmp_file)) {
      lsx_fail("error reading temporary file: %s", strerror(errno));
      result = SOX_EOF;
    }
    if (!p->do_limiter) for (*osamp = len; len; --len, ++obuf)
      *obuf = SOX_ROUND_CLIP_COUNT(*obuf * p->mult, effp->clips);
    else for (*osamp = len; len; --len) {
      double d = *obuf * p->mult;
      *obuf++ = d < 0 ? 1 / (1 / d - p->limiter) - .5 :
                d > 0 ? 1 / (1 / d + p->limiter) + .5 : 0;
    }
  }
  else *osamp = 0;
  return result;
}

static int stop_gain(sox_effect_t * effp)
{
  priv_t * p = (priv_t *)effp->priv;
  if (p->do_scan)
    lsx_close_tmpfile(p->tmp_file);
  return SOX_SUCCESS;
}

sox_effect_handler_t const * lsx_gain_effect_fn(void)
{
  static const char usage[] =
    "[-e|-b|-B|-r] [-n] [-l|-h] [gain]";

  static char const * const extra_usage[] = {
"-e   Equalize channels: peak to that with max peak",
"-B   Balance channels' RMS to that with max RMS; no clip protection",
"-b   Balance channels' RMS to that with max RMS; clip protection",
"-r   Reclaim headroom (as much as possible without clipping); see -h",
"-n   Normalize file to 0dBfs output precision; gain, if present, usually < 0",
"     Note: -Bn is the same as -bn",
"-l   Use a simple limiter",
"-h   Apply attenuation for headroom for subsequent effects;",
"     gain, if present, is subject to reclaim by a subsequent gain -r",
"gain Apply gain in dB",
    NULL
  };
  static sox_effect_handler_t handler = {
    "gain", usage, SOX_EFF_GAIN,
    create_gain, start_gain, flow_gain, drain_gain, stop_gain, NULL,
    sizeof(priv_t),
    extra_usage,
  };

  return &handler;
}

/*------------------ emulation of the old `normalize' effect -----------------*/

static int norm_getopts(sox_effect_t * effp, int argc, char * * argv)
{
  char * argv2[3];
  int argc2 = 2;

  argv2[0] = argv[0], --argc, ++argv;
  argv2[1] = "-n";
  if (argc)
    argv2[argc2++] = *argv, --argc, ++argv;
  return argc? lsx_usage(effp) :
    lsx_gain_effect_fn()->getopts(effp, argc2, argv2);
}

sox_effect_handler_t const * lsx_norm_effect_fn(void)
{
  static sox_effect_handler_t handler;
  static char const * const extra_usage[] = {
    "level is in dB with respect to 0dB, usually negative",
    NULL
  };
  handler = *lsx_gain_effect_fn();
  handler.name = "norm";
  handler.usage = "[level(0)]";
  handler.extra_usage = extra_usage;
  handler.getopts = norm_getopts;
  return &handler;
}
