/*
 * 4/5/6 -> 2 downmix with a simple spacialization
 *
 * Copyright (c) 2006 by Takashi Iwai <tiwai@suse.de>
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include <alsa/asoundlib.h>
#include <alsa/pcm_external.h>

/* #define I_AM_POWERFUL */

#ifdef I_AM_POWERFUL
#define RINGBUF_SIZE	(1 << 9)
#else
#define RINGBUF_SIZE	(1 << 7)
#endif
#define RINGBUF_MASK	(RINGBUF_SIZE - 1)

struct vdownmix_tap {
	int delay;
	int weight;
};

#define MAX_TAPS	30

struct vdownmix_filter {
	int taps;
	struct vdownmix_tap tap[MAX_TAPS];
};

typedef struct {
	snd_pcm_extplug_t ext;
	int channels;
	unsigned int curpos;
	short rbuf[RINGBUF_SIZE][5];
} snd_pcm_vdownmix_t;

static const struct vdownmix_filter tap_filters[5] = {
	{
#ifdef I_AM_POWERFUL
		18,
#else
		14,
#endif
		{{ 0, 0xfffffd0a },
		 { 1, 0x41d },
		 { 2, 0xffffe657 },
		 { 3, 0x6eb5 },
		 { 4, 0xffffe657 },
		 { 5, 0x41d },
		 { 6, 0xfffffd0a },
		 { 71, 0xffffff1c },
		 { 72, 0x12e },
		 { 73, 0xfffff81a },
		 { 74, 0x24de },
		 { 75, 0xfffff81a },
		 { 76, 0x12e },
		 { 77, 0xffffff1c },
		 { 265, 0xfffffc65 },
		 { 266, 0xee1 },
		 { 267, 0xfffffc65 },
		 { 395, 0x46a }},
	},

	{
#ifdef I_AM_POWERFUL
		17,
#else
		10,
#endif
		{{ 8, 0xcf },
		 { 9, 0xa7b },
		 { 10, 0xcd7 },
		 { 11, 0x5b3 },
		 { 12, 0x859 },
		 { 13, 0xaf },
		 { 80, 0x38b },
		 { 81, 0x454 },
		 { 82, 0x218 },
		 { 83, 0x2c1 },
		 { 268, 0x58b },
		 { 275, 0xc2 },
		 { 397, 0xbd },
		 { 398, 0x1e8 },
		 { 506, 0xfffffeac },
		 { 507, 0x636 },
		 { 508, 0xfffffeac }},
	},

	{
#ifdef I_AM_POWERFUL
		11,
#else
		1,
#endif
		{{ 3, 0x4000 },
		 { 125, 0x12a },
		 { 126, 0xda1 },
		 { 127, 0x12a },
		 { 193, 0xfffffed3 },
		 { 194, 0xdb9 },
		 { 195, 0xfffffed3 },
		 { 454, 0x10a },
		 { 483, 0xfffffe97 },
		 { 484, 0x698 },
		 { 485, 0xfffffe97 }},
	},

	{
#ifdef I_AM_POWERFUL
		25,
#else
		10,
#endif
		{{ 5, 0x1cb },
		 { 6, 0x9c5 },
		 { 7, 0x117e },
		 { 8, 0x200 },
		 { 9, 0x533 },
		 { 10, 0x1c6 },
		 { 11, 0x167 },
		 { 12, 0x5ff },
		 { 13, 0x425 },
		 { 14, 0xd9 },
		 { 128, 0x247 },
		 { 129, 0x5e1 },
		 { 130, 0xb7 },
		 { 131, 0x122 },
		 { 135, 0x10a },
		 { 200, 0x1b6 },
		 { 201, 0xa7 },
		 { 202, 0x188 },
		 { 203, 0x1d9 },
		 { 445, 0xffffff44 },
		 { 446, 0x5e2 },
		 { 447, 0xffffff44 },
		 { 484, 0xffffff51 },
		 { 485, 0x449 },
		 { 486, 0xffffff51 }},
	},

	{
#ifdef I_AM_POWERFUL
		21,
#else
		7,
#endif
		{{ 0, 0xfffffdee },
		 { 1, 0x28b },
		 { 2, 0xffffed1e },
		 { 3, 0x6336 },
		 { 4, 0xffffed1e },
		 { 5, 0x28b },
		 { 6, 0xfffffdee },
		 { 51, 0xffffff2c },
		 { 52, 0x105 },
		 { 53, 0xfffff86b },
		 { 54, 0x27d9 },
		 { 55, 0xfffff86b },
		 { 56, 0x105 },
		 { 57, 0xffffff2c },
		 { 333, 0xfffffd69 },
		 { 334, 0xb2f },
		 { 335, 0xfffffd69 },
		 { 339, 0xdf },
		 { 340, 0x168 },
		 { 342, 0xa6 },
		 { 343, 0xba }},
	},
};

static const int tap_index[5][2] = {
	/* left */
	{ 0, 1 },
	/* right */
	{ 1, 0 },
	/* rear left */
	{ 2, 3 },
	/* rear right */
	{ 3, 2 },
	/* center */
	{ 4, 4 },
};

static inline void *area_addr(const snd_pcm_channel_area_t *area, snd_pcm_uframes_t offset)
{
	unsigned int bitofs = area->first + area->step * offset;
	return (char *) area->addr + bitofs / 8;
}

static inline unsigned int area_step(const snd_pcm_channel_area_t *area)
{
	return area->step / 8;
}

static snd_pcm_sframes_t
vdownmix_transfer(snd_pcm_extplug_t *ext,
		  const snd_pcm_channel_area_t *dst_areas,
		  snd_pcm_uframes_t dst_offset,
		  const snd_pcm_channel_area_t *src_areas,
		  snd_pcm_uframes_t src_offset,
		  snd_pcm_uframes_t size)
{
	snd_pcm_vdownmix_t *mix = (snd_pcm_vdownmix_t *)ext;
	short *src[mix->channels], *ptr[2];
	unsigned int src_step[mix->channels], step[2];
	int i, ch, curpos, p, idx;
	int acc[2];
	int fr;

	ptr[0] = area_addr(dst_areas, dst_offset);
	step[0] = area_step(dst_areas) / 2;
	ptr[1] = area_addr(dst_areas + 1, dst_offset);
	step[1] = area_step(dst_areas + 1) / 2;
	for (ch = 0; ch < mix->channels; ch++) {
		const snd_pcm_channel_area_t *src_area = &src_areas[ch];
		src[ch] = area_addr(src_area, src_offset);
		src_step[ch] = area_step(src_area) / 2;
	}
	curpos = mix->curpos;
	fr = size;
	while (fr--) {
		acc[0] = acc[1] = 0;
		for (ch = 0; ch < mix->channels; ch++) {
			mix->rbuf[curpos][ch] = *src[ch];
			for (idx = 0; idx < 2; idx++) {
				int f = tap_index[ch][idx];
				const struct vdownmix_filter *filter;
				filter = &tap_filters[f];
				for (i = 0; i < filter->taps; i++) {
					p = (curpos + RINGBUF_SIZE - filter->tap[i].delay)
						& RINGBUF_MASK;
					acc[idx] += mix->rbuf[p][ch] * filter->tap[i].weight;
				}
			}
			src[ch] += src_step[ch];
		}
		for (idx = 0; idx < 2; idx++) {
			acc[idx] >>= 14;
			if (acc[idx] < -32768)
				*ptr[idx] = -32768;
			else if (acc[idx] > 32767)
				*ptr[idx] = 32767;
			else
				*ptr[idx] = acc[idx];
			ptr[idx] += step[idx];
		}
		curpos = (curpos + 1) & RINGBUF_MASK;
	}
	mix->curpos = curpos;
	return size;
}


static int vdownmix_init(snd_pcm_extplug_t *ext)
{
	snd_pcm_vdownmix_t *mix = (snd_pcm_vdownmix_t *)ext;
	mix->channels = ext->channels;
	if (mix->channels > 5) /* ignore LFE */
		mix->channels = 5;
	mix->curpos = 0;
	memset(mix->rbuf, 0, sizeof(mix->rbuf));
	return 0;
}

#if SND_PCM_EXTPLUG_VERSION >= 0x10002
static unsigned int chmap[6] = {
	SND_CHMAP_FL, SND_CHMAP_FR,
	SND_CHMAP_RL, SND_CHMAP_RR,
	SND_CHMAP_FC, SND_CHMAP_LFE,
};

static snd_pcm_chmap_query_t **vdownmix_query_chmaps(snd_pcm_extplug_t *ext ATTRIBUTE_UNUSED)
{
	snd_pcm_chmap_query_t **maps;
	int i;

	maps = calloc(4, sizeof(void *));
	if (!maps)
		return NULL;
	for (i = 0; i < 3; i++) {
		snd_pcm_chmap_query_t *p;
		p = maps[i] = calloc(i + 4 + 2, sizeof(int));
		if (!p) {
			snd_pcm_free_chmaps(maps);
			return NULL;
		}
		p->type = SND_CHMAP_TYPE_FIXED;
		p->map.channels = i + 4;
		memcpy(p->map.pos, chmap, (i + 4) * sizeof(int));
	}
	return maps;
}

static snd_pcm_chmap_t *vdownmix_get_chmap(snd_pcm_extplug_t *ext)
{
	snd_pcm_chmap_t *map;

	if (ext->channels < 4 || ext->channels > 6)
		return NULL;
	map = malloc((ext->channels + 1) * sizeof(int));
	if (!map)
		return NULL;
	map->channels = ext->channels;
	memcpy(map->pos, chmap, ext->channels * sizeof(int));
	return map;
}
#endif /* SND_PCM_EXTPLUG_VERSION >= 0x10002 */

static const snd_pcm_extplug_callback_t vdownmix_callback = {
	.transfer = vdownmix_transfer,
	.init = vdownmix_init,
	/* .dump = filr_dump, */
#if SND_PCM_EXTPLUG_VERSION >= 0x10002
	.query_chmaps = vdownmix_query_chmaps,
	.get_chmap = vdownmix_get_chmap,
#endif
};

SND_PCM_PLUGIN_DEFINE_FUNC(vdownmix)
{
	snd_config_iterator_t i, next;
	snd_pcm_vdownmix_t *mix;
	snd_config_t *sconf = NULL;
	int err;

	snd_config_for_each(i, next, conf) {
		snd_config_t *n = snd_config_iterator_entry(i);
		const char *id;
		if (snd_config_get_id(n, &id) < 0)
			continue;
		if (strcmp(id, "comment") == 0 || strcmp(id, "type") == 0 || strcmp(id, "hint") == 0)
			continue;
		if (strcmp(id, "slave") == 0) {
			sconf = n;
			continue;
		}
		SNDERR("Unknown field %s", id);
		return -EINVAL;
	}

	if (! sconf) {
		SNDERR("No slave configuration for vdownmix pcm");
		return -EINVAL;
	}

	mix = calloc(1, sizeof(*mix));
	if (mix == NULL)
		return -ENOMEM;

	mix->ext.version = SND_PCM_EXTPLUG_VERSION;
	mix->ext.name = "Vdownmix Plugin";
	mix->ext.callback = &vdownmix_callback;
	mix->ext.private_data = mix;

	err = snd_pcm_extplug_create(&mix->ext, name, root, sconf, stream, mode);
	if (err < 0) {
		free(mix);
		return err;
	}

	/* 4/5/6 -> 2 downmix */
	snd_pcm_extplug_set_param_minmax(&mix->ext, SND_PCM_EXTPLUG_HW_CHANNELS,
					 4, 6);
	snd_pcm_extplug_set_slave_param(&mix->ext, SND_PCM_EXTPLUG_HW_CHANNELS, 2);
	snd_pcm_extplug_set_param(&mix->ext, SND_PCM_EXTPLUG_HW_FORMAT,
				  SND_PCM_FORMAT_S16);
	snd_pcm_extplug_set_slave_param(&mix->ext, SND_PCM_EXTPLUG_HW_FORMAT,
					SND_PCM_FORMAT_S16);

	*pcmp = mix->ext.pcm;
	return 0;
}

SND_PCM_PLUGIN_SYMBOL(vdownmix);
