/*  Audacious-DUMB Plug-in to enable audacious to use DUMB for IT/XM/S3M/MOD files
 *  Copyright (C) 2006-2011 Christian Birchinger
 *  Audacious port based on previous plugins for XMMS and BMP
 *  Copyright (C) 2002-2003  Ben Davis
 *  Incorporates code from the wav plug-in,
 *  Copyright (C) 1998-2000  Peter Alm, Mikael Alm, Olle Hallnas, Thomas Nilsson and 4Front Technologies
 *
 *  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 "audacious-dumb.h"

#include <gdk/gdkkeysyms.h>
#include <gtk/gtk.h>
#include <audacious/i18n.h>

DuhFile *duh_file = NULL;
static GMutex *dumblock;

dumb_config_t dumb_config;
dumb_config_t dumb_config_gui;

static GMutex *control_mutex;
static GCond *control_cond;
static gboolean stop_flag = FALSE;

static const gchar *fmts[] = { "mod", "s3m", "it", "xm", "duh", NULL };

static const gchar * const DUMB_defaults[] = {
	"disable_amiga_mods", "FALSE",
	"output_frequency", "44100",
	"use_custom_title", "TRUE",
	NULL};

AUD_INPUT_PLUGIN
(
	.name = N_("DUMB Plugin " VERSION),
	.domain = PACKAGE,
	.init = duh_init,
	.cleanup = duh_cleanup,
	.about = duh_about,
	.prefs = &preferences,
	.play = play_start,
	.stop = stop,
	.pause = duh_pause,
	.file_info_box = file_info_box,
	.is_our_file_from_vfs = is_our_file_from_vfs,
	.extensions = fmts,
	.mseek = mseek,
	.probe_for_tuple = duh_probe_for_tuple,
	.priority = 0 /* Would be nice to be above Modplug but minus values don't work */ 
)

static gboolean duh_init(void)
{
	aud_config_set_defaults ("DUMB", DUMB_defaults);

	memset(&dumb_config, 0, sizeof(dumb_config));

	dumb_config.disable_amiga_mods = aud_get_bool("DUMB", "disable_amiga_mods");

	dumb_config.output_frequency = aud_get_int("DUMB", "output_frequency");

	if (dumb_config.output_frequency < 22050 || dumb_config.output_frequency > 192000) {
		dumb_config.output_frequency = 44100;
		aud_set_int ("DUMB", "output_frequency", dumb_config.output_frequency);
	}

	dumb_config.use_custom_title = aud_get_bool("DUMB", "use_custom_title");

	/* dumb_register_stdfiles(); */
	/*  Seems only needed when opening files with dumb directly * 
	 *  and not when VFS puts them to a memory location first   */
	
	dumb_it_max_to_mix = 256;
	
	dumblock = g_mutex_new();
	control_mutex = g_mutex_new();
	control_cond = g_cond_new();
	
	return TRUE;
}

static void duh_cleanup(void)
{
	g_mutex_free(dumblock);
	g_mutex_free(control_mutex);
	g_cond_free(control_cond);
}	

static int is_our_file_from_vfs(const gchar *filename, VFSFile *file)
{
	gchar magic[4];
	const gchar *ext;

	g_return_val_if_fail(filename != NULL, FALSE);
	g_return_val_if_fail(file != NULL, FALSE);

	if (vfs_fread(magic, 1, 4, file) != sizeof magic)
		return FALSE;
	if (!memcmp(magic, XM_MAGIC, 4))
                return TRUE;
	if (!memcmp(magic, IT_MAGIC, 4))
		return TRUE;

	if (vfs_fseek(file, 44, SEEK_SET))
		return FALSE;
	if (vfs_fread(magic, 1, 4, file) != sizeof magic)
		return FALSE;
	if (!memcmp(magic, S3M_MAGIC, 4))
		return TRUE;

	if (vfs_fseek(file, 1080, SEEK_SET))
		return FALSE;
	if (vfs_fread(magic, 1, 4, file) != sizeof magic)
		return FALSE;
	if (!memcmp(magic, MOD_MAGIC_FASTTRACKER6, 4))
		return TRUE;
	if (!memcmp(magic, MOD_MAGIC_FASTTRACKER8, 4))
		return TRUE;

/*
	printf("Is %s our file?\n", filename);

	if (disable_amiga_mods) {
		printf("Amiga support is disabled\n");
	}
*/
	if (!dumb_config.disable_amiga_mods) {
		if (!memcmp(magic, MOD_MAGIC_PROTRACKER4, 4))
			return TRUE;
		if (!memcmp(magic, MOD_MAGIC_PROTRACKER4X, 4))
			return TRUE;
		if (!memcmp(magic, MOD_MAGIC_NOISETRACKER, 4))
			return TRUE;
		if (!memcmp(magic, MOD_MAGIC_STARTRACKER4, 4))
			return TRUE;
		if (!memcmp(magic, MOD_MAGIC_STARTRACKER8, 4))
			return TRUE;
		if (!memcmp(magic, MOD_MAGIC_STARTRACKER4X, 4))
			return TRUE;
		if (!memcmp(magic, MOD_MAGIC_STARTRACKER8X, 4))
			return TRUE;
		if (!memcmp(magic, MOD_MAGIC_FASTTRACKER4, 4))
			return TRUE;
	}

	ext = strrchr(filename, '.');
	if (ext) {
		if (!strcasecmp(ext, ".duh")) return TRUE;
		if (!strcasecmp(ext, ".it")) return TRUE;
		if (!strcasecmp(ext, ".xm")) return TRUE;
		if (!strcasecmp(ext, ".s3m")) return TRUE;
		if (!dumb_config.disable_amiga_mods) {
			if (!strcasecmp(ext, ".mod")) return TRUE;
		}
	}

	return FALSE;
}

static gint duh_universal_load_vfs(DUH **duh, const char *filename, VFSFile *extfd, const short testmask)
{
	DUMBFILE *filedumb;

	gint ext_index = -1;
	VFSFile *fd = NULL;
	off_t filesize, readsize;
	gchar *filemap;
	
	*duh = NULL;

	if (extfd)
		fd = extfd;
	else {
		fd = vfs_fopen(filename, "rb");
		g_return_val_if_fail(fd != NULL, -1);
	}

	filesize = vfs_fsize(fd);
	g_return_val_if_fail(filesize > 0, -1);
	
	filemap = malloc(filesize);
	g_return_val_if_fail(filemap != NULL, -1);

	readsize = vfs_fread(filemap, 1, filesize, fd);

	if (!extfd)
		vfs_fclose(fd);

	if (readsize == 0) {
		g_warning("audacious-dumb: Couldn't read from %s", filename);
		free(filemap); filemap = NULL;
		return -1;
	}

	/*
	   The close and re-opening after reach read attempt is needed.

	   Quote from the documentation:

	   WARNING: The behaviour of these functions are undefined if you pass a
	   DUMBFILE from which data have already been read; they are likely
	   not to work. This oversight will be fixed in future releases.
	*/

	int count, total_count = sizeof(uniread) / sizeof(uniread_struct_t);

	for (count = 0; count < total_count; count++) {
		if (uniread[count].testmask & testmask) {
			g_mutex_lock(dumblock);
			filedumb = dumbfile_open_memory(filemap, readsize);
			*duh = uniread[count].read(filedumb);
			dumbfile_close(filedumb);
			g_mutex_unlock(dumblock);
			if (*duh) {
				ext_index = count;
				break;
			}
		}
	}

	free(filemap); filemap = NULL;

	return ext_index;
}

static Tuple *get_tuple_info_from_duh(DUH *duh, const gchar *filename)
{
	const gchar *mod_title_raw;
	gchar *mod_title;

	g_return_val_if_fail(filename != NULL, NULL);
	g_return_val_if_fail(duh != NULL, NULL);

	Tuple *tuple = tuple_new_from_filename(filename);
	tuple_set_str(tuple, FIELD_QUALITY, NULL, "sequenced");
	tuple_set_int(tuple, FIELD_LENGTH, NULL, (int)(duh_get_length(duh) * 1000LL >> 16) );
	
	mod_title_raw = duh_get_tag(duh, "TITLE");
	if (mod_title_raw) {
		/* DOS (CP850) all my XM IT and S3M use this (MODs should be 7bit ASCII) */ 
		mod_title = g_convert(mod_title_raw, -1, "UTF-8", "CP850", NULL, NULL, NULL);
		/* Raw copy but GTK2 doesn't like none-UTF8 stuff
		   mod_title = g_strdup(mod_title_raw); */
		g_strstrip(mod_title);

		if (mod_title[0]) {
			if (dumb_config.use_custom_title) {
				gchar *custom_title, *file_title;
				const char *basename, *basename_ext;

				uri_parse(filename, &basename, &basename_ext, NULL, NULL);
				file_title = g_strndup(basename, basename_ext - basename);
				custom_title = g_strdup_printf("%s (%s)", file_title, mod_title);
				tuple_set_str(tuple, FIELD_TITLE, NULL, custom_title );

				g_free(custom_title);
				g_free(file_title); 
			} else
				tuple_set_str(tuple, FIELD_TITLE, NULL, mod_title);

		}

		g_free(mod_title); mod_title = NULL;
	}
	mod_title_raw = NULL;

	return tuple;
}

static Tuple *duh_probe_for_tuple(const gchar *filename, VFSFile *fd)
{
	Tuple *input = NULL;
	DUH *duh = NULL;
	int ext_index;

	g_return_val_if_fail(filename != NULL, NULL);
	g_return_val_if_fail(fd != NULL, NULL);

	if (!is_our_file_from_vfs(filename, fd))
		return NULL;
		
	vfs_rewind(fd);

	ext_index = duh_universal_load_vfs(&duh, filename, fd, UNIREAD_ALL);
	if (ext_index == -1)
		return NULL; /* Return empty Tuple if module was not loadable */
	
	input = get_tuple_info_from_duh(duh, filename);
	tuple_set_str(input, FIELD_CODEC, NULL, uniread[ext_index].description);	
		
	return input;
}

static void install_callbacks(DUH_SIGRENDERER *sr)
{
	DUMB_IT_SIGRENDERER *itsr = duh_get_it_sigrenderer(sr);
	dumb_it_set_loop_callback(itsr, &dumb_it_callback_terminate, NULL);
	dumb_it_set_xm_speed_zero_callback(itsr, &dumb_it_callback_terminate, NULL);
}

static gboolean play_start(InputPlayback *playback, const gchar *filename,
 VFSFile *file, gint start_time, gint stop_time, gboolean pause)
{
	Tuple *tuple;
	int rate;

	const int buffer_size = 16 * 1024;
	char *buffer;
	int actual_read, render_size;
	float delta, volume;

	if (file == NULL)
		return FALSE;

	duh_file = g_malloc(sizeof (DuhFile));
	memset(duh_file, 0, sizeof (DuhFile));

	duh_universal_load_vfs(&duh_file->duh, filename, file, UNIREAD_ALL);
	if (!duh_file->duh) {
		g_warning("audacious-dumb: Unable to play %s", filename);
		g_free(duh_file);
		duh_file = NULL;
		return FALSE;
	}

	if ((duh_file->file = duh_start_sigrenderer(duh_file->duh, 0, 2, 0)))
	{
		install_callbacks(duh_file->file);
		duh_file->channels = 2;
		duh_file->samples_per_sec = dumb_config.output_frequency;
		duh_file->bits_per_sample = 16;

		if (playback->output->open_audio((duh_file->bits_per_sample == 16) ? FMT_S16_LE : FMT_U8, duh_file->samples_per_sec, duh_file->channels) == 0)
		{
			duh_end_sigrenderer(duh_file->file);
			unload_duh(duh_file->duh);
			g_free(duh_file);
			duh_file = NULL;
			return FALSE;
		}

		if (pause)
			playback->output->pause(TRUE);
		
		rate = duh_file->samples_per_sec * duh_file->channels * (duh_file->bits_per_sample / 8);
		tuple = get_tuple_info_from_duh(duh_file->duh, filename);
		playback->set_tuple(playback, tuple);
		playback->set_params(playback, 8 * rate, duh_file->samples_per_sec, duh_file->channels);

		g_mutex_lock(control_mutex);
		duh_file->seek_to = (start_time > 0) ? start_time : -1;
		stop_flag = FALSE;
		playback->set_pb_ready(playback);
		g_mutex_unlock(control_mutex);

		/* Values taken from the dumbout.c example */
		delta = 65536.0f / duh_file->samples_per_sec;
		volume = 1.0f;

		buffer = g_malloc(buffer_size);

		/* The docs say output buffer equals render_size * channels (2)
		 * but it seems not enough so we're keeping the old calculated value (4)
		 */
		render_size = buffer_size / ((duh_file->bits_per_sample / 8) * duh_file->channels);

		while (!stop_flag)
		{
			g_mutex_lock(control_mutex);
			if (duh_file->seek_to != -1)
			{
				duh_end_sigrenderer(duh_file->file);
				duh_file->file = duh_start_sigrenderer(duh_file->duh, 0, duh_file->channels, (unsigned long)((gint64)duh_file->seek_to << 16) / 1000L);
				install_callbacks(duh_file->file);
				playback->output->flush(duh_file->seek_to); 
				duh_file->seek_to = -1;
				g_cond_signal(control_cond);
			}

			g_mutex_unlock(control_mutex);

			if (!duh_file->eof)
			{
				actual_read = duh_render(duh_file->file, duh_file->bits_per_sample, 0, volume, delta, render_size, buffer);
				actual_read *= (duh_file->bits_per_sample / 8) * duh_file->channels;
	
				if (actual_read == 0)
					duh_file->eof = TRUE;
				else
				{
					if (!stop_flag && duh_file->seek_to == -1)
						playback->output->write_audio(buffer, actual_read);
				}
			}
			else
				break;
		}

		g_mutex_lock(control_mutex);
		stop_flag = TRUE;
		duh_file->eof = TRUE;
		g_mutex_unlock(control_mutex);

		duh_end_sigrenderer(duh_file->file);
		unload_duh(duh_file->duh);
		g_free(buffer);
		g_free(duh_file);
		duh_file = NULL;

		return TRUE;
	}
	return FALSE;
}

static void stop(InputPlayback *playback)
{
	g_mutex_lock(control_mutex);

	if (!stop_flag)
	{
		stop_flag = TRUE;
		playback->output->abort_write();
		g_cond_signal(control_cond);
	}

	g_mutex_unlock (control_mutex);
}

static void duh_pause(InputPlayback *playback, gboolean pause)
{
	g_mutex_lock(control_mutex);

	if (!stop_flag)
		playback->output->pause(pause);
            
	g_mutex_unlock(control_mutex);
}

static void mseek(InputPlayback *playback, gint time)
{
	g_mutex_lock(control_mutex);

	if (!stop_flag) {
		duh_file->seek_to = time;
		duh_file->eof = FALSE;
	        g_cond_signal(control_cond);
                g_cond_wait(control_cond, control_mutex);
	}
	
	g_mutex_unlock(control_mutex);
}

static void duh_about(void) {
	static GtkWidget *about;
	gchar *about_text;
	
	if (about)
		return;

        about_text = g_strjoin("", _("DUMB Input Plugin "), VERSION,
				   _("\n"
				     "by Christian Birchinger <joker@netswarm.net>\n"
				     "\n"
				     "Based on the original XMMS plugin by Ben Davis\n"
				     "and the BMP port by Michael Doering\n"
				     "\n"
				     "Built with DUMB version "), DUMB_VERSION_STR, NULL);
                                                                                
	audgui_simple_message (& about, GTK_MESSAGE_INFO, "About DUMB Plugin", about_text);

	g_free(about_text);
}

/* Preferences Window */ 

static void
configure_init(void)
{
	memcpy(&dumb_config_gui, &dumb_config, sizeof(dumb_config_gui));
}

static void
configure_cleanup(void)
{
}

static void
configure_apply()
{
	memcpy(&dumb_config, &dumb_config_gui, sizeof(dumb_config));

	aud_set_bool("DUMB", "disable_amiga_mods", dumb_config.disable_amiga_mods);
	aud_set_int("DUMB", "output_frequency", dumb_config.output_frequency);
	aud_set_bool("DUMB", "use_custom_title", dumb_config.use_custom_title);
}

static ComboBoxElements frequency_elements[] = {
	{ GINT_TO_POINTER(22050), "22050" },
	{ GINT_TO_POINTER(44100), "44100" },
	{ GINT_TO_POINTER(48000), "48000" },
	{ GINT_TO_POINTER(64000), "64000" },
	{ GINT_TO_POINTER(96000), "96000" },
	{ GINT_TO_POINTER(192000), "192000" },
};

static PreferencesWidget settings[] = {
	{WIDGET_CHK_BTN, N_("Disable Amiga Support"),
		.cfg_type = VALUE_BOOLEAN,
		.cfg = &dumb_config_gui.disable_amiga_mods,
		.tooltip = N_("Use this if you have a better decoder for 4 "
		              "channel Amiga MOD files (like UADE)"),
	},
	{WIDGET_COMBO_BOX, N_("Frequency:"),
		.cfg_type = VALUE_INT,
		.cfg = &dumb_config_gui.output_frequency,
		.data = {.combo = {frequency_elements, G_N_ELEMENTS (frequency_elements)}},
		.tooltip = N_("If you use dmix, pulseaudio or a similar stream "
		              "mixer, choose the same frequency here to avoid "
			      "useless resampling, otherwise pick whatever quality "
			      "you like"),
	},
	{WIDGET_SEPARATOR, .data = {.separator = {TRUE}}},
	{WIDGET_CHK_BTN, N_("Use \"Filename (Real Mod Title)\" as title"),
		.cfg_type = VALUE_BOOLEAN,
		.cfg = &dumb_config_gui.use_custom_title,
		.tooltip = N_("Recommended because many MOD titles are "
		              "stylized, contain trailing or leading spaces or even "
		              "ASCII art which is very bad for playlist sorting"),
	},
	{WIDGET_SEPARATOR, .data = {.separator = {TRUE}}},
};

static PluginPreferences preferences = {
	.widgets = settings,
	.n_widgets = G_N_ELEMENTS(settings),
	.init = configure_init,
	.apply = configure_apply,
	.cleanup = configure_cleanup,
};

static void close_window(GtkWidget *widget, gpointer win) {
	gtk_widget_destroy(GTK_WIDGET(win));
}

static gboolean escape_close(GtkWidget *widget, GdkEventKey *event, gpointer data) {
	if (event->keyval == GDK_KEY_Escape) {
		gtk_widget_destroy(GTK_WIDGET(widget));
		return TRUE;
	}
	return FALSE;
}

static void file_info_box(const gchar *filename) {
	GtkWidget *window;

	GtkWidget *window_vbox;
	GtkWidget *notebook;

	GtkWidget *label1, *label2, *label3;
	GtkWidget *child1, *child2, *child3;
	GtkWidget *child1_scroll, *child2_scroll, *child3_scroll;

	GtkWidget *close_hbox, *close_button;

	GtkTextIter child1_iter;
	GtkTextBuffer *child1_buffer;

	GtkTreeIter child2_iter, child3_iter;
	GtkListStore *child2_store, *child3_store;

	PangoFontDescription *font_desc;

	const gchar *message, *basename;
	gchar *msg_clean, *window_title;
	int i, n, ext_index;

	DUMB_IT_SIGDATA *sd;
	DUH *duh;

	ext_index = duh_universal_load_vfs(&duh, filename, NULL, UNIREAD_MOD);
	if (ext_index == -1)
		return;

	sd = duh_get_it_sigdata(duh);

	window = gtk_window_new (GTK_WINDOW_TOPLEVEL);

	uri_parse(filename, &basename, NULL, NULL, NULL);
	window_title = g_strdup_printf("Track Information - %s (%s)", basename, uniread[ext_index].description);
	gtk_window_set_title (GTK_WINDOW (window), window_title);
	g_free(window_title);

	gtk_container_set_border_width (GTK_CONTAINER (window), 10);
	gtk_widget_set_size_request (window, 640, 480);
	g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(close_window), window);
	g_signal_connect(G_OBJECT(window), "key-press-event", G_CALLBACK(escape_close), NULL);
	window_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
	gtk_container_add (GTK_CONTAINER (window), window_vbox);

	font_desc = pango_font_description_from_string ("monospace");

	/* ========= content child widgets for the 3 tabs =========*/

	notebook = gtk_notebook_new();
	gtk_box_pack_start(GTK_BOX(window_vbox), notebook, TRUE, TRUE, 4);

	/* --- tab 1 content (text view) --- */
	message = (gchar*) dumb_it_sd_get_song_message(sd);
	if (message) {
		child1_scroll = gtk_scrolled_window_new(NULL, NULL);
		gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(child1_scroll), GTK_POLICY_NEVER, GTK_POLICY_ALWAYS );
		child1 = gtk_text_view_new();
		gtk_text_view_set_editable(GTK_TEXT_VIEW(child1), FALSE);
		gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(child1), FALSE);
		gtk_container_add(GTK_CONTAINER(child1_scroll), child1);

		child1_buffer = gtk_text_view_get_buffer( GTK_TEXT_VIEW(child1) );
		gtk_text_buffer_get_iter_at_offset(child1_buffer, &child1_iter, 0);

		gtk_text_buffer_create_tag(child1_buffer, "lmarg", "left_margin", 5, NULL);

		/* add message textview content */

		/* msg_clean = g_strdup(message); */
		msg_clean = g_convert(message, -1, "UTF-8", "CP850", NULL, NULL, NULL);
		for (i = 0; msg_clean[i]; i++)
			if (msg_clean[i] == '\r') msg_clean[i] = '\n';

		gtk_text_buffer_insert_with_tags_by_name(child1_buffer, &child1_iter,
				msg_clean, -1, "lmarg",  NULL);

		g_free(msg_clean);

		gtk_widget_modify_font(child1, font_desc);

		label1 = gtk_label_new("Message");
		gtk_notebook_append_page(GTK_NOTEBOOK (notebook), child1_scroll, label1);
	}

	/* --- tab 2 content (tree view) --- */

	n = dumb_it_sd_get_n_samples(sd);

	if (n) {
		child2_scroll = gtk_scrolled_window_new(NULL, NULL);
		gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(child2_scroll), GTK_POLICY_NEVER, GTK_POLICY_ALWAYS );
		child2 = gtk_tree_view_new();
		gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(child2), TRUE);
		gtk_container_add(GTK_CONTAINER(child2_scroll), child2);

		gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW(child2), -1,
				"#",
				gtk_cell_renderer_text_new(),
				"text", COL_N, NULL);

		gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW(child2), -1,
				"Sample Name",
				gtk_cell_renderer_text_new(),
				"text", COL_NAME, NULL);

		gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW(child2), -1,
				"File Name",
				gtk_cell_renderer_text_new(),
				"text", COL_FILE, NULL);

		child2_store = gtk_list_store_new(NUM_COLS, G_TYPE_UINT, G_TYPE_STRING, G_TYPE_STRING);

		for (i = 0; i < n; i++) {
			gtk_list_store_append(child2_store, &child2_iter);
			gtk_list_store_set(child2_store, &child2_iter,
					COL_N, i+1,
					COL_NAME, (gchar*) dumb_it_sd_get_sample_name(sd, i),
					COL_FILE, (gchar*) dumb_it_sd_get_sample_filename(sd, i),
					-1);
		}
		gtk_tree_view_set_model(GTK_TREE_VIEW(child2), GTK_TREE_MODEL(child2_store));

		gtk_widget_modify_font(child2, font_desc);

		label2 = gtk_label_new("Samples");
		gtk_notebook_append_page(GTK_NOTEBOOK (notebook), child2_scroll, label2);
	}
	/* --- tab 3 content (tree view) --- */

	n = dumb_it_sd_get_n_instruments(sd);

	if (n) {
		child3_scroll = gtk_scrolled_window_new(NULL, NULL);
		gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(child3_scroll), GTK_POLICY_NEVER, GTK_POLICY_ALWAYS );
		child3 = gtk_tree_view_new();
		gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(child3), TRUE);
		gtk_container_add(GTK_CONTAINER(child3_scroll), child3);

		gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW(child3), -1,
				"#",
				gtk_cell_renderer_text_new(),
				"text", COL_N, NULL);

		gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW(child3), -1,
				"Instrument Name",
				gtk_cell_renderer_text_new(),
				"text", COL_NAME, NULL);

		gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW(child3), -1,
				"File Name",
				gtk_cell_renderer_text_new(),
				"text", COL_FILE, NULL);

		child3_store = gtk_list_store_new(NUM_COLS, G_TYPE_UINT, G_TYPE_STRING, G_TYPE_STRING);

		for (i = 0; i < n; i++) {
			gtk_list_store_append(child3_store, &child3_iter);
			gtk_list_store_set(child3_store, &child3_iter,
					COL_N, i+1,
					COL_NAME, (gchar*) dumb_it_sd_get_instrument_name(sd, i),
					COL_FILE, (gchar*) dumb_it_sd_get_instrument_filename(sd, i),
					-1);
		}

		gtk_tree_view_set_model(GTK_TREE_VIEW(child3), GTK_TREE_MODEL(child3_store));

		gtk_widget_modify_font(child3, font_desc);

		label3 = gtk_label_new("Instrument");
		gtk_notebook_append_page(GTK_NOTEBOOK (notebook), child3_scroll, label3);
	}

	/* close button within hbox */
	close_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
	close_button = gtk_button_new_from_stock(GTK_STOCK_CLOSE);
	gtk_box_pack_end(GTK_BOX(close_hbox), close_button, FALSE, FALSE, 6);
	g_signal_connect(G_OBJECT(close_button), "clicked", G_CALLBACK(close_window), window);

	gtk_box_pack_start(GTK_BOX(window_vbox), close_hbox, FALSE, FALSE, 2);

	/* */

	if (duh)
		unload_duh(duh);

	gtk_widget_show_all(window);
}
