/***************************************************************************
    NormalizePlugin.cpp  -  plugin for level normalizing
                             -------------------
    begin                : Fri May 01 2009
    copyright            : (C) 2009 by Thomas Eschenbacher
    email                : Thomas.Eschenbacher@gmx.de

    original algorithms  : (C) 1999-2005 Chris Vaill <chrisvaill at gmail>
                           taken from "normalize-0.7.7"
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

#include "config.h"

#include <math.h>
#include <new>

#include <QFutureSynchronizer>
#include <QList>
#include <QStringList>
#include <QThread>
#include <QtConcurrentRun>

#include <KLocalizedString> // for the i18n macro

#include "libkwave/Connect.h"
#include "libkwave/FileInfo.h"
#include "libkwave/MultiTrackReader.h"
#include "libkwave/MultiTrackWriter.h"
#include "libkwave/PluginManager.h"
#include "libkwave/SignalManager.h"
#include "libkwave/Utils.h"
#include "libkwave/Writer.h"
#include "libkwave/undo/UndoTransactionGuard.h"

#include "NormalizePlugin.h"
#include "Normalizer.h"

/** use a 100-element (1 second) window [units of 10ms] */
#define SMOOTHLEN 100

/** target volume level [dB] */
#define TARGET_LEVEL -12

KWAVE_PLUGIN(normalize, NormalizePlugin)

//***************************************************************************
Kwave::NormalizePlugin::NormalizePlugin(QObject *parent,
                                        const QVariantList &args)
    :Kwave::Plugin(parent, args)
{
}

//***************************************************************************
Kwave::NormalizePlugin::~NormalizePlugin()
{
}

//***************************************************************************
void Kwave::NormalizePlugin::run(QStringList params)
{
    Q_UNUSED(params)
    Kwave::UndoTransactionGuard undo_guard(*this, i18n("Normalize"));

    // get the current selection
    QVector<unsigned int> tracks;
    sample_index_t first = 0;
    sample_index_t last  = 0;
    sample_index_t length = selection(&tracks, &first, &last, true);
    if (!length || tracks.isEmpty()) return;

    // get the current volume level
    double level = 0.0;
    {
	Kwave::MultiTrackReader src(Kwave::SinglePassForward,
	    signalManager(), tracks, first, last);

	// connect the progress dialog
	connect(&src, SIGNAL(progress(qreal)),
		this,  SLOT(updateProgress(qreal)),
		Qt::BlockingQueuedConnection);

	// detect the peak value
	emit setProgressText(i18n("Analyzing volume level..."));
//         qDebug("NormalizePlugin: getting peak...");
	level = getMaxPower(src);
//         qDebug("NormalizePlugin: level is %g", level);
    }

    Kwave::MultiTrackReader source(Kwave::SinglePassForward,
	signalManager(), tracks, first, last);
    Kwave::MultiTrackWriter sink(signalManager(), tracks, Kwave::Overwrite,
	first, last);
    Kwave::MultiTrackSource<Kwave::Normalizer, true> normalizer(
	tracks.count(), this);

    // break if aborted
    if (!sink.tracks()) return;

    // connect the progress dialog
    connect(&source, SIGNAL(progress(qreal)),
	    this,  SLOT(updateProgress(qreal)),
	    Qt::BlockingQueuedConnection);

    // connect them
    bool ok = Kwave::connect(
	source,     SIGNAL(output(Kwave::SampleArray)),
	normalizer, SLOT(input(Kwave::SampleArray)));
    if (ok) ok = Kwave::connect(
	normalizer, SIGNAL(output(Kwave::SampleArray)),
	sink,       SLOT(input(Kwave::SampleArray)));
    if (!ok) {
	return;
    }

    double target = pow(10.0, (TARGET_LEVEL / 20.0));
    double gain = target / level;
    qDebug("NormalizePlugin: gain=%g", gain);

    QString db;
    emit setProgressText(i18n("Normalizing (%1 dB) ...",
	db.asprintf("%+0.1f", 20 * log10(gain))));

    normalizer.setAttribute(SLOT(setGain(QVariant)), QVariant(gain));
    while (!shouldStop() && !source.eof()) {
	source.goOn();
    }

    sink.flush();
}

//***************************************************************************
double Kwave::NormalizePlugin::getMaxPower(Kwave::MultiTrackReader &source)
{
    double maxpow = 0.0;
    const unsigned int tracks = source.tracks();
    const double rate = Kwave::FileInfo(signalManager().metaData()).rate();
    const unsigned int window_size = Kwave::toUint(rate / 100);
    if (!window_size) return 0;

    // set up smoothing window buffer
    QVector<Kwave::NormalizePlugin::Average> average(tracks);
    for (unsigned int t = 0; t < tracks; t++) {
	average[t].fifo = QVector<double>(SMOOTHLEN, double(0.0));
	average[t].wp  = 0;
	average[t].n   = 0;
	average[t].sum = 0.0;
	average[t].max = 0.0;
    }

    while (!shouldStop() && !source.eof()) {
	QFutureSynchronizer<void> synchronizer;

	for (unsigned int t = 0; t < tracks; t++) {
	    Kwave::SampleReader *reader = source[t];
	    if (!reader) continue;
	    if (reader->eof()) continue;

	    synchronizer.addFuture(QtConcurrent::run(
		this,
		&Kwave::NormalizePlugin::getMaxPowerOfTrack,
		reader, &(average[t]), window_size
	    ));
	}
	synchronizer.waitForFinished();
     }

    if (average[0].n < SMOOTHLEN) {
	// if file was too short, calculate power out of what we have
	for (unsigned int t = 0; t < tracks; t++) {
	    Kwave::NormalizePlugin::Average &avg = average[t];
	    double pow = avg.sum / static_cast<double>(avg.n);
	    if (pow > maxpow) maxpow = pow;
	}
    } else {
	// get maximum among all tracks
	for (unsigned int t = 0; t < tracks; t++) {
	    double p = average[t].max;
	    if (p > maxpow) maxpow = p;
	}
    }

    double level = sqrt(maxpow);
    return level;
}

//***************************************************************************
void Kwave::NormalizePlugin::getMaxPowerOfTrack(
    Kwave::SampleReader *reader,
    Kwave::NormalizePlugin::Average *p_average,
    unsigned int window_size)
{
    Kwave::NormalizePlugin::Average &average = *p_average;
    Kwave::SampleArray data(window_size);
    unsigned int round = 0;
    unsigned int loops = 5 * reader->blockSize() / window_size;
    loops++;

    while ((round++ < loops) && !reader->eof()) {
	unsigned int len = reader->read(data, 0, window_size);

	// calculate power of one block
	double sum = 0;
	const Kwave::SampleArray &in = data;
	for (unsigned int i = 0; i < len; i++) {
	    sample_t s = in[i];
	    double d   = sample2double(s);
	    sum += (d * d);
	}
	double pow = sum / static_cast<double>(len);

	// collect all power values in a FIFO
	unsigned int wp = average.wp;
	average.sum -= average.fifo[wp];
	average.sum += pow;
	average.fifo[wp] = pow;
	if (++wp >= SMOOTHLEN) wp = 0;
	average.wp = wp;
	if (average.n == SMOOTHLEN) {
	    // detect power peak
	    double p = average.sum / static_cast<double>(SMOOTHLEN);
	    if (p > average.max) average.max = p;
	} else {
	    average.n++;
	}
    }
//     qDebug("%p -> pos=%llu, max=%g", this, reader->pos(), average.max);
}

//***************************************************************************
#include "NormalizePlugin.moc"
//***************************************************************************
//***************************************************************************
