
/*
 * CINELERRA
 * Copyright (C) 2008 Adam Williams <broadcast at earthling dot net>
 *
 * 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.
 *
 * 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */

#include "bcdisplayinfo.h"
#include "clip.h"
#include "bchash.h"
#include "filexml.h"
#include "language.h"
#include "pitch.h"
#include "samples.h"
#include "theme.h"
#include "units.h"
#include "vframe.h"

#include <math.h>
#include <string.h>


// It needs to be at least 40Hz yet high enough to have enough precision
#define WINDOW_SIZE 2048


//#define WINDOW_SIZE 131072

REGISTER_PLUGIN(PitchEffect);





PitchEffect::PitchEffect(PluginServer *server)
 : PluginAClient(server)
{

	reset();
}

PitchEffect::~PitchEffect()
{


	if(fft) delete fft;
}

const char* PitchEffect::plugin_title() { return N_("Pitch shift"); }
int PitchEffect::is_realtime() { return 1; }



void PitchEffect::read_data(KeyFrame *keyframe)
{
	FileXML input;
	input.set_shared_input(keyframe->xbuf);

	int result = 0;
	while(!result)
	{
		result = input.read_tag();

		if(!result)
		{
			if(input.tag.title_is("PITCH"))
			{
				config.scale = input.tag.get_property("SCALE", config.scale);
				config.size = input.tag.get_property("SIZE", config.size);
			}
		}
	}
}

void PitchEffect::save_data(KeyFrame *keyframe)
{
	FileXML output;
	output.set_shared_output(keyframe->xbuf);

	output.tag.set_title("PITCH");
	output.tag.set_property("SCALE", config.scale);
	output.tag.set_property("SIZE", config.size);
	output.append_tag();
	output.tag.set_title("/PITCH");
	output.append_tag();
	output.append_newline();
	output.terminate_string();
}



LOAD_CONFIGURATION_MACRO(PitchEffect, PitchConfig)

NEW_WINDOW_MACRO(PitchEffect, PitchWindow)



void PitchEffect::reset()
{
	fft = 0;
}

void PitchEffect::update_gui()
{
	if(thread)
	{
		if(load_configuration())
		{
			thread->window->lock_window("PitchEffect::update_gui");
			((PitchWindow*)thread->window)->update();
			thread->window->unlock_window();
		}
	}
}



int PitchEffect::process_buffer(int64_t size,
		Samples *buffer,
		int64_t start_position,
		int sample_rate)
{
//printf("PitchEffect::process_buffer %d\n", __LINE__);
	load_configuration();

	if(fft && config.size != fft->window_size)
	{
		delete fft;
		fft = 0;
	}

	if(!fft)
	{
		fft = new PitchFFT(this);
		fft->initialize(config.size);
	}

	fft->process_buffer(start_position,
		size,
		buffer,
		get_direction());

	return 0;
}









PitchFFT::PitchFFT(PitchEffect *plugin)
 : CrossfadeFFT()
{
	this->plugin = plugin;
	last_phase = 0;
	new_freq = 0;
	new_magn = 0;
	sum_phase = 0;
	anal_magn = 0;
	anal_freq = 0;
}

PitchFFT::~PitchFFT()
{
	delete [] last_phase;
	delete [] new_freq;
	delete [] new_magn;
	delete [] sum_phase;
	delete [] anal_magn;
	delete [] anal_freq;
}


int PitchFFT::signal_process()
{
#if 0
	double scale = plugin->config.scale;
	double oversample = 1.0;

	if(!new_freq)
	{
		last_phase = new double[window_size];
		new_freq = new double[window_size];
		new_magn = new double[window_size];
		sum_phase = new double[window_size];
		anal_magn = new double[window_size];
		anal_freq = new double[window_size];
		memset (last_phase, 0, window_size * sizeof(double));
		memset (sum_phase, 0, window_size * sizeof(double));
	}

	memset(new_freq, 0, window_size * sizeof(double));
	memset(new_magn, 0, window_size * sizeof(double));

// expected phase difference between windows
	double expected_phase_diff = 2.0 * M_PI / oversample;
// frequency per bin
	double freq_per_bin = (double)plugin->PluginAClient::project_sample_rate / window_size;

	for (int i = 0; i < window_size / 2; i++)
	{
// Convert to magnitude and phase
		double magn = sqrt(freq_real[i] * freq_real[i] + freq_imag[i] * freq_imag[i]);
		double phase = atan2(freq_imag[i], freq_real[i]);

// Remember last phase
		double temp = phase - last_phase[i];
		last_phase[i] = phase;

// Substract the expected advancement of phase
		temp -= (double)i * expected_phase_diff;


// wrap temp into -/+ PI ...  good trick!
		int qpd = (int)(temp/M_PI);
		if (qpd >= 0)
			qpd += qpd & 1;
		else
			qpd -= qpd & 1;
		temp -= M_PI * (double)qpd;

// Deviation from bin frequency
		temp = oversample * temp / (2.0 * M_PI);

		temp = (double)(temp + i) * freq_per_bin;

// Now temp is the real freq... move it!
		int new_bin = (int)(i * scale);
		if (new_bin >= 0 && new_bin < window_size / 2)
		{
			new_freq[new_bin] = temp*scale;
			new_magn[new_bin] += magn;
		}

	}




// Synthesize back the fft window
	for (int i = 0; i < window_size / 2; i++)
	{
		double magn = new_magn[i];
		double temp = new_freq[i];
// substract the bin frequency
		temp -= (double)(i) * freq_per_bin;

// get bin deviation from freq deviation
		temp /= freq_per_bin;

// oversample
		temp = 2.0 * M_PI * temp / oversample;

// add back the expected phase difference (that we substracted in analysis)
		temp += (double)(i) * expected_phase_diff;

// accumulate delta phase, to get bin phase
		sum_phase[i] += temp;

		double phase = sum_phase[i];

		freq_real[i] = magn * cos(phase);
		freq_imag[i] = magn * sin(phase);
	}

	for (int i = window_size / 2; i < window_size; i++)
	{
		freq_real[i] = 0;
		freq_imag[i] = 0;
	}
#endif


#if 1
	int min_freq =
		1 + (int)(20.0 / ((double)plugin->PluginAClient::project_sample_rate /
			window_size * 2) + 0.5);
	if(plugin->config.scale < 1)
	{
		for(int i = min_freq; i < window_size / 2; i++)
		{
			double destination = i * plugin->config.scale;
			int dest_i = (int)(destination + 0.5);
			if(dest_i != i)
			{
				if(dest_i <= window_size / 2)
				{
					freq_real[dest_i] = freq_real[i];
					freq_imag[dest_i] = freq_imag[i];
				}
				freq_real[i] = 0;
				freq_imag[i] = 0;
			}
		}
	}
	else
	if(plugin->config.scale > 1)
	{
		for(int i = window_size / 2 - 1; i >= min_freq; i--)
		{
			double destination = i * plugin->config.scale;
			int dest_i = (int)(destination + 0.5);
			if(dest_i != i)
			{
				if(dest_i <= window_size / 2)
				{
					freq_real[dest_i] = freq_real[i];
					freq_imag[dest_i] = freq_imag[i];
				}
				freq_real[i] = 0;
				freq_imag[i] = 0;
			}
		}
	}

	symmetry(window_size, freq_real, freq_imag);
#endif


	return 0;
}

int PitchFFT::read_samples(int64_t output_sample,
	int samples,
	Samples *buffer)
{
	return plugin->read_samples(buffer,
		0,
		plugin->get_samplerate(),
		output_sample,
		samples);
}







PitchConfig::PitchConfig()
{
	scale = 1.0;
	size = 2048;
}

int PitchConfig::equivalent(PitchConfig &that)
{
	return EQUIV(scale, that.scale) && size == that.size;
}

void PitchConfig::copy_from(PitchConfig &that)
{
	scale = that.scale;
	size = that.size;
}

void PitchConfig::interpolate(PitchConfig &prev,
	PitchConfig &next,
	int64_t prev_frame,
	int64_t next_frame,
	int64_t current_frame)
{
	double next_scale = (double)(current_frame - prev_frame) / (next_frame - prev_frame);
	double prev_scale = (double)(next_frame - current_frame) / (next_frame - prev_frame);
	scale = prev.scale * prev_scale + next.scale * next_scale;
	size = prev.size;
}

















PitchWindow::PitchWindow(PitchEffect *plugin)
 : PluginClientWindow(plugin,
	xS(150),
	yS(100),
	xS(150),
	yS(100),
	0)
{
	this->plugin = plugin;
}

void PitchWindow::create_objects()
{
	int x1 = xS(10), x = xS(10), y = yS(10);

	BC_Title *title;
	add_subwindow(title = new BC_Title(x, y, _("Scale:")));
	x += title->get_w() + plugin->get_theme()->widget_border;
	add_subwindow(scale = new PitchScale(plugin, x, y));
	x = x1;
	y += scale->get_h() + plugin->get_theme()->widget_border;
	add_subwindow(title = new BC_Title(x, y, _("Window Size:")));
	y += title->get_h() + plugin->get_theme()->widget_border;
	add_subwindow(size = new PitchSize(this, plugin, x, y));
	size->create_objects();
	size->update(plugin->config.size);
	show_window();
	flush();
}



void PitchWindow::update()
{
	scale->update(plugin->config.scale);
	size->update(plugin->config.size);
}












PitchScale::PitchScale(PitchEffect *plugin, int x, int y)
 : BC_FPot(x, y, (float)plugin->config.scale, .5, 1.5)
{
	this->plugin = plugin;
	set_precision(0.01);
}

int PitchScale::handle_event()
{
	plugin->config.scale = get_value();
	plugin->send_configure_change();
	return 1;
}



PitchSize::PitchSize(PitchWindow *window, PitchEffect *plugin, int x, int y)
 : BC_PopupMenu(x, y, xS(110), "4096", 1)
{
	this->plugin = plugin;
}

int PitchSize::handle_event()
{
	plugin->config.size = atoi(get_text());
	plugin->send_configure_change();
	return 1;
}

void PitchSize::create_objects()
{
	add_item(new BC_MenuItem("2048"));
	add_item(new BC_MenuItem("4096"));
	add_item(new BC_MenuItem("8192"));
	add_item(new BC_MenuItem("16384"));
	add_item(new BC_MenuItem("32768"));
	add_item(new BC_MenuItem("65536"));
	add_item(new BC_MenuItem("131072"));
	add_item(new BC_MenuItem("262144"));
}

void PitchSize::update(int size)
{
	char string[BCTEXTLEN];
	sprintf(string, "%d", size);
	set_text(string);
}














