//	Granulator.cpp - Audio granulator effect.
//	----------------------------------------------------------------------------
//	This file is part of NiallGran, a VST plugin.
//	Copyright (c) 2013 Niall Moody.
//
//	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 3 of the License, or
//	(at your option) any later version.
//
//	This program 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 General Public License for more details.
//
//	You should have received a copy of the GNU General Public License
//	along with this program.  If not, see <http://www.gnu.org/licenses/>.
//	----------------------------------------------------------------------------

#include "Granulator.h"
#include "Random.h"

#include <cassert>
#include <cmath>

//------------------------------------------------------------------------------
Granulator::Granulator():
density(0.5f),
duration(1.0f),
pitch(1.0f),
feedback(0.0f),
mix(1.0f),
level(1.0f),
writePos(0),
nextOnset(0),
levelArray(0),
lastLevel(0.0f)
{
	int i;
	float tempf;

	for(i=0;i<MaxGrains;++i)
		grains[i].setArrays(&delayLine, &grainEnv);

	for(i=0;i<DelayLineSize;++i)
	{
		delayLine[0][i] = 0.0f;
		delayLine[1][i] = 0.0f;

		if(i < (DelayLineSize/3))
		{
			tempf = 1.0f-cosf(((float)i/(float)(DelayLineSize/3)) * 3.14159265f);
			tempf += 1.0f;
			tempf *= 0.5f;

			grainEnv[i] = tempf;
		}
		else if(i < (2*(DelayLineSize/3)))
			grainEnv[i] = 1.0f;
		else
		{
			tempf = cosf(((float)i/(float)(DelayLineSize/3)) * 3.14159265f);
			tempf += 1.0f;
			tempf *= 0.5f;

			grainEnv[i] = tempf;
		}
	}
}

//------------------------------------------------------------------------------
Granulator::~Granulator()
{
	if(levelArray)
		delete [] levelArray;
}

//------------------------------------------------------------------------------
void Granulator::setMaxBlockSize(int size)
{
	if(levelArray)
		delete [] levelArray;

	levelArray = new float[size];
}

//------------------------------------------------------------------------------
void Granulator::processBlock(float *buffer, int numSamples)
{
	int i, j;
	float adjustedLevel;
	int numActiveGrains = 0;

	//Write input to delayLine, calculate which grains should start during this
	//block.
	for(i=0;i<numSamples;++i)
	{
		//Write input to delayLine.
		delayLine[0][writePos] = (delayLine[0][writePos] * feedback) + buffer[(i*2)];
		delayLine[1][writePos] = (delayLine[1][writePos] * feedback) + buffer[(i*2)+1];

		//Update writePos.
		++writePos;
		writePos %= DelayLineSize;

		//Check nextOnset, activate new grain if < 0.
		--nextOnset;
		if(nextOnset <= 0)
		{
			for(j=0;j<MaxGrains;++j)
			{
				if(!grains[j].getActive())
				{
					int delayLineStartPos;
					int lowerBound, upperBound;

					//lowerBound and upperBound represent the bounds of the area
					//within which the grain may start, with 0 representing the
					//current delayLine write position.

					//Work out the bounds of the grain's start position
					//according to its pitch. We do it this way to ensure
					//the read pointer never goes past the write pointer
					//and glitches the audio.
					if(pitch == 1.0f)
					{
						lowerBound = (int)(-((duration*44100.0f)-1));
						upperBound = -1;
					}
					else if(pitch < 1.0f)
					{
						upperBound = -1;
						lowerBound = upperBound - (int)(duration*44100.0f);
					}
					else if(pitch > 1.0f)
					{
						//This should scale it with pitch; higher pitches will
						//need to start further behind the read pointer to avoid
						//overtaking it.
						upperBound = -1 - (int)((pitch-1.0f) * ((2.0f*duration) * 44100.0f));
						lowerBound = upperBound - (int)(duration*44100.0f);
					}

					//The grain's start position is a random position within
					//those bounds.
					delayLineStartPos = Random::getGlobal().getRandomInt(abs(upperBound-lowerBound));
					delayLineStartPos += lowerBound;
					delayLineStartPos += writePos;

					//Don't go outside the bounds of the delayLine.
					delayLineStartPos %= DelayLineSize;
					while(delayLineStartPos < 0)
						delayLineStartPos += DelayLineSize;

					//Activate the grain.
					grains[j].activate(delayLineStartPos,
									   i,
									   duration * 44100.0f,
									   pitch);

					//Calculate the next onset.
					calcNextOnset(true);

					break;
				}
			}
		}

		//Clear buffer so we can write our output in it.
		buffer[(i*2)] *= 1.0f-mix;
		buffer[(i*2)+1] *= 1.0f-mix;
	}

	//Calculate the adjusted level from the number of active grains.
	for(i=0;i<MaxGrains;++i)
	{
		if(grains[i].getActive())
			++numActiveGrains;
	}
	if(numActiveGrains > 1)
		adjustedLevel = 1.0f/sqrtf((float)numActiveGrains);
	else
		adjustedLevel = 1.0f;
	adjustedLevel *= level;
	adjustedLevel *= mix;

	//If the level has changed, slew it to the new value.
	if(adjustedLevel != lastLevel)
	{
		for(i=0;i<numSamples;++i)
		{
			lastLevel += (adjustedLevel-lastLevel) * 0.001f;
			if(fabsf(adjustedLevel-lastLevel) < 0.0001f)
				lastLevel = adjustedLevel;

			levelArray[i] = lastLevel;
		}
	}
	else
	{
		for(i=0;i<numSamples;++i)
			levelArray[i] = adjustedLevel;
	}

	//Write grain audio to output.
	for(i=0;i<MaxGrains;++i)
	{
		if(grains[i].getActive())
			grains[i].getBlock(buffer, numSamples, levelArray);
	}
}

//------------------------------------------------------------------------------
void Granulator::setDensity(float val)
{
	density = val;
	calcNextOnset(false);
}

//------------------------------------------------------------------------------
void Granulator::setDuration(float val)
{
	duration = val;
}

//------------------------------------------------------------------------------
void Granulator::setPitch(float val)
{
	pitch = val;
}

//------------------------------------------------------------------------------
void Granulator::setFeedback(float val)
{
	feedback = val;
}

//------------------------------------------------------------------------------
void Granulator::setMix(float val)
{
	mix = val;
}

//------------------------------------------------------------------------------
void Granulator::setLevel(float val)
{
	level = val;
}

//------------------------------------------------------------------------------
void Granulator::calcNextOnset(bool forceUpdate)
{
	float tempf;
	int tempOnset;
	float invDensity;

	if(density < 0.001f)
		tempf = 0.001f;
	else
		tempf = density;
	tempf = tanh((tempf*3.14159265f)-3.14159265f)+1.0f;
	invDensity = 1.0f/(tempf * 128.0f);

	tempf = Random::getGlobal().getRandomFloat();
	tempOnset = (int)((-logf(tempf)*invDensity) * 44100.0f);

	if((forceUpdate) || (nextOnset > tempOnset))
		nextOnset = tempOnset;
}

//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
Granulator::Grain::Grain():
delayLine(0),
envelope(0),
active(false),
grainPos(0.0f),
length(0.0f),
delayLinePos(0.0f),
inc(1.0f),
startPos(0)
{

}

//------------------------------------------------------------------------------
Granulator::Grain::~Grain()
{

}

//------------------------------------------------------------------------------
void Granulator::Grain::setArrays(DelayLineArray *globalDelayLine,
								  EnvelopeArray *globalEnvelope)
{
	delayLine = globalDelayLine;
	envelope = globalEnvelope;
}

//------------------------------------------------------------------------------
void Granulator::Grain::activate(int delayLineStartPos,
								 int bufferStartPos,
								 float grainLength,
								 float speed)
{
	assert(delayLine != 0);
	assert(!active);

	grainPos = grainLength;
	length = grainLength;
	delayLinePos = (float)delayLineStartPos;
	inc = speed;
	startPos = bufferStartPos;

	active = true;
}

//------------------------------------------------------------------------------
void Granulator::Grain::getBlock(float *buffer, int numSamples, float *level)
{
	int i;
	float env;
	int delayLineSample;
	float attackEnd = length/3.0f;
	float releaseStart = attackEnd * 2.0f;
	//Does this optimise things a bit?
	float invAttackEnd = 1.0f/attackEnd;

	for(i=startPos;i<numSamples;++i)
	{
		//Calculate the grain's amplitude envelope.
		env = (*envelope)[(int)grainPos];

		//Write grain's audio to buffer.
		delayLineSample = (int)delayLinePos;
		buffer[(i*2)] += (*delayLine)[0][delayLineSample] * env * level[i];
		buffer[(i*2)+1] += (*delayLine)[1][delayLineSample] * env * level[i];

		//Update grain sample positions.
		delayLinePos += inc;
		if(delayLinePos >= (float)Granulator::DelayLineSize)
			delayLinePos -= (float)Granulator::DelayLineSize;
		grainPos -= inc;
		if(grainPos <= 0.0f)
		{
			active = false;
			break;
		}
	}

	startPos = 0;
}
