diff options
Diffstat (limited to 'sound/oss/harmony.c')
-rw-r--r-- | sound/oss/harmony.c | 1330 |
1 files changed, 1330 insertions, 0 deletions
diff --git a/sound/oss/harmony.c b/sound/oss/harmony.c new file mode 100644 index 000000000000..bee9d344cd26 --- /dev/null +++ b/sound/oss/harmony.c @@ -0,0 +1,1330 @@ +/* + drivers/sound/harmony.c + + This is a sound driver for ASP's and Lasi's Harmony sound chip + and is unlikely to be used for anything other than on a HP PA-RISC. + + Harmony is found in HP 712s, 715/new and many other GSC based machines. + On older 715 machines you'll find the technically identical chip + called 'Vivace'. Both Harmony and Vicace are supported by this driver. + + Copyright 2000 (c) Linuxcare Canada, Alex deVries <alex@onefishtwo.ca> + Copyright 2000-2003 (c) Helge Deller <deller@gmx.de> + Copyright 2001 (c) Matthieu Delahaye <delahaym@esiee.fr> + Copyright 2001 (c) Jean-Christophe Vaugeois <vaugeoij@esiee.fr> + Copyright 2004 (c) Stuart Brady <sdbrady@ntlworld.com> + + +TODO: + - fix SNDCTL_DSP_GETOSPACE and SNDCTL_DSP_GETISPACE ioctls to + return the real values + - add private ioctl for selecting line- or microphone input + (only one of them is available at the same time) + - add module parameters + - implement mmap functionality + - implement gain meter ? + - ... +*/ + +#include <linux/delay.h> +#include <linux/errno.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/ioport.h> +#include <linux/types.h> +#include <linux/mm.h> +#include <linux/pci.h> + +#include <asm/parisc-device.h> +#include <asm/io.h> + +#include "sound_config.h" + + +#define PFX "harmony: " +#define HARMONY_VERSION "V0.9a" + +#undef DEBUG +#ifdef DEBUG +# define DPRINTK printk +#else +# define DPRINTK(x,...) +#endif + + +#define MAX_BUFS 10 /* maximum number of rotating buffers */ +#define HARMONY_BUF_SIZE 4096 /* needs to be a multiple of PAGE_SIZE (4096)! */ + +#define CNTL_C 0x80000000 +#define CNTL_ST 0x00000020 +#define CNTL_44100 0x00000015 /* HARMONY_SR_44KHZ */ +#define CNTL_8000 0x00000008 /* HARMONY_SR_8KHZ */ + +#define GAINCTL_HE 0x08000000 +#define GAINCTL_LE 0x04000000 +#define GAINCTL_SE 0x02000000 + +#define DSTATUS_PN 0x00000200 +#define DSTATUS_RN 0x00000002 + +#define DSTATUS_IE 0x80000000 + +#define HARMONY_DF_16BIT_LINEAR 0 +#define HARMONY_DF_8BIT_ULAW 1 +#define HARMONY_DF_8BIT_ALAW 2 + +#define HARMONY_SS_MONO 0 +#define HARMONY_SS_STEREO 1 + +#define HARMONY_SR_8KHZ 0x08 +#define HARMONY_SR_16KHZ 0x09 +#define HARMONY_SR_27KHZ 0x0A +#define HARMONY_SR_32KHZ 0x0B +#define HARMONY_SR_48KHZ 0x0E +#define HARMONY_SR_9KHZ 0x0F +#define HARMONY_SR_5KHZ 0x10 +#define HARMONY_SR_11KHZ 0x11 +#define HARMONY_SR_18KHZ 0x12 +#define HARMONY_SR_22KHZ 0x13 +#define HARMONY_SR_37KHZ 0x14 +#define HARMONY_SR_44KHZ 0x15 +#define HARMONY_SR_33KHZ 0x16 +#define HARMONY_SR_6KHZ 0x17 + +/* + * Some magics numbers used to auto-detect file formats + */ + +#define HARMONY_MAGIC_8B_ULAW 1 +#define HARMONY_MAGIC_8B_ALAW 27 +#define HARMONY_MAGIC_16B_LINEAR 3 +#define HARMONY_MAGIC_MONO 1 +#define HARMONY_MAGIC_STEREO 2 + +/* + * Channels Positions in mixer register + */ + +#define GAIN_HE_SHIFT 27 +#define GAIN_HE_MASK ( 1 << GAIN_HE_SHIFT) +#define GAIN_LE_SHIFT 26 +#define GAIN_LE_MASK ( 1 << GAIN_LE_SHIFT) +#define GAIN_SE_SHIFT 25 +#define GAIN_SE_MASK ( 1 << GAIN_SE_SHIFT) +#define GAIN_IS_SHIFT 24 +#define GAIN_IS_MASK ( 1 << GAIN_IS_SHIFT) +#define GAIN_MA_SHIFT 20 +#define GAIN_MA_MASK ( 0x0f << GAIN_MA_SHIFT) +#define GAIN_LI_SHIFT 16 +#define GAIN_LI_MASK ( 0x0f << GAIN_LI_SHIFT) +#define GAIN_RI_SHIFT 12 +#define GAIN_RI_MASK ( 0x0f << GAIN_RI_SHIFT) +#define GAIN_LO_SHIFT 6 +#define GAIN_LO_MASK ( 0x3f << GAIN_LO_SHIFT) +#define GAIN_RO_SHIFT 0 +#define GAIN_RO_MASK ( 0x3f << GAIN_RO_SHIFT) + + +#define MAX_OUTPUT_LEVEL (GAIN_RO_MASK >> GAIN_RO_SHIFT) +#define MAX_INPUT_LEVEL (GAIN_RI_MASK >> GAIN_RI_SHIFT) +#define MAX_MONITOR_LEVEL (GAIN_MA_MASK >> GAIN_MA_SHIFT) + +#define MIXER_INTERNAL SOUND_MIXER_LINE1 +#define MIXER_LINEOUT SOUND_MIXER_LINE2 +#define MIXER_HEADPHONES SOUND_MIXER_LINE3 + +#define MASK_INTERNAL SOUND_MASK_LINE1 +#define MASK_LINEOUT SOUND_MASK_LINE2 +#define MASK_HEADPHONES SOUND_MASK_LINE3 + +/* + * Channels Mask in mixer register + */ + +#define GAIN_TOTAL_SILENCE 0x00F00FFF +#define GAIN_DEFAULT 0x0FF00000 + + +struct harmony_hpa { + u8 unused000; + u8 id; + u8 teleshare_id; + u8 unused003; + u32 reset; + u32 cntl; + u32 gainctl; + u32 pnxtadd; + u32 pcuradd; + u32 rnxtadd; + u32 rcuradd; + u32 dstatus; + u32 ov; + u32 pio; + u32 unused02c; + u32 unused030[3]; + u32 diag; +}; + +struct harmony_dev { + struct harmony_hpa *hpa; + struct parisc_device *dev; + u32 current_gain; + u32 dac_rate; /* 8000 ... 48000 (Hz) */ + u8 data_format; /* HARMONY_DF_xx_BIT_xxx */ + u8 sample_rate; /* HARMONY_SR_xx_KHZ */ + u8 stereo_select; /* HARMONY_SS_MONO or HARMONY_SS_STEREO */ + int format_initialized :1; + int suspended_playing :1; + int suspended_recording :1; + + int blocked_playing :1; + int blocked_recording :1; + int audio_open :1; + int mixer_open :1; + + wait_queue_head_t wq_play, wq_record; + int first_filled_play; /* first buffer containing data (next to play) */ + int nb_filled_play; + int play_offset; + int first_filled_record; + int nb_filled_record; + + int dsp_unit, mixer_unit; +}; + + +static struct harmony_dev harmony; + + +/* + * Dynamic sound buffer allocation and DMA memory + */ + +struct harmony_buffer { + unsigned char *addr; + dma_addr_t dma_handle; + int dma_coherent; /* Zero if dma_alloc_coherent() fails */ + unsigned int len; +}; + +/* + * Harmony memory buffers + */ + +static struct harmony_buffer played_buf, recorded_buf, silent, graveyard; + + +#define CHECK_WBACK_INV_OFFSET(b,offset,len) \ + do { if (!b.dma_coherent) \ + dma_cache_wback_inv((unsigned long)b.addr+offset,len); \ + } while (0) + + +static int __init harmony_alloc_buffer(struct harmony_buffer *b, + unsigned int buffer_count) +{ + b->len = buffer_count * HARMONY_BUF_SIZE; + b->addr = dma_alloc_coherent(&harmony.dev->dev, + b->len, &b->dma_handle, GFP_KERNEL|GFP_DMA); + if (b->addr && b->dma_handle) { + b->dma_coherent = 1; + DPRINTK(KERN_INFO PFX "coherent memory: 0x%lx, played_buf: 0x%lx\n", + (unsigned long)b->dma_handle, (unsigned long)b->addr); + } else { + b->dma_coherent = 0; + /* kmalloc()ed memory will HPMC on ccio machines ! */ + b->addr = kmalloc(b->len, GFP_KERNEL); + if (!b->addr) { + printk(KERN_ERR PFX "couldn't allocate memory\n"); + return -EBUSY; + } + b->dma_handle = __pa(b->addr); + } + return 0; +} + +static void __exit harmony_free_buffer(struct harmony_buffer *b) +{ + if (!b->addr) + return; + + if (b->dma_coherent) + dma_free_coherent(&harmony.dev->dev, + b->len, b->addr, b->dma_handle); + else + kfree(b->addr); + + memset(b, 0, sizeof(*b)); +} + + + +/* + * Low-Level sound-chip programming + */ + +static void __inline__ harmony_wait_CNTL(void) +{ + /* Wait until we're out of control mode */ + while (gsc_readl(&harmony.hpa->cntl) & CNTL_C) + /* wait */ ; +} + + +static void harmony_update_control(void) +{ + u32 default_cntl; + + /* Set CNTL */ + default_cntl = (CNTL_C | /* The C bit */ + (harmony.data_format << 6) | /* Set the data format */ + (harmony.stereo_select << 5) | /* Stereo select */ + (harmony.sample_rate)); /* Set sample rate */ + harmony.format_initialized = 1; + + /* initialize CNTL */ + gsc_writel(default_cntl, &harmony.hpa->cntl); +} + +static void harmony_set_control(u8 data_format, u8 sample_rate, u8 stereo_select) +{ + harmony.sample_rate = sample_rate; + harmony.data_format = data_format; + harmony.stereo_select = stereo_select; + harmony_update_control(); +} + +static void harmony_set_rate(u8 data_rate) +{ + harmony.sample_rate = data_rate; + harmony_update_control(); +} + +static int harmony_detect_rate(int *freq) +{ + int newrate; + switch (*freq) { + case 8000: newrate = HARMONY_SR_8KHZ; break; + case 16000: newrate = HARMONY_SR_16KHZ; break; + case 27428: newrate = HARMONY_SR_27KHZ; break; + case 32000: newrate = HARMONY_SR_32KHZ; break; + case 48000: newrate = HARMONY_SR_48KHZ; break; + case 9600: newrate = HARMONY_SR_9KHZ; break; + case 5512: newrate = HARMONY_SR_5KHZ; break; + case 11025: newrate = HARMONY_SR_11KHZ; break; + case 18900: newrate = HARMONY_SR_18KHZ; break; + case 22050: newrate = HARMONY_SR_22KHZ; break; + case 37800: newrate = HARMONY_SR_37KHZ; break; + case 44100: newrate = HARMONY_SR_44KHZ; break; + case 33075: newrate = HARMONY_SR_33KHZ; break; + case 6615: newrate = HARMONY_SR_6KHZ; break; + default: newrate = HARMONY_SR_8KHZ; + *freq = 8000; break; + } + return newrate; +} + +static void harmony_set_format(u8 data_format) +{ + harmony.data_format = data_format; + harmony_update_control(); +} + +static void harmony_set_stereo(u8 stereo_select) +{ + harmony.stereo_select = stereo_select; + harmony_update_control(); +} + +static void harmony_disable_interrupts(void) +{ + harmony_wait_CNTL(); + gsc_writel(0, &harmony.hpa->dstatus); +} + +static void harmony_enable_interrupts(void) +{ + harmony_wait_CNTL(); + gsc_writel(DSTATUS_IE, &harmony.hpa->dstatus); +} + +/* + * harmony_silence() + * + * This subroutine fills in a buffer starting at location start and + * silences for length bytes. This references the current + * configuration of the audio format. + * + */ + +static void harmony_silence(struct harmony_buffer *buffer, int start, int length) +{ + u8 silence_char; + + /* Despite what you hear, silence is different in + different audio formats. */ + switch (harmony.data_format) { + case HARMONY_DF_8BIT_ULAW: silence_char = 0x55; break; + case HARMONY_DF_8BIT_ALAW: silence_char = 0xff; break; + case HARMONY_DF_16BIT_LINEAR: /* fall through */ + default: silence_char = 0; + } + + memset(buffer->addr+start, silence_char, length); +} + + +static int harmony_audio_open(struct inode *inode, struct file *file) +{ + if (harmony.audio_open) + return -EBUSY; + + harmony.audio_open = 1; + harmony.suspended_playing = harmony.suspended_recording = 1; + harmony.blocked_playing = harmony.blocked_recording = 0; + harmony.first_filled_play = harmony.first_filled_record = 0; + harmony.nb_filled_play = harmony.nb_filled_record = 0; + harmony.play_offset = 0; + init_waitqueue_head(&harmony.wq_play); + init_waitqueue_head(&harmony.wq_record); + + /* Start off in a balanced mode. */ + harmony_set_control(HARMONY_DF_8BIT_ULAW, HARMONY_SR_8KHZ, HARMONY_SS_MONO); + harmony_update_control(); + harmony.format_initialized = 0; + + /* Clear out all the buffers and flush to cache */ + harmony_silence(&played_buf, 0, HARMONY_BUF_SIZE*MAX_BUFS); + CHECK_WBACK_INV_OFFSET(played_buf, 0, HARMONY_BUF_SIZE*MAX_BUFS); + + return 0; +} + +/* + * Release (close) the audio device. + */ + +static int harmony_audio_release(struct inode *inode, struct file *file) +{ + if (!harmony.audio_open) + return -EBUSY; + + harmony.audio_open = 0; + + return 0; +} + +/* + * Read recorded data off the audio device. + */ + +static ssize_t harmony_audio_read(struct file *file, + char *buffer, + size_t size_count, + loff_t *ppos) +{ + int total_count = (int) size_count; + int count = 0; + int buf_to_read; + + while (count<total_count) { + /* Wait until we're out of control mode */ + harmony_wait_CNTL(); + + /* Figure out which buffer to fill in */ + if (harmony.nb_filled_record <= 2) { + harmony.blocked_recording = 1; + if (harmony.suspended_recording) { + harmony.suspended_recording = 0; + harmony_enable_interrupts(); + } + + interruptible_sleep_on(&harmony.wq_record); + harmony.blocked_recording = 0; + } + + if (harmony.nb_filled_record < 2) + return -EBUSY; + + buf_to_read = harmony.first_filled_record; + + /* Copy the page to an aligned buffer */ + if (copy_to_user(buffer+count, recorded_buf.addr + + (HARMONY_BUF_SIZE*buf_to_read), + HARMONY_BUF_SIZE)) { + count = -EFAULT; + break; + } + + harmony.nb_filled_record--; + harmony.first_filled_record++; + harmony.first_filled_record %= MAX_BUFS; + + count += HARMONY_BUF_SIZE; + } + return count; +} + + + + +/* + * Here is the place where we try to recognize file format. + * Sun/NeXT .au files begin with the string .snd + * At offset 12 is specified the encoding. + * At offset 16 is specified speed rate + * At Offset 20 is specified the numbers of voices + */ + +#define four_bytes_to_u32(start) (file_header[start] << 24)|\ + (file_header[start+1] << 16)|\ + (file_header[start+2] << 8)|\ + (file_header[start+3]); + +#define test_rate(tested,real_value,harmony_value) if ((tested)<=(real_value))\ + + +static int harmony_format_auto_detect(const char *buffer, int block_size) +{ + u8 file_header[24]; + u32 start_string; + int ret = 0; + + if (block_size>24) { + if (copy_from_user(file_header, buffer, sizeof(file_header))) + ret = -EFAULT; + + start_string = four_bytes_to_u32(0); + + if ((file_header[4]==0) && (start_string==0x2E736E64)) { + u32 format; + u32 nb_voices; + u32 speed; + + format = four_bytes_to_u32(12); + nb_voices = four_bytes_to_u32(20); + speed = four_bytes_to_u32(16); + + switch (format) { + case HARMONY_MAGIC_8B_ULAW: + harmony.data_format = HARMONY_DF_8BIT_ULAW; + break; + case HARMONY_MAGIC_8B_ALAW: + harmony.data_format = HARMONY_DF_8BIT_ALAW; + break; + case HARMONY_MAGIC_16B_LINEAR: + harmony.data_format = HARMONY_DF_16BIT_LINEAR; + break; + default: + harmony_set_control(HARMONY_DF_16BIT_LINEAR, + HARMONY_SR_44KHZ, HARMONY_SS_STEREO); + goto out; + } + switch (nb_voices) { + case HARMONY_MAGIC_MONO: + harmony.stereo_select = HARMONY_SS_MONO; + break; + case HARMONY_MAGIC_STEREO: + harmony.stereo_select = HARMONY_SS_STEREO; + break; + default: + harmony.stereo_select = HARMONY_SS_MONO; + break; + } + harmony_set_rate(harmony_detect_rate(&speed)); + harmony.dac_rate = speed; + goto out; + } + } + harmony_set_control(HARMONY_DF_8BIT_ULAW, HARMONY_SR_8KHZ, HARMONY_SS_MONO); +out: + return ret; +} +#undef four_bytes_to_u32 + + +static ssize_t harmony_audio_write(struct file *file, + const char *buffer, + size_t size_count, + loff_t *ppos) +{ + int total_count = (int) size_count; + int count = 0; + int frame_size; + int buf_to_fill; + int fresh_buffer; + + if (!harmony.format_initialized) { + if (harmony_format_auto_detect(buffer, total_count)) + return -EFAULT; + } + + while (count<total_count) { + /* Wait until we're out of control mode */ + harmony_wait_CNTL(); + + /* Figure out which buffer to fill in */ + if (harmony.nb_filled_play+2 >= MAX_BUFS && !harmony.play_offset) { + harmony.blocked_playing = 1; + interruptible_sleep_on(&harmony.wq_play); + harmony.blocked_playing = 0; + } + if (harmony.nb_filled_play+2 >= MAX_BUFS && !harmony.play_offset) + return -EBUSY; + + + buf_to_fill = (harmony.first_filled_play+harmony.nb_filled_play); + if (harmony.play_offset) { + buf_to_fill--; + buf_to_fill += MAX_BUFS; + } + buf_to_fill %= MAX_BUFS; + + fresh_buffer = (harmony.play_offset == 0); + + /* Figure out the size of the frame */ + if ((total_count-count) >= HARMONY_BUF_SIZE - harmony.play_offset) { + frame_size = HARMONY_BUF_SIZE - harmony.play_offset; + } else { + frame_size = total_count - count; + /* Clear out the buffer, since there we'll only be + overlaying part of the old buffer with the new one */ + harmony_silence(&played_buf, + HARMONY_BUF_SIZE*buf_to_fill+frame_size+harmony.play_offset, + HARMONY_BUF_SIZE-frame_size-harmony.play_offset); + } + + /* Copy the page to an aligned buffer */ + if (copy_from_user(played_buf.addr +(HARMONY_BUF_SIZE*buf_to_fill) + harmony.play_offset, + buffer+count, frame_size)) + return -EFAULT; + CHECK_WBACK_INV_OFFSET(played_buf, (HARMONY_BUF_SIZE*buf_to_fill + harmony.play_offset), + frame_size); + + if (fresh_buffer) + harmony.nb_filled_play++; + + count += frame_size; + harmony.play_offset += frame_size; + harmony.play_offset %= HARMONY_BUF_SIZE; + if (harmony.suspended_playing && (harmony.nb_filled_play>=4)) + harmony_enable_interrupts(); + } + + return count; +} + +static unsigned int harmony_audio_poll(struct file *file, + struct poll_table_struct *wait) +{ + unsigned int mask = 0; + + if (file->f_mode & FMODE_READ) { + if (!harmony.suspended_recording) + poll_wait(file, &harmony.wq_record, wait); + if (harmony.nb_filled_record) + mask |= POLLIN | POLLRDNORM; + } + + if (file->f_mode & FMODE_WRITE) { + if (!harmony.suspended_playing) + poll_wait(file, &harmony.wq_play, wait); + if (harmony.nb_filled_play) + mask |= POLLOUT | POLLWRNORM; + } + + return mask; +} + +static int harmony_audio_ioctl(struct inode *inode, + struct file *file, + unsigned int cmd, + unsigned long arg) +{ + int ival, new_format; + int frag_size, frag_buf; + struct audio_buf_info info; + + switch (cmd) { + case OSS_GETVERSION: + return put_user(SOUND_VERSION, (int *) arg); + + case SNDCTL_DSP_GETCAPS: + ival = DSP_CAP_DUPLEX; + return put_user(ival, (int *) arg); + + case SNDCTL_DSP_GETFMTS: + ival = (AFMT_S16_BE | AFMT_MU_LAW | AFMT_A_LAW ); + return put_user(ival, (int *) arg); + + case SNDCTL_DSP_SETFMT: + if (get_user(ival, (int *) arg)) + return -EFAULT; + if (ival != AFMT_QUERY) { + switch (ival) { + case AFMT_MU_LAW: new_format = HARMONY_DF_8BIT_ULAW; break; + case AFMT_A_LAW: new_format = HARMONY_DF_8BIT_ALAW; break; + case AFMT_S16_BE: new_format = HARMONY_DF_16BIT_LINEAR; break; + default: { + DPRINTK(KERN_WARNING PFX + "unsupported sound format 0x%04x requested.\n", + ival); + ival = AFMT_S16_BE; + return put_user(ival, (int *) arg); + } + } + harmony_set_format(new_format); + return 0; + } else { + switch (harmony.data_format) { + case HARMONY_DF_8BIT_ULAW: ival = AFMT_MU_LAW; break; + case HARMONY_DF_8BIT_ALAW: ival = AFMT_A_LAW; break; + case HARMONY_DF_16BIT_LINEAR: ival = AFMT_U16_BE; break; + default: ival = 0; + } + return put_user(ival, (int *) arg); + } + + case SOUND_PCM_READ_RATE: + ival = harmony.dac_rate; + return put_user(ival, (int *) arg); + + case SNDCTL_DSP_SPEED: + if (get_user(ival, (int *) arg)) + return -EFAULT; + harmony_set_rate(harmony_detect_rate(&ival)); + harmony.dac_rate = ival; + return put_user(ival, (int*) arg); + + case SNDCTL_DSP_STEREO: + if (get_user(ival, (int *) arg)) + return -EFAULT; + if (ival != 0 && ival != 1) + return -EINVAL; + harmony_set_stereo(ival); + return 0; + + case SNDCTL_DSP_CHANNELS: + if (get_user(ival, (int *) arg)) + return -EFAULT; + if (ival != 1 && ival != 2) { + ival = harmony.stereo_select == HARMONY_SS_MONO ? 1 : 2; + return put_user(ival, (int *) arg); + } + harmony_set_stereo(ival-1); + return 0; + + case SNDCTL_DSP_GETBLKSIZE: + ival = HARMONY_BUF_SIZE; + return put_user(ival, (int *) arg); + + case SNDCTL_DSP_NONBLOCK: + file->f_flags |= O_NONBLOCK; + return 0; + + case SNDCTL_DSP_RESET: + if (!harmony.suspended_recording) { + /* TODO: stop_recording() */ + } + return 0; + + case SNDCTL_DSP_SETFRAGMENT: + if (get_user(ival, (int *)arg)) + return -EFAULT; + frag_size = ival & 0xffff; + frag_buf = (ival>>16) & 0xffff; + /* TODO: We use hardcoded fragment sizes and numbers for now */ + frag_size = 12; /* 4096 == 2^12 */ + frag_buf = MAX_BUFS; + ival = (frag_buf << 16) + frag_size; + return put_user(ival, (int *) arg); + + case SNDCTL_DSP_GETOSPACE: + if (!(file->f_mode & FMODE_WRITE)) + return -EINVAL; + info.fragstotal = MAX_BUFS; + info.fragments = MAX_BUFS - harmony.nb_filled_play; + info.fragsize = HARMONY_BUF_SIZE; + info.bytes = info.fragments * info.fragsize; + return copy_to_user((void *)arg, &info, sizeof(info)) ? -EFAULT : 0; + + case SNDCTL_DSP_GETISPACE: + if (!(file->f_mode & FMODE_READ)) + return -EINVAL; + info.fragstotal = MAX_BUFS; + info.fragments = /*MAX_BUFS-*/ harmony.nb_filled_record; + info.fragsize = HARMONY_BUF_SIZE; + info.bytes = info.fragments * info.fragsize; + return copy_to_user((void *)arg, &info, sizeof(info)) ? -EFAULT : 0; + + case SNDCTL_DSP_SYNC: + return 0; + } + + return -EINVAL; +} + + +/* + * harmony_interrupt() + * + * harmony interruption service routine + * + */ + +static irqreturn_t harmony_interrupt(int irq, void *dev, struct pt_regs *regs) +{ + u32 dstatus; + struct harmony_hpa *hpa; + + /* Setup the hpa */ + hpa = ((struct harmony_dev *)dev)->hpa; + harmony_wait_CNTL(); + + /* Read dstatus and pcuradd (the current address) */ + dstatus = gsc_readl(&hpa->dstatus); + + /* Turn off interrupts */ + harmony_disable_interrupts(); + + /* Check if this is a request to get the next play buffer */ + if (dstatus & DSTATUS_PN) { + if (!harmony.nb_filled_play) { + harmony.suspended_playing = 1; + gsc_writel((unsigned long)silent.dma_handle, &hpa->pnxtadd); + + if (!harmony.suspended_recording) + harmony_enable_interrupts(); + } else { + harmony.suspended_playing = 0; + gsc_writel((unsigned long)played_buf.dma_handle + + (HARMONY_BUF_SIZE*harmony.first_filled_play), + &hpa->pnxtadd); + harmony.first_filled_play++; + harmony.first_filled_play %= MAX_BUFS; + harmony.nb_filled_play--; + + harmony_enable_interrupts(); + } + + if (harmony.blocked_playing) + wake_up_interruptible(&harmony.wq_play); + } + + /* Check if we're being asked to fill in a recording buffer */ + if (dstatus & DSTATUS_RN) { + if((harmony.nb_filled_record+2>=MAX_BUFS) || harmony.suspended_recording) + { + harmony.nb_filled_record = 0; + harmony.first_filled_record = 0; + harmony.suspended_recording = 1; + gsc_writel((unsigned long)graveyard.dma_handle, &hpa->rnxtadd); + if (!harmony.suspended_playing) + harmony_enable_interrupts(); + } else { + int buf_to_fill; + buf_to_fill = (harmony.first_filled_record+harmony.nb_filled_record) % MAX_BUFS; + CHECK_WBACK_INV_OFFSET(recorded_buf, HARMONY_BUF_SIZE*buf_to_fill, HARMONY_BUF_SIZE); + gsc_writel((unsigned long)recorded_buf.dma_handle + + HARMONY_BUF_SIZE*buf_to_fill, + &hpa->rnxtadd); + harmony.nb_filled_record++; + harmony_enable_interrupts(); + } + + if (harmony.blocked_recording && harmony.nb_filled_record>3) + wake_up_interruptible(&harmony.wq_record); + } + return IRQ_HANDLED; +} + +/* + * Sound playing functions + */ + +static struct file_operations harmony_audio_fops = { + .owner = THIS_MODULE, + .llseek = no_llseek, + .read = harmony_audio_read, + .write = harmony_audio_write, + .poll = harmony_audio_poll, + .ioctl = harmony_audio_ioctl, + .open = harmony_audio_open, + .release = harmony_audio_release, +}; + +static int harmony_audio_init(void) +{ + /* Request that IRQ */ + if (request_irq(harmony.dev->irq, harmony_interrupt, 0 ,"harmony", &harmony)) { + printk(KERN_ERR PFX "Error requesting irq %d.\n", harmony.dev->irq); + return -EFAULT; + } + + harmony.dsp_unit = register_sound_dsp(&harmony_audio_fops, -1); + if (harmony.dsp_unit < 0) { + printk(KERN_ERR PFX "Error registering dsp\n"); + free_irq(harmony.dev->irq, &harmony); + return -EFAULT; + } + + /* Clear the buffers so you don't end up with crap in the buffers. */ + harmony_silence(&played_buf, 0, HARMONY_BUF_SIZE*MAX_BUFS); + + /* Make sure this makes it to cache */ + CHECK_WBACK_INV_OFFSET(played_buf, 0, HARMONY_BUF_SIZE*MAX_BUFS); + + /* Clear out the silent buffer and flush to cache */ + harmony_silence(&silent, 0, HARMONY_BUF_SIZE); + CHECK_WBACK_INV_OFFSET(silent, 0, HARMONY_BUF_SIZE); + + harmony.audio_open = 0; + + return 0; +} + + +/* + * mixer functions + */ + +static void harmony_mixer_set_gain(void) +{ + harmony_wait_CNTL(); + gsc_writel(harmony.current_gain, &harmony.hpa->gainctl); +} + +/* + * Read gain of selected channel. + * The OSS rate is from 0 (silent) to 100 -> need some conversions + * + * The harmony gain are attenuation for output and monitor gain. + * is amplifaction for input gain + */ +#define to_harmony_level(level,max) ((level)*max/100) +#define to_oss_level(level,max) ((level)*100/max) + +static int harmony_mixer_get_level(int channel) +{ + int left_level; + int right_level; + + switch (channel) { + case SOUND_MIXER_VOLUME: + left_level = (harmony.current_gain & GAIN_LO_MASK) >> GAIN_LO_SHIFT; + right_level = (harmony.current_gain & GAIN_RO_MASK) >> GAIN_RO_SHIFT; + left_level = to_oss_level(MAX_OUTPUT_LEVEL - left_level, MAX_OUTPUT_LEVEL); + right_level = to_oss_level(MAX_OUTPUT_LEVEL - right_level, MAX_OUTPUT_LEVEL); + return (right_level << 8)+left_level; + + case SOUND_MIXER_IGAIN: + left_level = (harmony.current_gain & GAIN_LI_MASK) >> GAIN_LI_SHIFT; + right_level= (harmony.current_gain & GAIN_RI_MASK) >> GAIN_RI_SHIFT; + left_level = to_oss_level(left_level, MAX_INPUT_LEVEL); + right_level= to_oss_level(right_level, MAX_INPUT_LEVEL); + return (right_level << 8)+left_level; + + case SOUND_MIXER_MONITOR: + left_level = (harmony.current_gain & GAIN_MA_MASK) >> GAIN_MA_SHIFT; + left_level = to_oss_level(MAX_MONITOR_LEVEL-left_level, MAX_MONITOR_LEVEL); + return (left_level << 8)+left_level; + } + return -EINVAL; +} + + + +/* + * Some conversions for the same reasons. + * We give back the new real value(s) due to + * the rescale. + */ + +static int harmony_mixer_set_level(int channel, int value) +{ + int left_level; + int right_level; + int new_left_level; + int new_right_level; + + right_level = (value & 0x0000ff00) >> 8; + left_level = value & 0x000000ff; + if (right_level > 100) right_level = 100; + if (left_level > 100) left_level = 100; + + switch (channel) { + case SOUND_MIXER_VOLUME: + right_level = to_harmony_level(100-right_level, MAX_OUTPUT_LEVEL); + left_level = to_harmony_level(100-left_level, MAX_OUTPUT_LEVEL); + new_right_level = to_oss_level(MAX_OUTPUT_LEVEL - right_level, MAX_OUTPUT_LEVEL); + new_left_level = to_oss_level(MAX_OUTPUT_LEVEL - left_level, MAX_OUTPUT_LEVEL); + harmony.current_gain = (harmony.current_gain & ~(GAIN_LO_MASK | GAIN_RO_MASK)) + | (left_level << GAIN_LO_SHIFT) | (right_level << GAIN_RO_SHIFT); + harmony_mixer_set_gain(); + return (new_right_level << 8) + new_left_level; + + case SOUND_MIXER_IGAIN: + right_level = to_harmony_level(right_level, MAX_INPUT_LEVEL); + left_level = to_harmony_level(left_level, MAX_INPUT_LEVEL); + new_right_level = to_oss_level(right_level, MAX_INPUT_LEVEL); + new_left_level = to_oss_level(left_level, MAX_INPUT_LEVEL); + harmony.current_gain = (harmony.current_gain & ~(GAIN_LI_MASK | GAIN_RI_MASK)) + | (left_level << GAIN_LI_SHIFT) | (right_level << GAIN_RI_SHIFT); + harmony_mixer_set_gain(); + return (new_right_level << 8) + new_left_level; + + case SOUND_MIXER_MONITOR: + left_level = to_harmony_level(100-left_level, MAX_MONITOR_LEVEL); + new_left_level = to_oss_level(MAX_MONITOR_LEVEL-left_level, MAX_MONITOR_LEVEL); + harmony.current_gain = (harmony.current_gain & ~GAIN_MA_MASK) | (left_level << GAIN_MA_SHIFT); + harmony_mixer_set_gain(); + return (new_left_level << 8) + new_left_level; + } + + return -EINVAL; +} + +#undef to_harmony_level +#undef to_oss_level + +/* + * Return the selected input device (mic or line) + */ + +static int harmony_mixer_get_recmask(void) +{ + int current_input_line; + + current_input_line = (harmony.current_gain & GAIN_IS_MASK) + >> GAIN_IS_SHIFT; + if (current_input_line) + return SOUND_MASK_MIC; + + return SOUND_MASK_LINE; +} + +/* + * Set the input (only one at time, arbitrary priority to line in) + */ + +static int harmony_mixer_set_recmask(int recmask) +{ + int new_input_line; + int new_input_mask; + int current_input_line; + + current_input_line = (harmony.current_gain & GAIN_IS_MASK) + >> GAIN_IS_SHIFT; + if ((current_input_line && ((recmask & SOUND_MASK_LINE) || !(recmask & SOUND_MASK_MIC))) || + (!current_input_line && ((recmask & SOUND_MASK_LINE) && !(recmask & SOUND_MASK_MIC)))) { + new_input_line = 0; + new_input_mask = SOUND_MASK_LINE; + } else { + new_input_line = 1; + new_input_mask = SOUND_MASK_MIC; + } + harmony.current_gain = ((harmony.current_gain & ~GAIN_IS_MASK) | + (new_input_line << GAIN_IS_SHIFT )); + harmony_mixer_set_gain(); + return new_input_mask; +} + + +/* + * give the active outlines + */ + +static int harmony_mixer_get_outmask(void) +{ + int outmask = 0; + + if (harmony.current_gain & GAIN_SE_MASK) outmask |= MASK_INTERNAL; + if (harmony.current_gain & GAIN_LE_MASK) outmask |= MASK_LINEOUT; + if (harmony.current_gain & GAIN_HE_MASK) outmask |= MASK_HEADPHONES; + + return outmask; +} + + +static int harmony_mixer_set_outmask(int outmask) +{ + if (outmask & MASK_INTERNAL) + harmony.current_gain |= GAIN_SE_MASK; + else + harmony.current_gain &= ~GAIN_SE_MASK; + + if (outmask & MASK_LINEOUT) + harmony.current_gain |= GAIN_LE_MASK; + else + harmony.current_gain &= ~GAIN_LE_MASK; + + if (outmask & MASK_HEADPHONES) + harmony.current_gain |= GAIN_HE_MASK; + else + harmony.current_gain &= ~GAIN_HE_MASK; + + harmony_mixer_set_gain(); + + return (outmask & (MASK_INTERNAL | MASK_LINEOUT | MASK_HEADPHONES)); +} + +/* + * This code is inspired from sb_mixer.c + */ + +static int harmony_mixer_ioctl(struct inode * inode, struct file * file, + unsigned int cmd, unsigned long arg) +{ + int val; + int ret; + + if (cmd == SOUND_MIXER_INFO) { + mixer_info info; + memset(&info, 0, sizeof(info)); + strncpy(info.id, "harmony", sizeof(info.id)-1); + strncpy(info.name, "Harmony audio", sizeof(info.name)-1); + info.modify_counter = 1; /* ? */ + if (copy_to_user((void *)arg, &info, sizeof(info))) + return -EFAULT; + return 0; + } + + if (cmd == OSS_GETVERSION) + return put_user(SOUND_VERSION, (int *)arg); + + /* read */ + val = 0; + if (_SIOC_DIR(cmd) & _SIOC_WRITE) + if (get_user(val, (int *)arg)) + return -EFAULT; + + switch (cmd) { + case MIXER_READ(SOUND_MIXER_CAPS): + ret = SOUND_CAP_EXCL_INPUT; + break; + case MIXER_READ(SOUND_MIXER_STEREODEVS): + ret = SOUND_MASK_VOLUME | SOUND_MASK_IGAIN; + break; + + case MIXER_READ(SOUND_MIXER_RECMASK): + ret = SOUND_MASK_MIC | SOUND_MASK_LINE; + break; + case MIXER_READ(SOUND_MIXER_DEVMASK): + ret = SOUND_MASK_VOLUME | SOUND_MASK_IGAIN | + SOUND_MASK_MONITOR; + break; + case MIXER_READ(SOUND_MIXER_OUTMASK): + ret = MASK_INTERNAL | MASK_LINEOUT | + MASK_HEADPHONES; + break; + + case MIXER_WRITE(SOUND_MIXER_RECSRC): + ret = harmony_mixer_set_recmask(val); + break; + case MIXER_READ(SOUND_MIXER_RECSRC): + ret = harmony_mixer_get_recmask(); + break; + + case MIXER_WRITE(SOUND_MIXER_OUTSRC): + ret = harmony_mixer_set_outmask(val); + break; + case MIXER_READ(SOUND_MIXER_OUTSRC): + ret = harmony_mixer_get_outmask(); + break; + + case MIXER_WRITE(SOUND_MIXER_VOLUME): + case MIXER_WRITE(SOUND_MIXER_IGAIN): + case MIXER_WRITE(SOUND_MIXER_MONITOR): + ret = harmony_mixer_set_level(cmd & 0xff, val); + break; + + case MIXER_READ(SOUND_MIXER_VOLUME): + case MIXER_READ(SOUND_MIXER_IGAIN): + case MIXER_READ(SOUND_MIXER_MONITOR): + ret = harmony_mixer_get_level(cmd & 0xff); + break; + + default: + return -EINVAL; + } + + if (put_user(ret, (int *)arg)) + return -EFAULT; + return 0; +} + + +static int harmony_mixer_open(struct inode *inode, struct file *file) +{ + if (harmony.mixer_open) + return -EBUSY; + harmony.mixer_open = 1; + return 0; +} + +static int harmony_mixer_release(struct inode *inode, struct file *file) +{ + if (!harmony.mixer_open) + return -EBUSY; + harmony.mixer_open = 0; + return 0; +} + +static struct file_operations harmony_mixer_fops = { + .owner = THIS_MODULE, + .llseek = no_llseek, + .open = harmony_mixer_open, + .release = harmony_mixer_release, + .ioctl = harmony_mixer_ioctl, +}; + + +/* + * Mute all the output and reset Harmony. + */ + +static void __init harmony_mixer_reset(void) +{ + harmony.current_gain = GAIN_TOTAL_SILENCE; + harmony_mixer_set_gain(); + harmony_wait_CNTL(); + gsc_writel(1, &harmony.hpa->reset); + mdelay(50); /* wait 50 ms */ + gsc_writel(0, &harmony.hpa->reset); + harmony.current_gain = GAIN_DEFAULT; + harmony_mixer_set_gain(); +} + +static int __init harmony_mixer_init(void) +{ + /* Register the device file operations */ + harmony.mixer_unit = register_sound_mixer(&harmony_mixer_fops, -1); + if (harmony.mixer_unit < 0) { + printk(KERN_WARNING PFX "Error Registering Mixer Driver\n"); + return -EFAULT; + } + + harmony_mixer_reset(); + harmony.mixer_open = 0; + + return 0; +} + + + +/* + * This is the callback that's called by the inventory hardware code + * if it finds a match to the registered driver. + */ +static int __devinit +harmony_driver_probe(struct parisc_device *dev) +{ + u8 id; + u8 rev; + u32 cntl; + int ret; + + if (harmony.hpa) { + /* We only support one Harmony at this time */ + printk(KERN_ERR PFX "driver already registered\n"); + return -EBUSY; + } + + if (!dev->irq) { + printk(KERN_ERR PFX "no irq found\n"); + return -ENODEV; + } + + /* Set the HPA of harmony */ + harmony.hpa = (struct harmony_hpa *)dev->hpa; + harmony.dev = dev; + + /* Grab the ID and revision from the device */ + id = gsc_readb(&harmony.hpa->id); + if ((id | 1) != 0x15) { + printk(KERN_WARNING PFX "wrong harmony id 0x%02x\n", id); + return -EBUSY; + } + cntl = gsc_readl(&harmony.hpa->cntl); + rev = (cntl>>20) & 0xff; + + printk(KERN_INFO "Lasi Harmony Audio driver " HARMONY_VERSION ", " + "h/w id %i, rev. %i at 0x%lx, IRQ %i\n", + id, rev, dev->hpa, harmony.dev->irq); + + /* Make sure the control bit isn't set, although I don't think it + ever is. */ + if (cntl & CNTL_C) { + printk(KERN_WARNING PFX "CNTL busy\n"); + harmony.hpa = 0; + return -EBUSY; + } + + /* Initialize the memory buffers */ + if (harmony_alloc_buffer(&played_buf, MAX_BUFS) || + harmony_alloc_buffer(&recorded_buf, MAX_BUFS) || + harmony_alloc_buffer(&graveyard, 1) || + harmony_alloc_buffer(&silent, 1)) { + ret = -EBUSY; + goto out_err; + } + + /* Initialize /dev/mixer and /dev/audio */ + if ((ret=harmony_mixer_init())) + goto out_err; + if ((ret=harmony_audio_init())) + goto out_err; + + return 0; + +out_err: + harmony.hpa = 0; + harmony_free_buffer(&played_buf); + harmony_free_buffer(&recorded_buf); + harmony_free_buffer(&graveyard); + harmony_free_buffer(&silent); + return ret; +} + + +static struct parisc_device_id harmony_tbl[] = { + /* { HPHW_FIO, HVERSION_REV_ANY_ID, HVERSION_ANY_ID, 0x0007A }, Bushmaster/Flounder */ + { HPHW_FIO, HVERSION_REV_ANY_ID, HVERSION_ANY_ID, 0x0007B }, /* 712/715 Audio */ + { HPHW_FIO, HVERSION_REV_ANY_ID, HVERSION_ANY_ID, 0x0007E }, /* Pace Audio */ + { HPHW_FIO, HVERSION_REV_ANY_ID, HVERSION_ANY_ID, 0x0007F }, /* Outfield / Coral II */ + { 0, } +}; + +MODULE_DEVICE_TABLE(parisc, harmony_tbl); + +static struct parisc_driver harmony_driver = { + .name = "Lasi Harmony", + .id_table = harmony_tbl, + .probe = harmony_driver_probe, +}; + +static int __init init_harmony(void) +{ + return register_parisc_driver(&harmony_driver); +} + +static void __exit cleanup_harmony(void) +{ + free_irq(harmony.dev->irq, &harmony); + unregister_sound_mixer(harmony.mixer_unit); + unregister_sound_dsp(harmony.dsp_unit); + harmony_free_buffer(&played_buf); + harmony_free_buffer(&recorded_buf); + harmony_free_buffer(&graveyard); + harmony_free_buffer(&silent); + unregister_parisc_driver(&harmony_driver); +} + + +MODULE_AUTHOR("Alex DeVries <alex@onefishtwo.ca>"); +MODULE_DESCRIPTION("Harmony sound driver"); +MODULE_LICENSE("GPL"); + +module_init(init_harmony); +module_exit(cleanup_harmony); + |