android_kernel_cmhtcleo/sound/htcleo/alsa-pcm-htc-leo.c
tytung e7f4d881c0 ALSA: Add new ALSA driver for QDSP6 sound system
This driver works on the htc leo.

The driver was written by Cotulla

Signed-off-by: Denis 'GNUtoo' Carikli <GNUtoo@no-log.org>
2011-10-31 23:53:43 +08:00

774 lines
19 KiB
C

/* linux/sound/htcleo/alsa-htc-leo.c
*
* Copyright (c) 2011, DFT, Cotulla
* Copyright (c) 2009, Code Aurora Forum. All rights reserved.
*
* All source code in this file is licensed under the following license except
* where indicated.
*
* 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.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
* See the GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program; if not, you can find it at http://www.fsf.org.
*/
/*
Cotulla:
implemented playback and capture
this file use q6audio_* function primary, some new was added
*/
#include <linux/init.h>
#include <linux/err.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/time.h>
#include <linux/wait.h>
#include <linux/kthread.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/dma-mapping.h>
#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/pcm_params.h>
#include <sound/initval.h>
#include <sound/control.h>
#include <asm/dma.h>
#include "alsa-htc-leo.h"
#if 1
#define DBG(fmt, arg...) \
printk("[ALSA-PCM] %s " fmt "\n", __func__, ## arg)
#else
#define DBG(fmt, arg...) \
do {} while (0);
#endif
static int qsd_audio_volume_update(struct qsd_audio *prtd);
static int qsd_pcm_mmap(struct snd_pcm_substream *substream, struct vm_area_struct *vma);
//-----------------------------------------------------------------------------------
// Fake DMA code
//------------------------------------------------------------------------
//
// this thread emulate DMA transfer by QDSP
//
static int snd_qsd_fake_playback_dma_thread(void *data)
{
int rc = 0;
int rcopy;
struct snd_pcm_substream *substream = (struct snd_pcm_substream *)data;
struct snd_pcm_runtime *runtime = substream->runtime;
struct qsd_audio *prtd = runtime->private_data;
struct audio_buffer *ab;
struct audio_client *ac;
unsigned char *ptr;
while (prtd->thread_exit == 0)
{
DBG(" start handler");
// wait until state is changed
// at this point this thread waits until SNDRV_PCM_TRIGGER_START comes
// SNDRV_PCM_TRIGGER_STOP
//
wait_event_interruptible(prtd->fake_wait, (prtd->start == 1 || prtd->thread_exit == 1));
DBG(" wake up %d %d", prtd->start, prtd->thread_exit);
if (prtd->thread_exit) break;
ac = prtd->ac;
while (prtd->start == 1)
{
// wait until new buffer appear
//
// DBG(" ac = %X, prtd = %X, substream = %X", ac, prtd, substream);
ab = ac->buf + ac->cpu_buf;
if (ab->used)
{
DBG("wait a buffer %d", ac->cpu_buf);
if (!wait_event_timeout(ac->wait, (ab->used == 0), 5 * HZ))
{
DBG("timeout. dsp dead?\n");
rc = -EFAULT;
goto fail;
}
// some buffer become free, report about state change now
//
snd_pcm_period_elapsed(prtd->substream);
}
mutex_lock(&prtd->mlock);
if (prtd->start == 0)
{
DBG("flag is set - exit thread");
mutex_unlock(&prtd->mlock);
break; // EXIT FROM LOOP
}
// send buffer to DSP
//
ptr = runtime->dma_area + prtd->buf_curoff;
rcopy = prtd->buf_chunk;
if (rcopy > ab->size)
rcopy = ab->size;
memcpy(ab->data, ptr, rcopy);
ab->used = rcopy;
q6audio_write(ac, ab);
ac->cpu_buf ^= 1;
// move fake dma pointer forward
//
spin_lock(&prtd->lock);
prtd->buf_curoff += prtd->buf_chunk;
if (prtd->buf_curoff >= prtd->buf_maxoff)
prtd->buf_curoff = 0;
spin_unlock(&prtd->lock);
// update stream settings if required
//
if (qsd_glb_ctl.update)
{
rc = qsd_audio_volume_update(prtd);
qsd_glb_ctl.update = 0;
}
mutex_unlock(&prtd->mlock);
fail:
; // CotullaTODO: handle this fault somehow.
}
DBG(" end handler");
}
DBG(" exit");
return 0;
}
static int snd_qsd_fake_capture_dma_thread(void *data)
{
int rc = 0;
int rcopy;
struct snd_pcm_substream *substream = (struct snd_pcm_substream *)data;
struct snd_pcm_runtime *runtime = substream->runtime;
struct qsd_audio *prtd = runtime->private_data;
struct audio_buffer *ab;
struct audio_client *ac;
unsigned char *ptr;
while (prtd->thread_exit == 0)
{
DBG(" start handler");
// wait until state is changed
// at this point this thread waits until SNDRV_PCM_TRIGGER_START comes
// SNDRV_PCM_TRIGGER_STOP
//
wait_event_interruptible(prtd->fake_wait, (prtd->start == 1 || prtd->thread_exit == 1));
DBG(" wake up %d %d", prtd->start, prtd->thread_exit);
if (prtd->thread_exit) break;
ac = prtd->ac;
while (prtd->start == 1)
{
// wait until new buffer appear
//
// DBG(" ac = %X, prtd = %X, substream = %X", ac, prtd, substream);
ab = ac->buf + ac->cpu_buf;
if (ab->used)
{
DBG("wait a buffer %d", ac->cpu_buf);
if (!wait_event_timeout(ac->wait, (ab->used == 0), 5 * HZ))
{
DBG("timeout. dsp dead?\n");
rc = -EFAULT;
goto fail;
}
// some buffer become free, report about state change now
//
snd_pcm_period_elapsed(prtd->substream);
}
mutex_lock(&prtd->mlock);
if (prtd->start == 0)
{
DBG("flag is set - exit thread");
mutex_unlock(&prtd->mlock);
break; // EXIT FROM LOOP
}
// this buffer must contains recorded data
// copy it to dma buffer
//
ptr = runtime->dma_area + prtd->buf_curoff;
rcopy = prtd->buf_chunk;
if (rcopy > ab->size)
rcopy = ab->size;
memcpy(ptr, ab->data, rcopy);
// send this buffer to DSP queue again
//
ab->used = rcopy;
q6audio_read(ac, ab);
ac->cpu_buf ^= 1;
// move fake dma pointer forward
//
spin_lock(&prtd->lock);
prtd->buf_curoff += prtd->buf_chunk;
if (prtd->buf_curoff >= prtd->buf_maxoff)
prtd->buf_curoff = 0;
spin_unlock(&prtd->lock);
mutex_unlock(&prtd->mlock);
fail:
; // CotullaTODO: handle this fault somehow.
}
DBG(" end handler");
}
DBG(" exit");
return 0;
}
//-----------------------------------------------------------------------------------
struct snd_qsd
{
struct snd_card *card;
struct snd_pcm *pcm;
};
struct qsd_ctl qsd_glb_ctl;
EXPORT_SYMBOL(qsd_glb_ctl);
struct audio_locks the_locks;
EXPORT_SYMBOL(the_locks);
static struct snd_pcm_hardware qsd_pcm_playback_hardware =
{
.info = SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID | SNDRV_PCM_INFO_INTERLEAVED,
.formats = USE_FORMATS,
.rates = USE_RATE,
.rate_min = USE_RATE_MIN,
.rate_max = USE_RATE_MAX,
.channels_min = USE_CHANNELS_MIN,
.channels_max = USE_CHANNELS_MAX,
.buffer_bytes_max = MAX_BUFFER_SIZE,
.period_bytes_min = MIN_PERIOD_SIZE,
.period_bytes_max = MAX_PERIOD_SIZE,
.periods_min = USE_PERIODS_MIN,
.periods_max = USE_PERIODS_MAX,
.fifo_size = 0,
};
static struct snd_pcm_hardware qsd_pcm_capture_hardware =
{
.info = SNDRV_PCM_INFO_INTERLEAVED,
.formats = USE_FORMATS,
.rates = USE_RATE,
.rate_min = USE_RATE_MIN,
.rate_max = USE_RATE_MAX,
.channels_min = USE_CHANNELS_MIN,
.channels_max = USE_CHANNELS_MAX,
.buffer_bytes_max = MAX_BUFFER_SIZE,
.period_bytes_min = MIN_PERIOD_SIZE,
.period_bytes_max = MAX_PERIOD_SIZE,
.periods_min = USE_PERIODS_MIN,
.periods_max = USE_PERIODS_MAX,
.fifo_size = 0,
};
static int qsd_audio_volume_update(struct qsd_audio *prtd)
{
int rc = 0;
DBG("updating volume");
if (!prtd->ac)
{
DBG("prtd->ac == NULL");
return -EINVAL;
}
rc = q6audio_set_stream_volume(prtd->ac, qsd_glb_ctl.strm_volume);
if (rc)
{
DBG("set stream volume failed\n");
}
return rc;
}
static int qsd_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
int ret = 0;
struct snd_pcm_runtime *runtime = substream->runtime;
struct qsd_audio *prtd = runtime->private_data;
DBG("%d %X %X", cmd, prtd, prtd->ac);
switch (cmd)
{
case SNDRV_PCM_TRIGGER_START:
DBG("TRIGGER_START");
prtd->start = 1;
wake_up(&prtd->fake_wait);
break;
case SNDRV_PCM_TRIGGER_RESUME:
case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
break;
case SNDRV_PCM_TRIGGER_STOP:
DBG("TRIGGER_STOP");
prtd->start = 0;
wake_up(&prtd->fake_wait);
break;
case SNDRV_PCM_TRIGGER_SUSPEND:
case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
break;
default:
ret = -EINVAL;
}
return ret;
}
static int hw_rule_periodsize_by_rate(struct snd_pcm_hw_params *params, struct snd_pcm_hw_rule *rule)
{
struct snd_interval *ps = hw_param_interval(params, SNDRV_PCM_HW_PARAM_PERIOD_SIZE);
struct snd_interval *r = hw_param_interval(params, SNDRV_PCM_HW_PARAM_RATE);
struct snd_interval ch;
if (!ps || !r)
return 0;
snd_interval_any(&ch);
if (r->min > 8000)
{
ch.min = 512;
pr_debug("Minimum period size is adjusted to 512\n");
return snd_interval_refine(ps, &ch);
}
return 0;
}
static int qsd_pcm_open(struct snd_pcm_substream *substream)
{
struct snd_pcm_runtime *runtime = substream->runtime;
struct qsd_audio *prtd;
int rc = 0;
DBG("+");
prtd = kzalloc(sizeof(struct qsd_audio), GFP_KERNEL);
if (prtd == NULL)
{
rc = -ENOMEM;
goto fail;
}
DBG(" prtd = %X", prtd);
spin_lock_init(&prtd->lock);
init_waitqueue_head(&prtd->fake_wait);
mutex_init(&prtd->mlock);
prtd->enabled = 0;
prtd->substream = substream;
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
{
DBG("Stream = SNDRV_PCM_STREAM_PLAYBACK");
runtime->hw = qsd_pcm_playback_hardware;
prtd->dir = SNDRV_PCM_STREAM_PLAYBACK;
}
else
{
DBG("Stream = SNDRV_PCM_STREAM_CAPTURE");
runtime->hw = qsd_pcm_capture_hardware;
prtd->dir = SNDRV_PCM_STREAM_CAPTURE;
}
/* Ensure that buffer size is a multiple of period size */
rc = snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS);
if (rc < 0)
{
kfree(prtd);
goto fail;
}
rc = snd_pcm_hw_rule_add(substream->runtime, 0,
SNDRV_PCM_HW_PARAM_PERIOD_SIZE,
hw_rule_periodsize_by_rate, substream,
SNDRV_PCM_HW_PARAM_RATE, -1);
if (rc < 0)
{
kfree(prtd);
goto fail;
}
runtime->private_data = prtd;
fail:
DBG("-: %d", rc);
return rc;
}
static snd_pcm_uframes_t qsd_pcm_pointer(struct snd_pcm_substream *substream)
{
struct snd_pcm_runtime *runtime = substream->runtime;
struct qsd_audio *prtd = runtime->private_data;
unsigned int res;
// DBG("");
res = prtd->buf_curoff;
if (res >= prtd->buf_maxoff) DBG("fatal overflow: %d > %d", prtd->buf_curoff, prtd->buf_maxoff);
DBG("res = %d", res);
return bytes_to_frames(substream->runtime, res);
}
static int qsd_pcm_close(struct snd_pcm_substream *substream)
{
struct snd_pcm_runtime *runtime = substream->runtime;
struct qsd_audio *prtd = runtime->private_data;
int ret = 0;
DBG("+");
if (prtd->enabled)
{
if (prtd->ac)
{
q6audio_close(prtd->ac);
}
}
prtd->enabled = 0;
kfree(prtd);
DBG("-");
return ret;
}
//-----------------------------------------------------------------------------------
// Playback
//-----------------------------------------------------------------------------------
static int qsd_pcm_playback_prepare(struct snd_pcm_substream *substream)
{
int rc = 0;
struct snd_pcm_runtime *runtime = substream->runtime;
struct qsd_audio *prtd = runtime->private_data;
unsigned long acdb_id = 0;
DBG ("+");
if (prtd->enabled)
{
DBG("already enabled");
goto fail;
}
prtd->buf_maxoff = snd_pcm_lib_buffer_bytes(substream);
prtd->buf_chunk = snd_pcm_lib_period_bytes(substream);
prtd->start = 0;
DBG("INFO:");
DBG("\tMaxSize = %d, MaxPeriod = %d", prtd->buf_maxoff, prtd->buf_chunk);
DBG("\tRate = %d, Channels = %d", runtime->rate, runtime->channels);
acdb_id = qsd_glb_ctl.acdb_tx_id;
prtd->ac = q6audio_open_pcm(prtd->buf_chunk, runtime->rate, runtime->channels, AUDIO_FLAG_WRITE, acdb_id);
if (prtd->ac == NULL)
{
DBG("q6audio_open_pcm() failed");
rc = -ENOMEM;
goto fail;
}
q6audio_set_stream_volume(prtd->ac, qsd_glb_ctl.strm_volume);
prtd->enabled = 1;
fail:
DBG ("-: %d", rc);
return rc;
}
//-----------------------------------------------------------------------------------
// Capture
//-----------------------------------------------------------------------------------
static int qsd_pcm_capture_prepare(struct snd_pcm_substream *substream)
{
struct snd_pcm_runtime *runtime = substream->runtime;
struct qsd_audio *prtd = runtime->private_data;
int rc = 0;
uint32_t acdb_id = 0;
if (prtd->enabled)
{
DBG("already enabled");
goto fail;
}
prtd->buf_maxoff = snd_pcm_lib_buffer_bytes(substream);
prtd->buf_chunk = snd_pcm_lib_period_bytes(substream);
prtd->start = 0;
DBG("INFO:");
DBG("\tMaxSize = %d, MaxPeriod = %d", prtd->buf_maxoff, prtd->buf_chunk);
DBG("\tRate = %d, Channels = %d", runtime->rate, runtime->channels);
acdb_id = qsd_glb_ctl.acdb_rx_id;
prtd->ac = q6audio_open_pcm(prtd->buf_chunk, runtime->rate, runtime->channels, AUDIO_FLAG_READ, acdb_id);
if (!prtd->ac)
{
DBG("q6audio_open_pcm fails");
rc = -ENOMEM;
goto fail;
}
prtd->enabled = 1;
fail:
DBG ("-: %d", rc);
return rc;
}
//-----------------------------------------------------------------------------------
// ALSA thunks
//-----------------------------------------------------------------------------------
static int qsd_pcm_prepare(struct snd_pcm_substream *substream)
{
int ret = 0;
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
ret = qsd_pcm_playback_prepare(substream);
else if (substream->stream == SNDRV_PCM_STREAM_CAPTURE)
ret = qsd_pcm_capture_prepare(substream);
return ret;
}
int qsd_pcm_hw_params(struct snd_pcm_substream *substream, struct snd_pcm_hw_params *params)
{
int rc = 0;
struct snd_pcm_runtime *runtime = substream->runtime;
struct qsd_audio *prtd = runtime->private_data;
unsigned long totbytes = params_buffer_bytes(params);
DBG("+: totbytes=%d", totbytes);
snd_pcm_lib_malloc_pages(substream, totbytes);
if (prtd->fake_dma_started == 0)
{
prtd->fake_dma_started = 1;
DBG("create thread");
prtd->thread_exit = 0;
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
{
prtd->fake_dma_thread = kthread_run(snd_qsd_fake_playback_dma_thread, (void*)substream, "leo_alsa_fake_playback_dma");
}
else if (substream->stream == SNDRV_PCM_STREAM_CAPTURE)
{
prtd->fake_dma_thread = kthread_run(snd_qsd_fake_capture_dma_thread, (void*)substream, "leo_alsa_fake_capture_dma");
}
else
{
DBG("Wrong direction?");
prtd->fake_dma_started = 0;
rc = -ENOMEM;
goto fail;
}
if (IS_ERR(prtd->fake_dma_thread))
{
rc = PTR_ERR(prtd->fake_dma_thread);
goto fail;
}
}
runtime->dma_bytes = totbytes;
fail:
DBG("-: %d", rc);
return 0;
}
static int qsd_pcm_hw_free(struct snd_pcm_substream *substream)
{
struct snd_pcm_runtime *runtime = substream->runtime;
struct qsd_audio *prtd = runtime->private_data;
DBG("+");
// Cotulla: this prevents freeing memory during execution code
// inside capture thread.
// if copy data / put buffer code executed inside thread
// this will wait util it finishes. then start = 0 will cancel the next loop.
//
mutex_lock(&prtd->mlock);
mutex_unlock(&prtd->mlock);
snd_pcm_lib_free_pages(substream);
if (prtd->fake_dma_started == 1)
{
prtd->fake_dma_started = 0;
prtd->thread_exit = 1;
wake_up(&prtd->fake_wait);
// Cotulla: POSSIBLE DEADLOCK IF THREAD DOES NOT EXIT
//
kthread_stop(prtd->fake_dma_thread);
}
DBG("-");
return 0;
}
struct snd_pcm_ops qsd_pcm_ops =
{
.open = qsd_pcm_open,
.close = qsd_pcm_close,
.hw_params = qsd_pcm_hw_params,
.hw_free = qsd_pcm_hw_free,
.ioctl = snd_pcm_lib_ioctl,
.prepare = qsd_pcm_prepare,
.trigger = qsd_pcm_trigger,
.pointer = qsd_pcm_pointer,
};
EXPORT_SYMBOL_GPL(qsd_pcm_ops);
/*
static int qsd_pcm_remove(struct platform_device *devptr)
{
struct snd_soc_device *socdev = platform_get_drvdata(devptr);
DBG("");
platform_set_drvdata(devptr, NULL);
return 0;
}
*/
//==================================================================================
// Init
//==================================================================================
int htcleo_alsa_init_pcm(struct snd_card *card)
{
struct snd_pcm *pcm;
int rc;
DBG("+");
rc = snd_pcm_new(card,
/* ID */ "htcleo_snd",
/* device */ 0,
/* playback count */ 1,
/* capture count */ 1, &pcm);
if (rc < 0)
{
DBG("snd_pcm_new() failed %d", rc);
goto fail;
}
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &qsd_pcm_ops);
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &qsd_pcm_ops);
pcm->private_data = 0;
pcm->info_flags = 0;
strcpy(pcm->name, card->shortname);
rc = snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_CONTINUOUS,
snd_dma_continuous_data(GFP_KERNEL), 64*1024, 64*1024);
if (rc < 0)
{
DBG("snd_pcm_lib_preallocate_pages_for_all() failed %d", rc);
goto fail;
}
fail:
DBG("-:%d", rc);
return rc;
}
MODULE_AUTHOR("DFT-Cotulla");
MODULE_DESCRIPTION("HTC LEO ALSA PCM driver");
MODULE_LICENSE("GPL v2");