/* libSoX effect: SpeexDsp effect to apply processing from libspeexdsp.
 *
 * Copyright 1999-2009 Chris Bagwell And SoX Contributors
 *
 * 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
 */

#include "sox_i.h"

#ifdef HAVE_SPEEXDSP

#include <speex/speex_types.h>
#include <speex/speex_preprocess.h>

#if !defined(HAVE_LIBLTDL)
#undef DL_SPEEXDSP
#endif

static const char* const speexdsp_library_names[] =
{
#ifdef DL_SPEEXDSP
  "libspeexdsp",
  "libspeexdsp-1",
  "cygspeexdsp-1",
#endif
  NULL
};

#ifdef DL_SPEEXDSP
  #define SPEEXDSP_FUNC      LSX_DLENTRY_DYNAMIC
  #define SPEEXDSP_FUNC_STOP LSX_DLENTRY_STUB
#else
  #define SPEEXDSP_FUNC      LSX_DLENTRY_STATIC
  #define SPEEXDSP_FUNC_STOP LSX_DLENTRY_STUB
#endif /* DL_SPEEXDSP/ */

#define SPEEXDSP_FUNC_ENTRIES(f,x) \
  SPEEXDSP_FUNC(f,x, SpeexPreprocessState*, speex_preprocess_state_init, \
                (int, int)) \
  SPEEXDSP_FUNC(f,x, int, speex_preprocess_ctl, \
                (SpeexPreprocessState*, int, void *)) \
  SPEEXDSP_FUNC(f,x, int, speex_preprocess_run, \
                (SpeexPreprocessState*, spx_int16_t *)) \
  SPEEXDSP_FUNC(f,x, void, speex_preprocess_state_destroy, \
                (SpeexPreprocessState*))

/* Private data for effect */
typedef struct speexdsp_priv_t {
    size_t buffer_end;        /* Index of the end of the buffer. */
    size_t buffer_ipos;       /* Index for the next input sample. */
    size_t buffer_opos;       /* Index of the next sample that has not been drained. */
    int16_t* buffer;          /* Work buffer. */
    SpeexPreprocessState* sps;/* DSP state. */
    size_t agc;               /* Param: Automatic Gain Control target volume level: 0 to disable, or 1-100 (target volume). */
    size_t denoise;           /* Param: Denoise: 0 to disable, or maximum noise attenuation in dB. */
    size_t dereverb;          /* Param: Dereverb: 0 to disable, 1 to enable. */
    size_t frames_per_second; /* Param: Used to compute buffer size from sample rate. */
    size_t samples_per_frame; /* Param: Used to compute buffer size directly. Default is to use frames_per_second instead. */
    LSX_DLENTRIES_TO_PTRS(SPEEXDSP_FUNC_ENTRIES, speexdsp_dl);
} priv_t;

static int get_param(
    int* pArgc,
    char*** pArgv,
    size_t* pParam,
    size_t default_val,
    size_t min_valid,
    size_t max_valid)
{
    *pParam = default_val;
    if (*pArgc > 1 && (*pArgv)[1][0] != '-')
    {
        char* arg_end;
        *pParam = strtoul((*pArgv)[1], &arg_end, 0);
        if (!arg_end || arg_end[0] || *pParam < min_valid || max_valid < *pParam)
            return 0;

        --*pArgc;
        ++*pArgv;
    }

    return 1;
}

/*
 * Process command-line options but don't do other
 * initialization now: effp->in_signal & effp->out_signal are not
 * yet filled in.
 */
static int getopts_speexdsp(sox_effect_t* effp, int argc, char** argv)
{
    priv_t* p = (priv_t*)effp->priv;
    const size_t agcDefault = 100;
    const size_t denoiseDefault = 15;
    const size_t fpsDefault = 50;

    for (argc--, argv++; argc; argc--, argv++)
    {
        if (!strcasecmp("-agc", argv[0]))
        {
            /* AGC level argument is optional. If not specified, it defaults to agcDefault.
               If specified, it must be from 0 to 100. */
            if (!get_param(&argc, &argv, &p->agc, agcDefault, 1, 100))
            {
                lsx_fail("invalid argument \"%s\" to -agc parameter - expected number from 1 to 100", argv[1]);
                return SOX_EOF;
            }
        }
        else if (!strcasecmp("-denoise", argv[0]))
        {
            /* Denoise level argument is optional. If not specified, it defaults to denoiseDefault.
               If specified, it must be from 1 to 100. */
            if (!get_param(&argc, &argv, &p->denoise, denoiseDefault, 1, 100))
            {
                lsx_fail("invalid argument \"%s\" to -denoise parameter - expected number from 1 to 100", argv[1]);
                return SOX_EOF;
            }
        }
        else if (!strcasecmp("-dereverb", argv[0]))
        {
            p->dereverb = 1;
        }
        else if (!strcasecmp("-spf", argv[0]))
        {
            /* If samples_per_frame option is given, argument is required and must be
               greater than 0. */
            if (!get_param(&argc, &argv, &p->samples_per_frame, 0, 1, 1000000000) || !p->samples_per_frame)
            {
                lsx_fail("invalid argument \"%s\" to -spf parameter - expected positive number", argv[1]);
                return SOX_EOF;
            }
        }
        else if (!strcasecmp("-fps", argv[0]))
        {
            /* If frames_per_second option is given, argument is required and must be
               from 1 to 100. This will be used later to compute samples_per_frame once
               we know the sample rate). */
            if (!get_param(&argc, &argv, &p->frames_per_second, 0, 1, 100) || !p->frames_per_second)
            {
                lsx_fail("invalid argument \"%s\" to -fps parameter - expected number from 1 to 100", argv[1]);
                return SOX_EOF;
            }
        }
        else
        {
            lsx_fail("invalid parameter \"%s\"", argv[0]);
            return SOX_EOF;
        }
    }

    if (!p->frames_per_second)
        p->frames_per_second = fpsDefault;

    if (!p->agc && !p->denoise && !p->dereverb)
    {
        lsx_report("no features specified. Enabling default settings \"-agc %zu -denoise %zu\".", agcDefault, denoiseDefault);
        p->agc = agcDefault;
        p->denoise = denoiseDefault;
    }

    return SOX_SUCCESS;
}

/*
 * Do anything required when you stop reading samples.
 */
static int stop_speexdsp(sox_effect_t* effp)
{
    priv_t* p = (priv_t*)effp->priv;

    if (p->sps)
    {
        p->speex_preprocess_state_destroy(p->sps);
        p->sps = NULL;
    }

    if (p->buffer)
    {
        free(p->buffer);
        p->buffer = NULL;
    }

    LSX_DLLIBRARY_CLOSE(p, speexdsp_dl);
    return SOX_SUCCESS;
}

/*
 * Prepare processing.
 * Do all initializations.
 */
static int start_speexdsp(sox_effect_t* effp)
{
    priv_t* p = (priv_t*)effp->priv;
    int result = SOX_SUCCESS;
    spx_int32_t int_val;
    float float_val;
    int open_library_result;

    LSX_DLLIBRARY_OPEN(
        p,
        speexdsp_dl,
        SPEEXDSP_FUNC_ENTRIES,
        "libspeexdsp library",
        speexdsp_library_names,
        open_library_result);
    if (open_library_result)
      return SOX_EOF;

    if (p->samples_per_frame)
    {
        p->buffer_end = p->samples_per_frame;
    }
    else
    {
        p->buffer_end = effp->in_signal.rate / p->frames_per_second;
        if (!p->buffer_end)
        {
            lsx_fail("frames_per_second too large for the current sample rate");
            return SOX_EOF;
        }
    }

    p->buffer_opos = p->buffer_end;
    effp->out_signal.precision = 16;

    lsx_valloc(p->buffer, p->buffer_end);

    p->sps = p->speex_preprocess_state_init((int)p->buffer_end, (int)(effp->in_signal.rate + .5));
    if (!p->sps)
    {
        lsx_fail("failed to initialize preprocessor DSP");
        result = SOX_EOF;
        goto Done;
    }

    int_val = p->agc ? 1 : 2;
    p->speex_preprocess_ctl(p->sps, SPEEX_PREPROCESS_SET_AGC, &int_val);
    if (p->agc)
    {
        float_val = p->agc * 327.68f;
        p->speex_preprocess_ctl(p->sps, SPEEX_PREPROCESS_SET_AGC_LEVEL, &float_val);
    }

    int_val = p->denoise ? 1 : 2;
    p->speex_preprocess_ctl(p->sps, SPEEX_PREPROCESS_SET_DENOISE, &int_val);
    if (p->denoise)
    {
        int_val = -(spx_int32_t)p->denoise;
        p->speex_preprocess_ctl(p->sps, SPEEX_PREPROCESS_SET_NOISE_SUPPRESS, &int_val);
    }

    int_val = p->dereverb ? 1 : 2;
    p->speex_preprocess_ctl(p->sps, SPEEX_PREPROCESS_SET_DEREVERB, &int_val);

Done:
    if (result != SOX_SUCCESS)
        stop_speexdsp(effp);

    return result;
}

/*
 * Process up to *isamp samples from ibuf and produce up to *osamp samples
 * in obuf.  Write back the actual numbers of samples to *isamp and *osamp.
 * Return SOX_SUCCESS or, if error occurs, SOX_EOF.
 */
static int flow_speexdsp(
    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 ibuf_pos = 0;
    size_t ibuf_end = *isamp;
    size_t obuf_pos = 0;
    size_t obuf_end = *osamp;
    size_t end_pos;
    SOX_SAMPLE_LOCALS;

    for (;;)
    {
        /* Write any processed data in working buffer to the output buffer. */
        end_pos = obuf_pos + min(p->buffer_end - p->buffer_opos, obuf_end - obuf_pos);
        for (; obuf_pos < end_pos; obuf_pos++, p->buffer_opos++)
            obuf[obuf_pos] = SOX_SIGNED_16BIT_TO_SAMPLE(p->buffer[p->buffer_opos], dummy);
        if (p->buffer_opos != p->buffer_end)
            break; /* Output buffer is full and we still have more processed data. */

        /* Fill working buffer from input buffer. */
        end_pos = ibuf_pos + min(p->buffer_end - p->buffer_ipos, ibuf_end - ibuf_pos);
        for (; ibuf_pos < end_pos; ibuf_pos++, p->buffer_ipos++)
            p->buffer[p->buffer_ipos] = SOX_SAMPLE_TO_SIGNED_16BIT(ibuf[ibuf_pos], effp->clips);
        if (p->buffer_ipos != p->buffer_end)
            break; /* Working buffer is not full and there is no more input data. */

        p->speex_preprocess_run(p->sps, p->buffer);
        p->buffer_ipos = 0;
        p->buffer_opos = 0;
    }

    *isamp = ibuf_pos;
    *osamp = obuf_pos;
    return SOX_SUCCESS;
}

/*
 * Drain out remaining samples if the effect generates any.
 */
static int drain_speexdsp(sox_effect_t* effp, sox_sample_t* obuf, size_t* osamp)
{
    priv_t* p = (priv_t*)effp->priv;
    size_t obuf_pos = 0;
    size_t obuf_end = *osamp;
    size_t i;
    size_t end_pos;

    /* Input that hasn't been processed yet? */
    if (p->buffer_ipos != 0)
    {
        /* DSP only works on full frames, so fill the remaining space with 0s. */
        for (i = p->buffer_ipos; i < p->buffer_end; i++)
            p->buffer[i] = 0;
        p->speex_preprocess_run(p->sps, p->buffer);
        p->buffer_end = p->buffer_ipos;
        p->buffer_ipos = 0;
        p->buffer_opos = 0;
    }

    end_pos = obuf_pos + min(p->buffer_end - p->buffer_opos, obuf_end - obuf_pos);
    for (; obuf_pos < end_pos; obuf_pos++, p->buffer_opos++)
        obuf[obuf_pos] = SOX_SIGNED_16BIT_TO_SAMPLE(p->buffer[p->buffer_opos], dummy);

    *osamp = obuf_pos;
    return
        p->buffer_opos != p->buffer_end
        ? SOX_SUCCESS
        : SOX_EOF;
}

/*
 * Function returning effect descriptor. This should be the only
 * externally visible object.
 */
const sox_effect_handler_t* lsx_speexdsp_effect_fn(void)
{
  /*
   * Effect descriptor.
   * If no specific processing is needed for any of
   * the 6 functions, then the function above can be deleted
   * and NULL used in place of the its name below.
   */
  static const char usage[] = "[options]";

  static char const * const extra_usage[] = {
"Use the Speex DSP library to improve perceived sound quality",
"OPTION         RANGE DEFAULT DESCRIPTION",
"-agc [target]  1-100  100    Enable automatic gain control and optionally",
"                             specify a target volume level",
"-denoise [max] 1-100   15    Enable noise reduction and optionally specify",
"                             the maximum attenuation in dB",
"-dereverb                    Enable reverb reduction",
"-fps N         1-100   20    Specify the number of frames per second",
"-spf N         1-    sr/fps  Specify the number of samples per frame",
"If no options are specified, the -agc and -denoise features are enabled",
    NULL
  };

  static sox_effect_handler_t descriptor = {
    "speexdsp", usage, extra_usage, SOX_EFF_PREC | SOX_EFF_GAIN,
    getopts_speexdsp, start_speexdsp, flow_speexdsp,
    drain_speexdsp, stop_speexdsp, NULL,
    sizeof(priv_t)
  };

  return &descriptor;
}

#endif /* HAVE_SPEEXDSP */
