[ALSA] Add PC-speaker sound driver

Added PC-speaker sound driver (snd-pcsp).

Signed-off-by: Stas Sergeev <stsp@aknet.ru>
Signed-off-by: Takashi Iwai <tiwai@suse.de>
diff --git a/sound/drivers/pcsp/Makefile b/sound/drivers/pcsp/Makefile
new file mode 100644
index 0000000..b19555b
--- /dev/null
+++ b/sound/drivers/pcsp/Makefile
@@ -0,0 +1,2 @@
+snd-pcsp-objs := pcsp.o pcsp_lib.o pcsp_mixer.o pcsp_input.o
+obj-$(CONFIG_SND_PCSP) += snd-pcsp.o
diff --git a/sound/drivers/pcsp/pcsp.c b/sound/drivers/pcsp/pcsp.c
new file mode 100644
index 0000000..3447728
--- /dev/null
+++ b/sound/drivers/pcsp/pcsp.c
@@ -0,0 +1,241 @@
+/*
+ * PC-Speaker driver for Linux
+ *
+ * Copyright (C) 1997-2001  David Woodhouse
+ * Copyright (C) 2001-2008  Stas Sergeev
+ */
+
+#include <linux/init.h>
+#include <linux/moduleparam.h>
+#include <linux/platform_device.h>
+#include <sound/core.h>
+#include <sound/initval.h>
+#include <sound/pcm.h>
+
+#include <linux/input.h>
+#include <linux/delay.h>
+#include <asm/bitops.h>
+#include "pcsp_input.h"
+#include "pcsp.h"
+
+MODULE_AUTHOR("Stas Sergeev <stsp@users.sourceforge.net>");
+MODULE_DESCRIPTION("PC-Speaker driver");
+MODULE_LICENSE("GPL");
+MODULE_SUPPORTED_DEVICE("{{PC-Speaker, pcsp}}");
+MODULE_ALIAS("platform:pcspkr");
+
+static int index = SNDRV_DEFAULT_IDX1;	/* Index 0-MAX */
+static char *id = SNDRV_DEFAULT_STR1;	/* ID for this card */
+static int enable = SNDRV_DEFAULT_ENABLE1;	/* Enable this card */
+
+module_param(index, int, 0444);
+MODULE_PARM_DESC(index, "Index value for pcsp soundcard.");
+module_param(id, charp, 0444);
+MODULE_PARM_DESC(id, "ID string for pcsp soundcard.");
+module_param(enable, bool, 0444);
+MODULE_PARM_DESC(enable, "dummy");
+
+struct snd_pcsp pcsp_chip;
+
+static int __devinit snd_pcsp_create(struct snd_card *card)
+{
+	static struct snd_device_ops ops = { };
+	struct timespec tp;
+	int err;
+	int div, min_div, order;
+
+	hrtimer_get_res(CLOCK_MONOTONIC, &tp);
+	if (tp.tv_sec || tp.tv_nsec > PCSP_MAX_PERIOD_NS) {
+		printk(KERN_ERR "PCSP: Timer resolution is not sufficient "
+		       "(%linS)\n", tp.tv_nsec);
+		printk(KERN_ERR "PCSP: Make sure you have HPET and ACPI "
+		       "enabled.\n");
+		return -EIO;
+	}
+
+	if (loops_per_jiffy >= PCSP_MIN_LPJ && tp.tv_nsec <= PCSP_MIN_PERIOD_NS)
+		min_div = MIN_DIV;
+	else
+		min_div = MAX_DIV;
+#if PCSP_DEBUG
+	printk("PCSP: lpj=%li, min_div=%i, res=%li\n",
+	       loops_per_jiffy, min_div, tp.tv_nsec);
+#endif
+
+	div = MAX_DIV / min_div;
+	order = fls(div) - 1;
+
+	pcsp_chip.max_treble = min(order, PCSP_MAX_TREBLE);
+	pcsp_chip.treble = min(pcsp_chip.max_treble, PCSP_DEFAULT_TREBLE);
+	pcsp_chip.playback_ptr = 0;
+	pcsp_chip.period_ptr = 0;
+	atomic_set(&pcsp_chip.timer_active, 0);
+	pcsp_chip.enable = 1;
+	pcsp_chip.pcspkr = 1;
+
+	spin_lock_init(&pcsp_chip.substream_lock);
+
+	pcsp_chip.card = card;
+	pcsp_chip.port = 0x61;
+	pcsp_chip.irq = -1;
+	pcsp_chip.dma = -1;
+
+	/* Register device */
+	err = snd_device_new(card, SNDRV_DEV_LOWLEVEL, &pcsp_chip, &ops);
+	if (err < 0)
+		return err;
+
+	return 0;
+}
+
+static int __devinit snd_card_pcsp_probe(int devnum, struct device *dev)
+{
+	struct snd_card *card;
+	int err;
+
+	if (devnum != 0)
+		return -EINVAL;
+
+	hrtimer_init(&pcsp_chip.timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
+	pcsp_chip.timer.cb_mode = HRTIMER_CB_IRQSAFE;
+	pcsp_chip.timer.function = pcsp_do_timer;
+
+	card = snd_card_new(index, id, THIS_MODULE, 0);
+	if (!card)
+		return -ENOMEM;
+
+	err = snd_pcsp_create(card);
+	if (err < 0) {
+		snd_card_free(card);
+		return err;
+	}
+	err = snd_pcsp_new_pcm(&pcsp_chip);
+	if (err < 0) {
+		snd_card_free(card);
+		return err;
+	}
+	err = snd_pcsp_new_mixer(&pcsp_chip);
+	if (err < 0) {
+		snd_card_free(card);
+		return err;
+	}
+
+	snd_card_set_dev(pcsp_chip.card, dev);
+
+	strcpy(card->driver, "PC-Speaker");
+	strcpy(card->shortname, "pcsp");
+	sprintf(card->longname, "Internal PC-Speaker at port 0x%x",
+		pcsp_chip.port);
+
+	err = snd_card_register(card);
+	if (err < 0) {
+		snd_card_free(card);
+		return err;
+	}
+
+	return 0;
+}
+
+static int __devinit alsa_card_pcsp_init(struct device *dev)
+{
+	int devnum = 0, cards = 0;
+
+#ifdef CONFIG_DEBUG_PAGEALLOC
+	/* Well, CONFIG_DEBUG_PAGEALLOC makes the sound horrible. Lets alert */
+	printk(KERN_WARNING
+	       "PCSP: Warning, CONFIG_DEBUG_PAGEALLOC is enabled!\n"
+	       "You have to disable it if you want to use the PC-Speaker "
+	       "driver.\n"
+	       "Unless it is disabled, enjoy the horrible, distorted "
+	       "and crackling noise.\n");
+#endif
+
+	if (enable) {
+		if (snd_card_pcsp_probe(devnum, dev) >= 0)
+			cards++;
+		if (!cards) {
+			printk(KERN_ERR "PC-Speaker initialization failed.\n");
+			return -ENODEV;
+		}
+	}
+
+	return 0;
+}
+
+static void __devexit alsa_card_pcsp_exit(struct snd_pcsp *chip)
+{
+	snd_card_free(chip->card);
+}
+
+static int __devinit pcsp_probe(struct platform_device *dev)
+{
+	int err;
+	err = pcspkr_input_init(&pcsp_chip.input_dev, &dev->dev);
+	if (err < 0)
+		return err;
+
+	err = alsa_card_pcsp_init(&dev->dev);
+	if (err < 0) {
+		pcspkr_input_remove(pcsp_chip.input_dev);
+		return err;
+	}
+
+	platform_set_drvdata(dev, &pcsp_chip);
+	return 0;
+}
+
+static int __devexit pcsp_remove(struct platform_device *dev)
+{
+	struct snd_pcsp *chip = platform_get_drvdata(dev);
+	alsa_card_pcsp_exit(chip);
+	pcspkr_input_remove(chip->input_dev);
+	platform_set_drvdata(dev, NULL);
+	return 0;
+}
+
+static void pcsp_stop_beep(struct snd_pcsp *chip)
+{
+	unsigned long flags;
+	spin_lock_irqsave(&chip->substream_lock, flags);
+	if (!chip->playback_substream)
+		pcspkr_stop_sound();
+	spin_unlock_irqrestore(&chip->substream_lock, flags);
+}
+
+static int pcsp_suspend(struct platform_device *dev, pm_message_t state)
+{
+	struct snd_pcsp *chip = platform_get_drvdata(dev);
+	pcsp_stop_beep(chip);
+	snd_pcm_suspend_all(chip->pcm);
+	return 0;
+}
+
+static void pcsp_shutdown(struct platform_device *dev)
+{
+	struct snd_pcsp *chip = platform_get_drvdata(dev);
+	pcsp_stop_beep(chip);
+}
+
+static struct platform_driver pcsp_platform_driver = {
+	.driver		= {
+		.name	= "pcspkr",
+		.owner	= THIS_MODULE,
+	},
+	.probe		= pcsp_probe,
+	.remove		= __devexit_p(pcsp_remove),
+	.suspend	= pcsp_suspend,
+	.shutdown	= pcsp_shutdown,
+};
+
+static int __init pcsp_init(void)
+{
+	return platform_driver_register(&pcsp_platform_driver);
+}
+
+static void __exit pcsp_exit(void)
+{
+	platform_driver_unregister(&pcsp_platform_driver);
+}
+
+module_init(pcsp_init);
+module_exit(pcsp_exit);
diff --git a/sound/drivers/pcsp/pcsp.h b/sound/drivers/pcsp/pcsp.h
new file mode 100644
index 0000000..f07cc1e
--- /dev/null
+++ b/sound/drivers/pcsp/pcsp.h
@@ -0,0 +1,82 @@
+/*
+ * PC-Speaker driver for Linux
+ *
+ * Copyright (C) 1993-1997  Michael Beck
+ * Copyright (C) 1997-2001  David Woodhouse
+ * Copyright (C) 2001-2008  Stas Sergeev
+ */
+
+#ifndef __PCSP_H__
+#define __PCSP_H__
+
+#include <linux/hrtimer.h>
+#if defined(CONFIG_MIPS) || defined(CONFIG_X86)
+/* Use the global PIT lock ! */
+#include <asm/i8253.h>
+#else
+#include <asm/8253pit.h>
+static DEFINE_SPINLOCK(i8253_lock);
+#endif
+
+#define PCSP_SOUND_VERSION 0x400	/* read 4.00 */
+#define PCSP_DEBUG 0
+
+/* default timer freq for PC-Speaker: 18643 Hz */
+#define DIV_18KHZ 64
+#define MAX_DIV DIV_18KHZ
+#define CUR_DIV() (MAX_DIV >> chip->treble)
+#define PCSP_MAX_TREBLE 1
+
+/* unfortunately, with hrtimers 37KHz does not work very well :( */
+#define PCSP_DEFAULT_TREBLE 0
+#define MIN_DIV (MAX_DIV >> PCSP_MAX_TREBLE)
+
+/* wild guess */
+#define PCSP_MIN_LPJ 1000000
+#define PCSP_DEFAULT_SDIV (DIV_18KHZ >> 1)
+#define PCSP_DEFAULT_SRATE (PIT_TICK_RATE / PCSP_DEFAULT_SDIV)
+#define PCSP_INDEX_INC() (1 << (PCSP_MAX_TREBLE - chip->treble))
+#define PCSP_RATE() (PIT_TICK_RATE / CUR_DIV())
+#define PCSP_MIN_RATE__1 MAX_DIV/PIT_TICK_RATE
+#define PCSP_MAX_RATE__1 MIN_DIV/PIT_TICK_RATE
+#define PCSP_MAX_PERIOD_NS (1000000000ULL * PCSP_MIN_RATE__1)
+#define PCSP_MIN_PERIOD_NS (1000000000ULL * PCSP_MAX_RATE__1)
+#define PCSP_CALC_NS(div) ({ \
+	u64 __val = 1000000000ULL * (div); \
+	do_div(__val, PIT_TICK_RATE); \
+	__val; \
+})
+#define PCSP_PERIOD_NS() PCSP_CALC_NS(CUR_DIV())
+
+#define PCSP_MAX_PERIOD_SIZE	(64*1024)
+#define PCSP_MAX_PERIODS	512
+#define PCSP_BUFFER_SIZE	(128*1024)
+
+struct snd_pcsp {
+	struct snd_card *card;
+	struct snd_pcm *pcm;
+	struct input_dev *input_dev;
+	struct hrtimer timer;
+	unsigned short port, irq, dma;
+	spinlock_t substream_lock;
+	struct snd_pcm_substream *playback_substream;
+	size_t playback_ptr;
+	size_t period_ptr;
+	atomic_t timer_active;
+	int thalf;
+	u64 ns_rem;
+	unsigned char val61;
+	int enable;
+	int max_treble;
+	int treble;
+	int pcspkr;
+};
+
+extern struct snd_pcsp pcsp_chip;
+
+extern enum hrtimer_restart pcsp_do_timer(struct hrtimer *handle);
+
+extern int snd_pcsp_new_pcm(struct snd_pcsp *chip);
+extern int snd_pcsp_new_mixer(struct snd_pcsp *chip);
+
+#endif
diff --git a/sound/drivers/pcsp/pcsp_input.c b/sound/drivers/pcsp/pcsp_input.c
new file mode 100644
index 0000000..cd9b83e
--- /dev/null
+++ b/sound/drivers/pcsp/pcsp_input.c
@@ -0,0 +1,116 @@
+/*
+ *  PC Speaker beeper driver for Linux
+ *
+ *  Copyright (c) 2002 Vojtech Pavlik
+ *  Copyright (c) 1992 Orest Zborowski
+ *
+ */
+
+/*
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 as published by
+ * the Free Software Foundation
+ */
+
+#include <linux/init.h>
+#include <linux/input.h>
+#include <asm/io.h>
+#include "pcsp.h"
+
+static void pcspkr_do_sound(unsigned int count)
+{
+	unsigned long flags;
+
+	spin_lock_irqsave(&i8253_lock, flags);
+
+	if (count) {
+		/* enable counter 2 */
+		outb_p(inb_p(0x61) | 3, 0x61);
+		/* set command for counter 2, 2 byte write */
+		outb_p(0xB6, 0x43);
+		/* select desired HZ */
+		outb_p(count & 0xff, 0x42);
+		outb((count >> 8) & 0xff, 0x42);
+	} else {
+		/* disable counter 2 */
+		outb(inb_p(0x61) & 0xFC, 0x61);
+	}
+
+	spin_unlock_irqrestore(&i8253_lock, flags);
+}
+
+void pcspkr_stop_sound(void)
+{
+	pcspkr_do_sound(0);
+}
+
+static int pcspkr_input_event(struct input_dev *dev, unsigned int type,
+			      unsigned int code, int value)
+{
+	unsigned int count = 0;
+
+	if (atomic_read(&pcsp_chip.timer_active) || !pcsp_chip.pcspkr)
+		return 0;
+
+	switch (type) {
+	case EV_SND:
+		switch (code) {
+		case SND_BELL:
+			if (value)
+				value = 1000;
+		case SND_TONE:
+			break;
+		default:
+			return -1;
+		}
+		break;
+
+	default:
+		return -1;
+	}
+
+	if (value > 20 && value < 32767)
+		count = PIT_TICK_RATE / value;
+
+	pcspkr_do_sound(count);
+
+	return 0;
+}
+
+int __devinit pcspkr_input_init(struct input_dev **rdev, struct device *dev)
+{
+	int err;
+
+	struct input_dev *input_dev = input_allocate_device();
+	if (!input_dev)
+		return -ENOMEM;
+
+	input_dev->name = "PC Speaker";
+	input_dev->phys = "isa0061/input0";
+	input_dev->id.bustype = BUS_ISA;
+	input_dev->id.vendor = 0x001f;
+	input_dev->id.product = 0x0001;
+	input_dev->id.version = 0x0100;
+	input_dev->dev.parent = dev;
+
+	input_dev->evbit[0] = BIT(EV_SND);
+	input_dev->sndbit[0] = BIT(SND_BELL) | BIT(SND_TONE);
+	input_dev->event = pcspkr_input_event;
+
+	err = input_register_device(input_dev);
+	if (err) {
+		input_free_device(input_dev);
+		return err;
+	}
+
+	*rdev = input_dev;
+	return 0;
+}
+
+int pcspkr_input_remove(struct input_dev *dev)
+{
+	pcspkr_stop_sound();
+	input_unregister_device(dev);	/* this also does kfree() */
+
+	return 0;
+}
diff --git a/sound/drivers/pcsp/pcsp_input.h b/sound/drivers/pcsp/pcsp_input.h
new file mode 100644
index 0000000..e66738c
--- /dev/null
+++ b/sound/drivers/pcsp/pcsp_input.h
@@ -0,0 +1,14 @@
+/*
+ * PC-Speaker driver for Linux
+ *
+ * Copyright (C) 2001-2008  Stas Sergeev
+ */
+
+#ifndef __PCSP_INPUT_H__
+#define __PCSP_INPUT_H__
+
+int __devinit pcspkr_input_init(struct input_dev **rdev, struct device *dev);
+int pcspkr_input_remove(struct input_dev *dev);
+void pcspkr_stop_sound(void);
+
+#endif
diff --git a/sound/drivers/pcsp/pcsp_lib.c b/sound/drivers/pcsp/pcsp_lib.c
new file mode 100644
index 0000000..6bdcb89
--- /dev/null
+++ b/sound/drivers/pcsp/pcsp_lib.c
@@ -0,0 +1,347 @@
+/*
+ * PC-Speaker driver for Linux
+ *
+ * Copyright (C) 1993-1997  Michael Beck
+ * Copyright (C) 1997-2001  David Woodhouse
+ * Copyright (C) 2001-2008  Stas Sergeev
+ */
+
+#include <linux/module.h>
+#include <linux/moduleparam.h>
+#include <sound/pcm.h>
+#include <sound/pcm_params.h>
+#include <linux/interrupt.h>
+#include <asm/io.h>
+#include <asm/i8253.h>
+#include "pcsp.h"
+
+static int nforce_wa;
+module_param(nforce_wa, bool, 0444);
+MODULE_PARM_DESC(nforce_wa, "Apply NForce chipset workaround "
+		"(expect bad sound)");
+
+#define DMIX_WANTS_S16	1
+
+static void pcsp_start_timer(unsigned long dummy)
+{
+	hrtimer_start(&pcsp_chip.timer, ktime_set(0, 0), HRTIMER_MODE_REL);
+}
+
+/*
+ * We need the hrtimer_start as a tasklet to avoid
+ * the nasty locking problem. :(
+ * The problem:
+ * - The timer handler is called with the cpu_base->lock
+ *   already held by hrtimer code.
+ * - snd_pcm_period_elapsed() takes the
+ *   substream->self_group.lock.
+ * So far so good.
+ * But the snd_pcsp_trigger() is called with the
+ * substream->self_group.lock held, and it calls
+ * hrtimer_start(), which takes the cpu_base->lock.
+ * You see the problem. We have the code pathes
+ * which take two locks in a reverse order. This
+ * can deadlock and the lock validator complains.
+ * The only solution I could find was to move the
+ * hrtimer_start() into a tasklet. -stsp
+ */
+DECLARE_TASKLET(pcsp_start_timer_tasklet, pcsp_start_timer, 0);
+
+enum hrtimer_restart pcsp_do_timer(struct hrtimer *handle)
+{
+	unsigned long flags;
+	unsigned char timer_cnt, val;
+	int fmt_size, periods_elapsed;
+	u64 ns;
+	size_t period_bytes, buffer_bytes;
+	struct snd_pcm_substream *substream;
+	struct snd_pcm_runtime *runtime;
+	struct snd_pcsp *chip = container_of(handle, struct snd_pcsp, timer);
+
+	if (chip->thalf) {
+		outb(chip->val61, 0x61);
+		chip->thalf = 0;
+		if (!atomic_read(&chip->timer_active))
+			return HRTIMER_NORESTART;
+		hrtimer_forward(&chip->timer, chip->timer.expires,
+				ktime_set(0, chip->ns_rem));
+		return HRTIMER_RESTART;
+	}
+
+	/* hrtimer calls us from both hardirq and softirq contexts,
+	 * so irqsave :( */
+	spin_lock_irqsave(&chip->substream_lock, flags);
+	/* Takashi Iwai says regarding this extra lock:
+
+	If the irq handler handles some data on the DMA buffer, it should
+	do snd_pcm_stream_lock().
+	That protects basically against all races among PCM callbacks, yes.
+	However, there are two remaining issues:
+	1. The substream pointer you try to lock isn't protected _before_
+	  this lock yet.
+	2. snd_pcm_period_elapsed() itself acquires the lock.
+	The requirement of another lock is because of 1.  When you get
+	chip->playback_substream, it's not protected.
+	Keeping this lock while snd_pcm_period_elapsed() assures the substream
+	is still protected (at least, not released).  And the other status is
+	handled properly inside snd_pcm_stream_lock() in
+	snd_pcm_period_elapsed().
+
+	*/
+	if (!chip->playback_substream)
+		goto exit_nr_unlock1;
+	substream = chip->playback_substream;
+	snd_pcm_stream_lock(substream);
+	if (!atomic_read(&chip->timer_active))
+		goto exit_nr_unlock2;
+
+	runtime = substream->runtime;
+	fmt_size = snd_pcm_format_physical_width(runtime->format) >> 3;
+	/* assume it is mono! */
+	val = runtime->dma_area[chip->playback_ptr + fmt_size - 1];
+	if (snd_pcm_format_signed(runtime->format))
+		val ^= 0x80;
+	timer_cnt = val * CUR_DIV() / 256;
+
+	if (timer_cnt && chip->enable) {
+		spin_lock(&i8253_lock);
+		if (!nforce_wa) {
+			outb_p(chip->val61, 0x61);
+			outb_p(timer_cnt, 0x42);
+			outb(chip->val61 ^ 1, 0x61);
+		} else {
+			outb(chip->val61 ^ 2, 0x61);
+			chip->thalf = 1;
+		}
+		spin_unlock(&i8253_lock);
+	}
+
+	period_bytes = snd_pcm_lib_period_bytes(substream);
+	buffer_bytes = snd_pcm_lib_buffer_bytes(substream);
+	chip->playback_ptr += PCSP_INDEX_INC() * fmt_size;
+	periods_elapsed = chip->playback_ptr - chip->period_ptr;
+	if (periods_elapsed < 0) {
+		printk(KERN_WARNING "PCSP: playback_ptr inconsistent "
+			"(%zi %zi %zi)\n",
+			chip->playback_ptr, period_bytes, buffer_bytes);
+		periods_elapsed += buffer_bytes;
+	}
+	periods_elapsed /= period_bytes;
+	/* wrap the pointer _before_ calling snd_pcm_period_elapsed(),
+	 * or ALSA will BUG on us. */
+	chip->playback_ptr %= buffer_bytes;
+
+	snd_pcm_stream_unlock(substream);
+
+	if (periods_elapsed) {
+		snd_pcm_period_elapsed(substream);
+		chip->period_ptr += periods_elapsed * period_bytes;
+		chip->period_ptr %= buffer_bytes;
+	}
+
+	spin_unlock_irqrestore(&chip->substream_lock, flags);
+
+	if (!atomic_read(&chip->timer_active))
+		return HRTIMER_NORESTART;
+
+	chip->ns_rem = PCSP_PERIOD_NS();
+	ns = (chip->thalf ? PCSP_CALC_NS(timer_cnt) : chip->ns_rem);
+	chip->ns_rem -= ns;
+	hrtimer_forward(&chip->timer, chip->timer.expires, ktime_set(0, ns));
+	return HRTIMER_RESTART;
+
+exit_nr_unlock2:
+	snd_pcm_stream_unlock(substream);
+exit_nr_unlock1:
+	spin_unlock_irqrestore(&chip->substream_lock, flags);
+	return HRTIMER_NORESTART;
+}
+
+static void pcsp_start_playing(struct snd_pcsp *chip)
+{
+#if PCSP_DEBUG
+	printk(KERN_INFO "PCSP: start_playing called\n");
+#endif
+	if (atomic_read(&chip->timer_active)) {
+		printk(KERN_ERR "PCSP: Timer already active\n");
+		return;
+	}
+
+	spin_lock(&i8253_lock);
+	chip->val61 = inb(0x61) | 0x03;
+	outb_p(0x92, 0x43);	/* binary, mode 1, LSB only, ch 2 */
+	spin_unlock(&i8253_lock);
+	atomic_set(&chip->timer_active, 1);
+	chip->thalf = 0;
+
+	tasklet_schedule(&pcsp_start_timer_tasklet);
+}
+
+static void pcsp_stop_playing(struct snd_pcsp *chip)
+{
+#if PCSP_DEBUG
+	printk(KERN_INFO "PCSP: stop_playing called\n");
+#endif
+	if (!atomic_read(&chip->timer_active))
+		return;
+
+	atomic_set(&chip->timer_active, 0);
+	spin_lock(&i8253_lock);
+	/* restore the timer */
+	outb_p(0xb6, 0x43);	/* binary, mode 3, LSB/MSB, ch 2 */
+	outb(chip->val61 & 0xFC, 0x61);
+	spin_unlock(&i8253_lock);
+}
+
+static int snd_pcsp_playback_close(struct snd_pcm_substream *substream)
+{
+	struct snd_pcsp *chip = snd_pcm_substream_chip(substream);
+#if PCSP_DEBUG
+	printk(KERN_INFO "PCSP: close called\n");
+#endif
+	if (atomic_read(&chip->timer_active)) {
+		printk(KERN_ERR "PCSP: timer still active\n");
+		pcsp_stop_playing(chip);
+	}
+	spin_lock_irq(&chip->substream_lock);
+	chip->playback_substream = NULL;
+	spin_unlock_irq(&chip->substream_lock);
+	return 0;
+}
+
+static int snd_pcsp_playback_hw_params(struct snd_pcm_substream *substream,
+				       struct snd_pcm_hw_params *hw_params)
+{
+	int err;
+	err = snd_pcm_lib_malloc_pages(substream,
+				      params_buffer_bytes(hw_params));
+	if (err < 0)
+		return err;
+	return 0;
+}
+
+static int snd_pcsp_playback_hw_free(struct snd_pcm_substream *substream)
+{
+#if PCSP_DEBUG
+	printk(KERN_INFO "PCSP: hw_free called\n");
+#endif
+	return snd_pcm_lib_free_pages(substream);
+}
+
+static int snd_pcsp_playback_prepare(struct snd_pcm_substream *substream)
+{
+	struct snd_pcsp *chip = snd_pcm_substream_chip(substream);
+#if PCSP_DEBUG
+	printk(KERN_INFO "PCSP: prepare called, "
+			"size=%zi psize=%zi f=%zi f1=%i\n",
+			snd_pcm_lib_buffer_bytes(substream),
+			snd_pcm_lib_period_bytes(substream),
+			snd_pcm_lib_buffer_bytes(substream) /
+			snd_pcm_lib_period_bytes(substream),
+			substream->runtime->periods);
+#endif
+	chip->playback_ptr = 0;
+	chip->period_ptr = 0;
+	return 0;
+}
+
+static int snd_pcsp_trigger(struct snd_pcm_substream *substream, int cmd)
+{
+	struct snd_pcsp *chip = snd_pcm_substream_chip(substream);
+#if PCSP_DEBUG
+	printk(KERN_INFO "PCSP: trigger called\n");
+#endif
+	switch (cmd) {
+	case SNDRV_PCM_TRIGGER_START:
+	case SNDRV_PCM_TRIGGER_RESUME:
+		pcsp_start_playing(chip);
+		break;
+	case SNDRV_PCM_TRIGGER_STOP:
+	case SNDRV_PCM_TRIGGER_SUSPEND:
+		pcsp_stop_playing(chip);
+		break;
+	default:
+		return -EINVAL;
+	}
+	return 0;
+}
+
+static snd_pcm_uframes_t snd_pcsp_playback_pointer(struct snd_pcm_substream
+						   *substream)
+{
+	struct snd_pcsp *chip = snd_pcm_substream_chip(substream);
+	return bytes_to_frames(substream->runtime, chip->playback_ptr);
+}
+
+static struct snd_pcm_hardware snd_pcsp_playback = {
+	.info = (SNDRV_PCM_INFO_INTERLEAVED |
+		 SNDRV_PCM_INFO_HALF_DUPLEX |
+		 SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID),
+	.formats = (SNDRV_PCM_FMTBIT_U8
+#if DMIX_WANTS_S16
+		    | SNDRV_PCM_FMTBIT_S16_LE
+#endif
+	    ),
+	.rates = SNDRV_PCM_RATE_KNOT,
+	.rate_min = PCSP_DEFAULT_SRATE,
+	.rate_max = PCSP_DEFAULT_SRATE,
+	.channels_min = 1,
+	.channels_max = 1,
+	.buffer_bytes_max = PCSP_BUFFER_SIZE,
+	.period_bytes_min = 64,
+	.period_bytes_max = PCSP_MAX_PERIOD_SIZE,
+	.periods_min = 2,
+	.periods_max = PCSP_MAX_PERIODS,
+	.fifo_size = 0,
+};
+
+static int snd_pcsp_playback_open(struct snd_pcm_substream *substream)
+{
+	struct snd_pcsp *chip = snd_pcm_substream_chip(substream);
+	struct snd_pcm_runtime *runtime = substream->runtime;
+#if PCSP_DEBUG
+	printk(KERN_INFO "PCSP: open called\n");
+#endif
+	if (atomic_read(&chip->timer_active)) {
+		printk(KERN_ERR "PCSP: still active!!\n");
+		return -EBUSY;
+	}
+	runtime->hw = snd_pcsp_playback;
+	chip->playback_substream = substream;
+	return 0;
+}
+
+static struct snd_pcm_ops snd_pcsp_playback_ops = {
+	.open = snd_pcsp_playback_open,
+	.close = snd_pcsp_playback_close,
+	.ioctl = snd_pcm_lib_ioctl,
+	.hw_params = snd_pcsp_playback_hw_params,
+	.hw_free = snd_pcsp_playback_hw_free,
+	.prepare = snd_pcsp_playback_prepare,
+	.trigger = snd_pcsp_trigger,
+	.pointer = snd_pcsp_playback_pointer,
+};
+
+int __devinit snd_pcsp_new_pcm(struct snd_pcsp *chip)
+{
+	int err;
+
+	err = snd_pcm_new(chip->card, "pcspeaker", 0, 1, 0, &chip->pcm);
+	if (err < 0)
+		return err;
+
+	snd_pcm_set_ops(chip->pcm, SNDRV_PCM_STREAM_PLAYBACK,
+			&snd_pcsp_playback_ops);
+
+	chip->pcm->private_data = chip;
+	chip->pcm->info_flags = SNDRV_PCM_INFO_HALF_DUPLEX;
+	strcpy(chip->pcm->name, "pcsp");
+
+	snd_pcm_lib_preallocate_pages_for_all(chip->pcm,
+					      SNDRV_DMA_TYPE_CONTINUOUS,
+					      snd_dma_continuous_data
+					      (GFP_KERNEL), PCSP_BUFFER_SIZE,
+					      PCSP_BUFFER_SIZE);
+
+	return 0;
+}
diff --git a/sound/drivers/pcsp/pcsp_mixer.c b/sound/drivers/pcsp/pcsp_mixer.c
new file mode 100644
index 0000000..64a695f
--- /dev/null
+++ b/sound/drivers/pcsp/pcsp_mixer.c
@@ -0,0 +1,143 @@
+/*
+ * PC-Speaker driver for Linux
+ *
+ * Mixer implementation.
+ * Copyright (C) 2001-2008  Stas Sergeev
+ */
+
+#include <sound/core.h>
+#include <sound/control.h>
+#include "pcsp.h"
+
+
+static int pcsp_enable_info(struct snd_kcontrol *kcontrol,
+			    struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+	uinfo->count = 1;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = 1;
+	return 0;
+}
+
+static int pcsp_enable_get(struct snd_kcontrol *kcontrol,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_pcsp *chip = snd_kcontrol_chip(kcontrol);
+	ucontrol->value.integer.value[0] = chip->enable;
+	return 0;
+}
+
+static int pcsp_enable_put(struct snd_kcontrol *kcontrol,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_pcsp *chip = snd_kcontrol_chip(kcontrol);
+	int changed = 0;
+	int enab = ucontrol->value.integer.value[0];
+	if (enab != chip->enable) {
+		chip->enable = enab;
+		changed = 1;
+	}
+	return changed;
+}
+
+static int pcsp_treble_info(struct snd_kcontrol *kcontrol,
+			    struct snd_ctl_elem_info *uinfo)
+{
+	struct snd_pcsp *chip = snd_kcontrol_chip(kcontrol);
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_ENUMERATED;
+	uinfo->count = 1;
+	uinfo->value.enumerated.items = chip->max_treble + 1;
+	if (uinfo->value.enumerated.item > chip->max_treble)
+		uinfo->value.enumerated.item = chip->max_treble;
+	sprintf(uinfo->value.enumerated.name, "%d", PCSP_RATE());
+	return 0;
+}
+
+static int pcsp_treble_get(struct snd_kcontrol *kcontrol,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_pcsp *chip = snd_kcontrol_chip(kcontrol);
+	ucontrol->value.enumerated.item[0] = chip->treble;
+	return 0;
+}
+
+static int pcsp_treble_put(struct snd_kcontrol *kcontrol,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_pcsp *chip = snd_kcontrol_chip(kcontrol);
+	int changed = 0;
+	int treble = ucontrol->value.enumerated.item[0];
+	if (treble != chip->treble) {
+		chip->treble = treble;
+#if PCSP_DEBUG
+		printk(KERN_INFO "PCSP: rate set to %i\n", PCSP_RATE());
+#endif
+		changed = 1;
+	}
+	return changed;
+}
+
+static int pcsp_pcspkr_info(struct snd_kcontrol *kcontrol,
+			    struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
+	uinfo->count = 1;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = 1;
+	return 0;
+}
+
+static int pcsp_pcspkr_get(struct snd_kcontrol *kcontrol,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_pcsp *chip = snd_kcontrol_chip(kcontrol);
+	ucontrol->value.integer.value[0] = chip->pcspkr;
+	return 0;
+}
+
+static int pcsp_pcspkr_put(struct snd_kcontrol *kcontrol,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_pcsp *chip = snd_kcontrol_chip(kcontrol);
+	int changed = 0;
+	int spkr = ucontrol->value.integer.value[0];
+	if (spkr != chip->pcspkr) {
+		chip->pcspkr = spkr;
+		changed = 1;
+	}
+	return changed;
+}
+
+#define PCSP_MIXER_CONTROL(ctl_type, ctl_name) \
+{ \
+	.iface =	SNDRV_CTL_ELEM_IFACE_MIXER, \
+	.name =		ctl_name, \
+	.info =		pcsp_##ctl_type##_info, \
+	.get =		pcsp_##ctl_type##_get, \
+	.put =		pcsp_##ctl_type##_put, \
+}
+
+static struct snd_kcontrol_new __devinitdata snd_pcsp_controls[] = {
+	PCSP_MIXER_CONTROL(enable, "Master Playback Switch"),
+	PCSP_MIXER_CONTROL(treble, "BaseFRQ Playback Volume"),
+	PCSP_MIXER_CONTROL(pcspkr, "PC Speaker Playback Switch"),
+};
+
+int __devinit snd_pcsp_new_mixer(struct snd_pcsp *chip)
+{
+	struct snd_card *card = chip->card;
+	int i, err;
+
+	for (i = 0; i < ARRAY_SIZE(snd_pcsp_controls); i++) {
+		err = snd_ctl_add(card,
+				 snd_ctl_new1(snd_pcsp_controls + i,
+					      chip));
+		if (err < 0)
+			return err;
+	}
+
+	strcpy(card->mixername, "PC-Speaker");
+
+	return 0;
+}