| /* |
| 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); |
| |