
/*
 * CINELERRA
 * Copyright (C) 1997-2011 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 "audioscope.h"
#include "bcdisplayinfo.h"
#include "bcsignals.h"
#include "clip.h"
#include "cursors.h"
#include "bchash.h"
#include "filexml.h"
#include "language.h"
#include "bccolors.h"
#include "samples.h"
#include "theme.h"
#include "transportque.inc"
#include "units.h"
#include "vframe.h"


#include <string.h>



REGISTER_PLUGIN(AudioScope)


AudioScopeConfig::AudioScopeConfig()
{
	window_size = MAX_WINDOW;
	history_size = 4;
	mode = XY_MODE;
	trigger_level = 0;
}

int AudioScopeConfig::equivalent(AudioScopeConfig &that)
{
	return window_size == that.window_size &&
		history_size == that.history_size &&
		mode == that.mode &&
		EQUIV(trigger_level, that.trigger_level);
}

void AudioScopeConfig::copy_from(AudioScopeConfig &that)
{
	window_size = that.window_size;
	history_size = that.history_size;
	mode = that.mode;
	trigger_level = that.trigger_level;
}

void AudioScopeConfig::interpolate(AudioScopeConfig &prev,
	AudioScopeConfig &next,
	int64_t prev_frame,
	int64_t next_frame,
	int64_t current_frame)
{
	copy_from(prev);

	CLAMP(history_size, MIN_HISTORY, MAX_HISTORY - 1);
}



AudioScopeFrame::AudioScopeFrame(int data_size, int channels)
{
	this->size = data_size;
	this->channels = channels;
	for(int i = 0; i < CHANNELS; i++)
		data[i] = new float[data_size];
	force = 0;
}

AudioScopeFrame::~AudioScopeFrame()
{
	for(int i = 0; i < CHANNELS; i++)
		delete [] data[i];
}







AudioScopeHistory::AudioScopeHistory(AudioScope *plugin,
	int x,
	int y)
 : BC_IPot(x,
	y,
	plugin->config.history_size,
	MIN_HISTORY,
	MAX_HISTORY)
{
	this->plugin = plugin;
}

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








AudioScopeWindowSize::AudioScopeWindowSize(AudioScope *plugin,
	int x,
	int y,
	char *text)
 : BC_PopupMenu(x,
	y,
	xS(110),
	text)
{
	this->plugin = plugin;
}

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


AudioScopeWindowSizeTumbler::AudioScopeWindowSizeTumbler(AudioScope *plugin, int x, int y)
 : BC_Tumbler(x,
 	y)
{
	this->plugin = plugin;
}

int AudioScopeWindowSizeTumbler::handle_up_event()
{
	plugin->config.window_size *= 2;
	if(plugin->config.window_size > MAX_WINDOW)
		plugin->config.window_size = MAX_WINDOW;
	char string[BCTEXTLEN];
	sprintf(string, "%d", plugin->config.window_size);
	((AudioScopeWindow*)plugin->get_thread()->get_window())->window_size->set_text(string);
	plugin->send_configure_change();
	return 1;
}

int AudioScopeWindowSizeTumbler::handle_down_event()
{
	plugin->config.window_size /= 2;
	if(plugin->config.window_size < MIN_WINDOW)
		plugin->config.window_size = MIN_WINDOW;
	char string[BCTEXTLEN];
	sprintf(string, "%d", plugin->config.window_size);
	((AudioScopeWindow*)plugin->get_thread()->get_window())->window_size->set_text(string);
	plugin->send_configure_change();
	return 1;
}





AudioScopeCanvas::AudioScopeCanvas(AudioScope *plugin,
	int x,
	int y,
	int w,
	int h)
 : BC_SubWindow(x, y, w, h, BLACK)
{
	this->plugin = plugin;
	current_operation = NONE;
}

int AudioScopeCanvas::button_press_event()
{
	if(is_event_win() && cursor_inside())
	{
		calculate_point();
		current_operation = DRAG;
		plugin->send_configure_change();
		return 1;
	}
	return 0;
}

int AudioScopeCanvas::button_release_event()
{
	if(current_operation == DRAG)
	{
		current_operation = NONE;
		return 1;
	}
	return 0;
}

int AudioScopeCanvas::cursor_motion_event()
{
	if(current_operation == DRAG)
	{
		calculate_point();
		return 1;
	}
	return 0;
}


void AudioScopeCanvas::calculate_point()
{
	int x = get_cursor_x();
	int y = get_cursor_y();
	CLAMP(x, 0, get_w() - 1);
	CLAMP(y, 0, get_h() - 1);

	((AudioScopeWindow*)plugin->thread->window)->calculate_probe(x, y, 1);

//printf("AudioScopeCanvas::calculate_point %d %d\n", __LINE__, Freq::tofreq(freq_index));
}







AudioScopeTriggerLevel::AudioScopeTriggerLevel(AudioScope *plugin, int x, int y)
 : BC_FPot(x, y, plugin->config.trigger_level, (float)-1.0, (float)1.0)
{
	this->plugin = plugin;
	set_precision(0.01);
}

int AudioScopeTriggerLevel::handle_event()
{
	AudioScopeWindow *window = (AudioScopeWindow*)plugin->thread->window;
	window->draw_trigger();
	plugin->config.trigger_level = get_value();
	plugin->send_configure_change();
	window->draw_trigger();
	window->canvas->flash();
	return 1;
}





AudioScopeMode::AudioScopeMode(AudioScope *plugin,
	int x,
	int y)
 : BC_PopupMenu(x,
	y,
	xS(180),
	mode_to_text(plugin->config.mode))
{
	this->plugin = plugin;
}

void AudioScopeMode::create_objects()
{
	add_item(new BC_MenuItem(mode_to_text(XY_MODE)));
	add_item(new BC_MenuItem(mode_to_text(WAVEFORM_NO_TRIGGER)));
	add_item(new BC_MenuItem(mode_to_text(WAVEFORM_RISING_TRIGGER)));
	add_item(new BC_MenuItem(mode_to_text(WAVEFORM_FALLING_TRIGGER)));
}

int AudioScopeMode::handle_event()
{
	if(plugin->config.mode != text_to_mode(get_text()))
	{
		AudioScopeWindow *window = (AudioScopeWindow*)plugin->thread->window;
		window->probe_x = -1;
		window->probe_y = -1;
		plugin->config.mode = text_to_mode(get_text());
		plugin->send_configure_change();

		window->canvas->clear_box(0, 0, window->canvas->get_w(), window->canvas->get_h());
		window->draw_overlay();
		window->canvas->flash();
	}
	return 1;
}

const char* AudioScopeMode::mode_to_text(int mode)
{
	switch(mode)
	{
		case XY_MODE:
			return _("XY Mode");
		case WAVEFORM_NO_TRIGGER:
			return _("Waveform");
		case WAVEFORM_RISING_TRIGGER:
			return _("Rising Trigger");
		case WAVEFORM_FALLING_TRIGGER:
		default:
			return _("Falling Trigger");
	}
}

int AudioScopeMode::text_to_mode(const char *text)
{
	if(!strcmp(mode_to_text(XY_MODE), text)) return XY_MODE;
	if(!strcmp(mode_to_text(WAVEFORM_NO_TRIGGER), text)) return WAVEFORM_NO_TRIGGER;
	if(!strcmp(mode_to_text(WAVEFORM_RISING_TRIGGER), text)) return WAVEFORM_RISING_TRIGGER;
	if(!strcmp(mode_to_text(WAVEFORM_FALLING_TRIGGER), text)) return WAVEFORM_FALLING_TRIGGER;
	return XY_MODE;
}









AudioScopeWindow::AudioScopeWindow(AudioScope *plugin)
 : PluginClientWindow(plugin, plugin->w, plugin->h, xS(320), yS(320), 1)
{
	this->plugin = plugin; 
	probe_x = -1;
	probe_y = -1;
}

AudioScopeWindow::~AudioScopeWindow()
{
}

void AudioScopeWindow::create_objects()
{
	int x = plugin->get_theme()->widget_border;
	int y = 0;
	char string[BCTEXTLEN];

	add_subwindow(canvas = new AudioScopeCanvas(plugin,
		0,
		0,
		get_w(),
		get_h() -
			plugin->get_theme()->widget_border * 4 -
			BC_Title::calculate_h(this, "X") * 3));
	canvas->set_cursor(CROSS_CURSOR, 0, 0);
	draw_overlay();

	y += canvas->get_h() + plugin->get_theme()->widget_border;

	add_subwindow(history_size_title = new BC_Title(x, y, _("History Size:")));
	x += history_size_title->get_w() + plugin->get_theme()->widget_border;
	add_subwindow(history_size = new AudioScopeHistory(plugin,
		x,
		y));
	x += history_size->get_w() + plugin->get_theme()->widget_border;


	int x1 = x;
	sprintf(string, "%d", plugin->config.window_size);
	add_subwindow(window_size_title = new BC_Title(x, y, _("Window Size:")));

	x += window_size_title->get_w() + plugin->get_theme()->widget_border;
	add_subwindow(window_size = new AudioScopeWindowSize(plugin, x, y, string));
	x += window_size->get_w();
	add_subwindow(window_size_tumbler = new AudioScopeWindowSizeTumbler(plugin, x, y));

	for(int i = MIN_WINDOW; i <= MAX_WINDOW; i *= 2)
	{
		sprintf(string, "%d", i);
		window_size->add_item(new BC_MenuItem(string));
	}

	x += window_size_tumbler->get_w() + plugin->get_theme()->widget_border;

	int y1 = y;
	x = x1;
	y += window_size->get_h() + plugin->get_theme()->widget_border;
	add_subwindow(mode_title = new BC_Title(x, y, _("Mode:")));

	x += mode_title->get_w() + plugin->get_theme()->widget_border;
	add_subwindow(mode = new AudioScopeMode(plugin, x, y));
	mode->create_objects();

	x += mode->get_w() + plugin->get_theme()->widget_border;
	y = y1;
	add_subwindow(trigger_level_title = new BC_Title(x, y, _("Trigger level:")));

	x += trigger_level_title->get_w() + plugin->get_theme()->widget_border;
	add_subwindow(trigger_level = new AudioScopeTriggerLevel(plugin, x, y));


	x += trigger_level->get_w() + plugin->get_theme()->widget_border;
	y = y1;
	add_subwindow(probe_sample = new BC_Title(x, y, _("Sample: 0")));
	y += probe_sample->get_h() + plugin->get_theme()->widget_border;
	add_subwindow(probe_level0 = new BC_Title(x, y, _("Level 0: 0"), MEDIUMFONT, CHANNEL0_COLOR));
	y += probe_level0->get_h() + plugin->get_theme()->widget_border;
	add_subwindow(probe_level1 = new BC_Title(x, y, _("Level 1: 0"), MEDIUMFONT, CHANNEL1_COLOR));
	y += probe_level1->get_h() + plugin->get_theme()->widget_border;



	show_window();
}

int AudioScopeWindow::resize_event(int w, int h)
{
	int y_diff = h - get_h();

	int canvas_factor = get_h() - canvas->get_h();
	canvas->reposition_window(0,
		0,
		w,
		h - canvas_factor);
	canvas->clear_box(0, 0, canvas->get_w(), canvas->get_h());
	draw_overlay();
	canvas->flash(0);


// Remove all columns which may be a different size.
	plugin->frame_buffer.remove_all_objects();

	window_size_title->reposition_window(window_size_title->get_x(),
		window_size_title->get_y() + y_diff);
	window_size->reposition_window(window_size->get_x(),
		window_size->get_y() + y_diff);
	window_size_tumbler->reposition_window(window_size_tumbler->get_x(),
		window_size_tumbler->get_y() + y_diff);



	history_size_title->reposition_window(history_size_title->get_x(),
		history_size_title->get_y() + y_diff);
	history_size->reposition_window(history_size->get_x(),
		history_size->get_y() + y_diff);

	mode_title->reposition_window(mode_title->get_x(),
		mode_title->get_y() + y_diff);
	mode->reposition_window(mode->get_x(),
		mode->get_y() + y_diff);


	trigger_level_title->reposition_window(trigger_level_title->get_x(),
		trigger_level_title->get_y() + y_diff);
	trigger_level->reposition_window(trigger_level->get_x(),
		trigger_level->get_y() + y_diff);


	probe_sample->reposition_window(probe_sample->get_x(),
		probe_sample->get_y() + y_diff);
	probe_level0->reposition_window(probe_level0->get_x(),
		probe_level0->get_y() + y_diff);
	probe_level1->reposition_window(probe_level1->get_x(),
		probe_level1->get_y() + y_diff);

	flush();

	plugin->w = w;
	plugin->h = h;
	return 0;
}

void AudioScopeWindow::draw_overlay()
{
	canvas->set_color(GREEN);

	canvas->draw_line(0,
		canvas->get_h() / 2,
		canvas->get_w(),
		canvas->get_h() / 2);
	if(plugin->config.mode == XY_MODE)
	{
		canvas->draw_line(canvas->get_w() / 2,
			0,
			canvas->get_w() / 2,
			canvas->get_h());
	}

	draw_trigger();
	draw_probe();
//printf("AudioScopeWindow::draw_overlay %d\n", __LINE__);
}

void AudioScopeWindow::draw_trigger()
{
	if(plugin->config.mode == WAVEFORM_RISING_TRIGGER ||
		plugin->config.mode == WAVEFORM_FALLING_TRIGGER)
	{
		int y = (int)(-plugin->config.trigger_level *
			canvas->get_h() / 2 +
			canvas->get_h() / 2);
		CLAMP(y, 0, canvas->get_h() - 1);
		canvas->set_inverse();
		canvas->set_color(RED);
		canvas->draw_line(0, y, canvas->get_w(), y);
		canvas->set_opaque();
	}
}

void AudioScopeWindow::draw_probe()
{
	if(probe_x >= 0 || probe_y >= 0)
	{
		canvas->set_color(GREEN);
		canvas->set_inverse();

		if(plugin->config.mode == XY_MODE)
		{
			canvas->draw_line(0, probe_y, get_w(), probe_y);
			canvas->draw_line(probe_x, 0, probe_x, get_h());
		}
		else
		{
			canvas->draw_line(probe_x, 0, probe_x, get_h());
		}

		canvas->set_opaque();
	}
}




void AudioScopeWindow::calculate_probe(int x, int y, int do_overlay)
{
	if(x < 0 && y < 0) return;

// Clear previous overlay
	if(do_overlay) draw_probe();

// New probe position
	probe_x = x;
	probe_y = y;

	int sample = 0;
	double channel0 = 0;
	double channel1 = 0;
	if(plugin->config.mode == XY_MODE)
	{
		channel0 = (float)(probe_x - canvas->get_w() / 2) / (canvas->get_w() / 2);
		channel1 = (float)(canvas->get_h() / 2 - probe_y) / (canvas->get_h() / 2);
	}
	else
	if(plugin->current_frame)
	{
		sample = probe_x * plugin->current_frame->size / canvas->get_w();
		CLAMP(sample, 0, plugin->current_frame->size - 1);
		channel0 = plugin->current_frame->data[0][sample];
		channel1 = plugin->current_frame->data[1][sample];
	}


	char string[BCTEXTLEN];
	sprintf(string, _("Sample: %d"), sample);
	probe_sample->update(string);

	sprintf(string, _("Level 0: %.2f"), channel0);
	probe_level0->update(string);

	sprintf(string, _("Level 1: %.2f"), channel1);
	probe_level1->update(string);


	if(do_overlay)
	{
		draw_probe();
		canvas->flash();
	}
}





void AudioScopeWindow::update_gui()
{
	char string[BCTEXTLEN];
	sprintf(string, "%d", plugin->config.window_size);
	window_size->set_text(string);
	history_size->update(plugin->config.history_size);
}

























AudioScope::AudioScope(PluginServer *server)
 : PluginAClient(server)
{
	reset();
	timer = new Timer;
	w = xS(640);
	h = yS(480);
}

AudioScope::~AudioScope()
{
	delete [] data;
	for(int i = 0; i < CHANNELS; i++)
		delete audio_buffer[i];
	delete timer;
	frame_buffer.remove_all_objects();
	frame_history.remove_all_objects();
}


void AudioScope::reset()
{
	thread = 0;
	data = 0;
	for(int i = 0; i < CHANNELS; i++)
		audio_buffer[i] = 0;
	allocated_data = 0;
	total_windows = 0;
	buffer_size = 0;
	bzero(&header, sizeof(data_header_t));
	current_frame = 0;
}


const char* AudioScope::plugin_title() { return N_("AudioScope"); }
int AudioScope::is_realtime() { return 1; }
int AudioScope::is_multichannel() { return 1; }

int AudioScope::process_buffer(int64_t size,
		Samples **buffer,
		int64_t start_position,
		int sample_rate)
{
	int channels = MIN(get_total_buffers(), CHANNELS);

// Pass through
	for(int i = 0; i < get_total_buffers(); i++)
	{
		read_samples(buffer[i],
			i,
			sample_rate,
			start_position,
			size);
	}

	load_configuration();

// Reset audio buffer
	if(window_size != config.window_size)
	{
		render_stop();
		window_size = config.window_size;
	}

	if(!data)
	{
		data = new unsigned char[sizeof(data_header_t)];
		allocated_data = 0;
	}

	if(!audio_buffer[0])
	{
		for(int i = 0; i < CHANNELS; i++)
		{
			audio_buffer[i] = new Samples(MAX_WINDOW);
		}
	}

// Accumulate audio
	int needed = buffer_size + size;
	if(audio_buffer[0]->get_allocated() < needed)
	{
		for(int i = 0; i < CHANNELS; i++)
		{
			Samples *new_samples = new Samples(needed);
			memcpy(new_samples->get_data(),
				audio_buffer[i]->get_data(),
				sizeof(double) * buffer_size);
			delete audio_buffer[i];
			audio_buffer[i] = new_samples;
		}
	}

	for(int i = 0; i < CHANNELS; i++)
	{
		memcpy(audio_buffer[i]->get_data() + buffer_size,
			buffer[MIN(i, channels - 1)]->get_data(),
			sizeof(double) * size);
	}
	buffer_size += size;


// Append a windows to end of GUI buffer
	total_windows = 0;
	while(buffer_size >= window_size)
	{
		if(allocated_data < (total_windows + 1) * window_size * CHANNELS)
		{
			int new_allocation = (total_windows + 1) *
				window_size *
				CHANNELS;
			unsigned char *new_data = new unsigned char[sizeof(data_header_t) +
				sizeof(float) * new_allocation];
			data_header_t *new_header = (data_header_t*)new_data;
			data_header_t *old_header = (data_header_t*)data;
			memcpy(new_header->samples,
				old_header->samples,
				sizeof(float) * allocated_data);
			delete [] data;
			data = new_data;
			allocated_data = new_allocation;
		}

// Search for trigger
		int need_trigger = 0;
		int got_trigger = 0;
		int trigger_sample = 0;
//printf("AudioScope::process_buffer %d\n", __LINE__);
		if(config.mode == WAVEFORM_RISING_TRIGGER)
		{
			need_trigger = 1;
			double *trigger_data = audio_buffer[0]->get_data();
			for(int i = 1; i < buffer_size - window_size; i++)
			{
				if(trigger_data[i - 1] < config.trigger_level &&
					trigger_data[i] >= config.trigger_level)
				{
					got_trigger = 1;
					trigger_sample = i;
					break;
				}
			}
		}
		else
		if(config.mode == WAVEFORM_FALLING_TRIGGER)
		{
			need_trigger = 1;
			double *trigger_data = audio_buffer[0]->get_data();
			for(int i = 1; i < buffer_size - window_size; i++)
			{
				if(trigger_data[i - 1] > config.trigger_level &&
					trigger_data[i] <= config.trigger_level)
				{
					got_trigger = 1;
					trigger_sample = i;
					break;
				}
			}
		}

//printf("AudioScope::process_buffer %d\n", __LINE__);
// Flush buffer
		if(need_trigger && !got_trigger)
		{
//printf("AudioScope::process_buffer %d\n", __LINE__);
			for(int j = 0; j < CHANNELS; j++)
			{
				double *sample_input = audio_buffer[j]->get_data();
				memcpy(sample_input,
					sample_input + buffer_size - window_size,
					sizeof(double) * window_size);
			}

			buffer_size = window_size;
			if(buffer_size == window_size) break;
//printf("AudioScope::process_buffer %d\n", __LINE__);
		}
		else
		{
			data_header_t *header = (data_header_t*)data;
//printf("AudioScope::process_buffer %d\n", __LINE__);
			for(int j = 0; j < CHANNELS; j++)
			{
				float *sample_output = header->samples +
					total_windows * CHANNELS * window_size +
					window_size * j;
				double *sample_input = audio_buffer[j]->get_data();
				for(int i = 0; i < window_size; i++)
				{
					sample_output[i] = sample_input[i + trigger_sample];
				}

// Shift accumulation buffer
//printf("AudioScope::process_buffer %d\n", __LINE__);
				memcpy(sample_input,
					sample_input + trigger_sample + window_size,
					sizeof(double) * (buffer_size - trigger_sample - window_size));
//printf("AudioScope::process_buffer %d\n", __LINE__);
			}

//printf("AudioScope::process_buffer %d\n", __LINE__);

			total_windows++;
			buffer_size -= window_size + trigger_sample;
//printf("AudioScope::process_buffer %d\n", __LINE__);
		}
	}

	data_header_t *header = (data_header_t*)data;
	header->window_size = window_size;
	header->sample_rate = sample_rate;
	header->channels = channels;
	header->total_windows = total_windows;

	send_render_gui(data,
		sizeof(data_header_t) +
			sizeof(float) * total_windows * window_size * CHANNELS);

	return 0;
}

void AudioScope::render_stop()
{
	buffer_size = 0;
	frame_buffer.remove_all_objects();
}




NEW_WINDOW_MACRO(AudioScope, AudioScopeWindow)

void AudioScope::update_gui()
{
	if(thread)
	{
		int result = load_configuration();
		AudioScopeWindow *window = (AudioScopeWindow*)thread->get_window();
		window->lock_window("AudioScope::update_gui");
		if(result) window->update_gui();

// Shift in accumulated frames
		if(frame_buffer.size())
		{
			BC_SubWindow *canvas = window->canvas;
// Frames to draw in this iteration
			int total_frames = timer->get_difference() *
				header.sample_rate /
				header.window_size /
				1000;
			if(total_frames) timer->subtract(total_frames *
				header.window_size *
				1000 /
				header.sample_rate);

// Add forced frame drawing
			for(int i = 0; i < frame_buffer.size(); i++)
				if(frame_buffer.get(i)->force) total_frames++;
			total_frames = MIN(frame_buffer.size(), total_frames);
//printf("AudioScope::update_gui %d %d\n", __LINE__, total_frames);


// expire old history frames
			int new_frames = frame_history.size() + total_frames;
			int expired = new_frames - config.history_size;
			if( expired > 0 )
				frame_history.remove_object_block(0, expired);
// Shift frames into history
			for(int frame = 0; frame < total_frames; frame++)
				frame_history.append(frame_buffer.get(frame));
			frame_buffer.remove_block(0, total_frames);

// Point probe data at history
			current_frame = frame_history.get(frame_history.size() - 1);

// Draw frames from history
			canvas->clear_box(0, 0, canvas->get_w(), canvas->get_h());
			for(int frame = 0; frame < frame_history.size(); frame++)
			{
				AudioScopeFrame *ptr = frame_history.get(frame);

				int luma = (frame + 1) * 0x80 / frame_history.size();
				if(frame == frame_history.size() - 1)
				{
					canvas->set_color(WHITE);
					canvas->set_line_width(2);
				}
				else
				{
					canvas->set_color((luma << 16) |
						(luma << 8) |
						luma);
				}

				int x1 = 0;
				int y1 = 0;
				int w = canvas->get_w();
				int h = canvas->get_h();
				float *channel0 = ptr->data[0];
				float *channel1 = ptr->data[1];

				if(config.mode == XY_MODE)
				{

					int number = 0;
					for(int point = 0; point < ptr->size; point++)
					{
						int x2 = (int)(channel0[point] * w / 2 + w / 2);
						int y2 = (int)(-channel1[point] * h / 2 + h / 2);
						CLAMP(x2, 0, w - 1);
						CLAMP(y2, 0, h - 1);

						if(number)
						{
							canvas->draw_line(x1, y1, x2, y2);
						}
						else
						{
							number++;
						}

						x1 = x2;
						y1 = y2;
					}
				}
				else
				{
					for(int channel = ptr->channels - 1; channel >= 0; channel--)
					{
						if(channel > 0)
						{
							if(frame == frame_history.size() - 1)
							{
								canvas->set_color(PINK);
								canvas->set_line_width(2);
							}
							else
								canvas->set_color(((luma * 0xff / 0xff) << 16) |
									((luma * 0x80 / 0xff) << 8) |
									((luma * 0x80 / 0xff)));
						}
						else
						{
							if(frame == frame_history.size() - 1)
								canvas->set_color(WHITE);
							else
								canvas->set_color((luma << 16) |
									(luma << 8) |
									luma);
						}

						if(ptr->size < w)
						{
							for(int point = 0; point < ptr->size; point++)
							{
								int x2 = point * w / ptr->size;
								int y2 = (int)(-ptr->data[channel][point] * h / 2 + h / 2);
								CLAMP(y2, 0, h - 1);
								if(point > 0)
								{
									canvas->draw_line(x1, y1, x2, y2);
								}

								x1 = x2;
								y1 = y2;
							}
						}
						else
						{
							for(int x2 = 0; x2 < w; x2++)
							{
								int sample1 = x2 * ptr->size / w;
								int sample2 = (x2 + 1) * ptr->size / w;
								double min = ptr->data[channel][sample1];
								double max = ptr->data[channel][sample1];
								for(int i = sample1 + 1; i < sample2; i++)
								{
									double value = ptr->data[channel][i];
									if(value < min) min = value;
									if(value > max) max = value;
								}

								int min2 = (int)(-min * h / 2 + h / 2);
								int max2 = (int)(-max * h / 2 + h / 2);
								CLAMP(min2, 0, h - 1);
								CLAMP(max2, 0, h - 1);
								int y2 = (min2 + max2) / 2;
								canvas->draw_line(x2, min2, x2, max2);
								if(x2 > 0)
								{
									canvas->draw_line(x2, y1, x2, y2);
								}

								y1 = y2;
							}
						}
					}
				}

				canvas->set_line_width(1);
			}

// Recompute probe level
			window->calculate_probe(window->probe_x, window->probe_y, 0);


// Draw overlay
			window->draw_overlay();
			canvas->flash();
		}



		while(frame_buffer.size() > MAX_COLUMNS)
			frame_buffer.remove_object_number(0);

		thread->get_window()->unlock_window();
	}
}

void AudioScope::render_gui(void *data, int size)
{
	if(thread)
	{
		thread->get_window()->lock_window("AudioScope::render_gui");
		data_header_t *header = (data_header_t*)data;
		memcpy(&this->header, header, sizeof(data_header_t));
		//BC_SubWindow *canvas = ((AudioScopeWindow*)thread->get_window())->canvas;
		//int h = canvas->get_h();

// Set all previous frames to draw immediately
		for(int i = 0; i < frame_buffer.size(); i++)
			frame_buffer.get(i)->force = 1;

		for(int current_fragment = 0;
			current_fragment < header->total_windows;
			current_fragment++)
		{
			float *in_frame = header->samples +
				current_fragment * header->window_size * CHANNELS;
			AudioScopeFrame *out_frame = new AudioScopeFrame(
				header->window_size,
				header->channels);

// Copy the window to the frame
			for(int j = 0; j < CHANNELS; j++)
			{
				for(int i = 0; i < header->window_size; i++)
				{
					out_frame->data[j][i] = in_frame[header->window_size * j + i];
				}
			}

			frame_buffer.append(out_frame);
			total_windows++;
		}

		timer->update();
		thread->get_window()->unlock_window();
	}
}








LOAD_CONFIGURATION_MACRO(AudioScope, AudioScopeConfig)

void AudioScope::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("AUDIOSCOPE"))
			{
				if(is_defaults())
				{
					w = input.tag.get_property("W", w);
					h = input.tag.get_property("H", h);
				}

				config.history_size = input.tag.get_property("HISTORY_SIZE", config.history_size);
				config.window_size = input.tag.get_property("WINDOW_SIZE", config.window_size);
				config.mode = input.tag.get_property("MODE", config.mode);
				config.trigger_level = input.tag.get_property("TRIGGER_LEVEL", config.trigger_level);
			}
		}
	}
}

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

//printf("AudioScope::save_data %d %d %d\n", __LINE__, config.w, config.h);
	output.tag.set_title("AUDIOSCOPE");
	output.tag.set_property("W", w);
	output.tag.set_property("H", h);
	output.tag.set_property("HISTORY_SIZE", config.history_size);
	output.tag.set_property("WINDOW_SIZE", (int)config.window_size);
	output.tag.set_property("MODE", (int)config.mode);
	output.tag.set_property("TRIGGER_LEVEL", (float)config.trigger_level);
	output.append_tag();
	output.tag.set_title("/AUDIOSCOPE");
	output.append_tag();
	output.append_newline();
	output.terminate_string();
}





