blob: c07de51fec3a405f5188420e82c9a55766ab3570 [file] [log] [blame]
/*
* Copyright (c) 2016, Intel Corporation
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the Intel Corporation nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* Author: Liam Girdwood <liam.r.girdwood@linux.intel.com>
* Keyon Jie <yang.jie@linux.intel.com>
* Tomasz Lauda <tomasz.lauda@linux.intel.com>
*/
/**
* \file audio/volume.c
* \brief Volume component implementation
* \authors Liam Girdwood <liam.r.girdwood@linux.intel.com>\n
* Keyon Jie <yang.jie@linux.intel.com>\n
* Tomasz Lauda <tomasz.lauda@linux.intel.com>
*/
#include <stddef.h>
#include <errno.h>
#include <sof/sof.h>
#include <sof/lock.h>
#include <sof/list.h>
#include <sof/stream.h>
#include <sof/alloc.h>
#include <sof/work.h>
#include <sof/clock.h>
#include "volume.h"
/**
* \brief Synchronize host mmap() volume with real value.
* \param[in,out] cd Volume component private data.
* \param[in] chan Channel number.
*/
static void vol_sync_host(struct comp_data *cd, uint32_t chan)
{
if (cd->hvol == NULL)
return;
if (chan < SOF_IPC_MAX_CHANNELS)
cd->hvol[chan].value = cd->volume[chan];
else {
trace_volume_error("veh");
tracev_value(chan);
}
}
/**
* \brief Update volume with target value.
* \param[in,out] cd Volume component private data.
* \param[in] chan Channel number.
*/
static void vol_update(struct comp_data *cd, uint32_t chan)
{
cd->volume[chan] = cd->tvolume[chan];
vol_sync_host(cd, chan);
}
/**
* \brief Ramps volume changes over time.
* \param[in,out] data Volume base component device.
* \param[in] delay Update time.
* \return Time until next work.
*/
static uint64_t vol_work(void *data, uint64_t delay)
{
struct comp_dev *dev = (struct comp_dev *)data;
struct comp_data *cd = comp_get_drvdata(dev);
uint32_t vol;
int again = 0;
int i, new_vol;
/* inc/dec each volume if it's not at target */
for (i = 0; i < PLATFORM_MAX_CHANNELS; i++) {
/* skip if target reached */
if (cd->volume[i] == cd->tvolume[i])
continue;
vol = cd->volume[i];
if (cd->volume[i] < cd->tvolume[i]) {
/* ramp up */
vol += VOL_RAMP_STEP;
/* ramp completed ? */
if (vol >= cd->tvolume[i] || vol >= cd->max_volume)
vol_update(cd, i);
else {
cd->volume[i] = vol;
again = 1;
}
} else {
/* ramp down */
new_vol = vol - VOL_RAMP_STEP;
if (new_vol <= 0) {
/* cannot ramp down below 0 */
vol_update(cd, i);
} else {
/* ramp completed ? */
if (new_vol <= cd->tvolume[i] ||
new_vol <= cd->min_volume) {
vol_update(cd, i);
} else {
cd->volume[i] = new_vol;
again = 1;
}
}
}
/* sync host with new value */
vol_sync_host(cd, i);
}
/* do we need to continue ramping */
if (again)
return VOL_RAMP_US;
else
return 0;
}
/**
* \brief Validates and sets minimum and maximum volume levels.
* \details If max_vol < min_vol or it's equals 0 then set max_vol = VOL_MAX
* \param[in,out] cd Volume component private data.
* \param[in] min_vol Minimum volume level
* \param[in] max_vol Maximum volume level
*/
static void vol_set_min_max_levels(struct comp_data *cd,
uint32_t min_vol, uint32_t max_vol)
{
if (max_vol < min_vol || max_vol == 0)
cd->max_volume = VOL_ZERO_DB;
else
cd->max_volume = max_vol;
cd->min_volume = min_vol;
}
/**
* \brief Creates volume component.
* \param[in,out] data Volume base component device.
* \param[in] delay Update time.
* \return Pointer to volume base component device.
*/
static struct comp_dev *volume_new(struct sof_ipc_comp *comp)
{
struct comp_dev *dev;
struct sof_ipc_comp_volume *vol;
struct sof_ipc_comp_volume *ipc_vol =
(struct sof_ipc_comp_volume *)comp;
struct comp_data *cd;
int i;
trace_volume("new");
dev = rzalloc(RZONE_RUNTIME, SOF_MEM_CAPS_RAM,
COMP_SIZE(struct sof_ipc_comp_volume));
if (dev == NULL)
return NULL;
vol = (struct sof_ipc_comp_volume *)&dev->comp;
memcpy(vol, ipc_vol, sizeof(struct sof_ipc_comp_volume));
cd = rzalloc(RZONE_RUNTIME, SOF_MEM_CAPS_RAM, sizeof(*cd));
if (cd == NULL) {
rfree(dev);
return NULL;
}
comp_set_drvdata(dev, cd);
work_init(&cd->volwork, vol_work, dev, WORK_ASYNC);
/* set the default volumes */
for (i = 0; i < PLATFORM_MAX_CHANNELS; i++) {
cd->volume[i] = VOL_ZERO_DB;
cd->tvolume[i] = VOL_ZERO_DB;
}
/* set volume min/max levels */
vol_set_min_max_levels(cd, ipc_vol->min_value, ipc_vol->max_value);
dev->state = COMP_STATE_READY;
return dev;
}
/**
* \brief Frees volume component.
* \param[in,out] dev Volume base component device.
*/
static void volume_free(struct comp_dev *dev)
{
struct comp_data *cd = comp_get_drvdata(dev);
trace_volume("fre");
rfree(cd);
rfree(dev);
}
/**
* \brief Sets volume component audio stream parameters.
* \param[in,out] dev Volume base component device.
* \return Error code.
*
* All done in prepare() since we need to know source and sink component params.
*/
static int volume_params(struct comp_dev *dev)
{
struct comp_data *cd = comp_get_drvdata(dev);
trace_volume("par");
/* rewrite params format for all downstream */
dev->params.frame_fmt = cd->sink_format;
return 0;
}
/**
* \brief Sets channel target volume.
* \param[in,out] dev Volume base component device.
* \param[in] chan Channel number.
* \param[in] vol Target volume.
*/
static inline void volume_set_chan(struct comp_dev *dev, int chan, uint32_t vol)
{
struct comp_data *cd = comp_get_drvdata(dev);
uint32_t v = vol;
/* Limit received volume gain to MIN..MAX range before applying it.
* MAX is needed for now for the generic C gain arithmetics to prevent
* multiplication overflow with the 32 bit value. Non-zero MIN option
* can be useful to prevent totally muted small volume gain.
*/
if (v <= cd->min_volume)
v = cd->min_volume;
if (v > cd->max_volume)
v = cd->max_volume;
cd->tvolume[chan] = v;
}
/**
* \brief Mutes channel.
* \param[in,out] dev Volume base component device.
* \param[in] chan Channel number.
*/
static inline void volume_set_chan_mute(struct comp_dev *dev, int chan)
{
struct comp_data *cd = comp_get_drvdata(dev);
/* Check if not muted already */
if (cd->volume[chan] != 0)
cd->mvolume[chan] = cd->volume[chan];
cd->tvolume[chan] = 0;
}
/**
* \brief Unmutes channel.
* \param[in,out] dev Volume base component device.
* \param[in] chan Channel number.
*/
static inline void volume_set_chan_unmute(struct comp_dev *dev, int chan)
{
struct comp_data *cd = comp_get_drvdata(dev);
/* Check if muted */
if (cd->volume[chan] == 0)
cd->tvolume[chan] = cd->mvolume[chan];
}
/**
* \brief Sets volume control command.
* \param[in,out] dev Volume base component device.
* \param[in,out] cdata Control command data.
* \return Error code.
*/
static int volume_ctrl_set_cmd(struct comp_dev *dev,
struct sof_ipc_ctrl_data *cdata)
{
struct comp_data *cd = comp_get_drvdata(dev);
int i;
int j;
/* validate */
if (cdata->num_elems == 0 || cdata->num_elems > SOF_IPC_MAX_CHANNELS) {
trace_volume_error("gs0");
return -EINVAL;
}
switch (cdata->cmd) {
case SOF_CTRL_CMD_VOLUME:
trace_volume("vst");
trace_value(cdata->comp_id);
for (j = 0; j < cdata->num_elems; j++) {
trace_value(cdata->chanv[j].channel);
trace_value(cdata->chanv[j].value);
i = cdata->chanv[j].channel;
if ((i >= 0) && (i < SOF_IPC_MAX_CHANNELS))
volume_set_chan(dev, i, cdata->chanv[j].value);
else {
trace_volume_error("gs2");
tracev_value(i);
}
}
work_schedule_default(&cd->volwork, VOL_RAMP_US);
break;
case SOF_CTRL_CMD_SWITCH:
trace_volume("mst");
trace_value(cdata->comp_id);
for (j = 0; j < cdata->num_elems; j++) {
trace_value(cdata->chanv[j].channel);
trace_value(cdata->chanv[j].value);
i = cdata->chanv[j].channel;
if ((i >= 0) && (i < SOF_IPC_MAX_CHANNELS)) {
if (cdata->chanv[j].value)
volume_set_chan_unmute(dev, i);
else
volume_set_chan_mute(dev, i);
} else {
trace_volume_error("gs3");
tracev_value(i);
}
}
work_schedule_default(&cd->volwork, VOL_RAMP_US);
break;
default:
trace_volume_error("gs1");
return -EINVAL;
}
return 0;
}
/**
* \brief Gets volume control command.
* \param[in,out] dev Volume base component device.
* \param[in,out] cdata Control command data.
* \return Error code.
*/
static int volume_ctrl_get_cmd(struct comp_dev *dev,
struct sof_ipc_ctrl_data *cdata)
{
struct comp_data *cd = comp_get_drvdata(dev);
int j;
/* validate */
if (cdata->num_elems == 0 || cdata->num_elems > SOF_IPC_MAX_CHANNELS) {
trace_volume_error("gc0");
tracev_value(cdata->num_elems);
return -EINVAL;
}
if (cdata->cmd == SOF_CTRL_CMD_VOLUME ||
cdata->cmd == SOF_CTRL_CMD_SWITCH) {
trace_volume("vgt");
trace_value(cdata->comp_id);
for (j = 0; j < cdata->num_elems; j++) {
cdata->chanv[j].channel = j;
cdata->chanv[j].value = cd->tvolume[j];
trace_value(cdata->chanv[j].channel);
trace_value(cdata->chanv[j].value);
}
} else {
trace_volume_error("ec2");
return -EINVAL;
}
return 0;
}
/**
* \brief Used to pass standard and bespoke commands (with data) to component.
* \param[in,out] dev Volume base component device.
* \param[in] cmd Command type.
* \param[in,out] data Control command data.
* \return Error code.
*/
static int volume_cmd(struct comp_dev *dev, int cmd, void *data)
{
struct sof_ipc_ctrl_data *cdata = data;
trace_volume("cmd");
switch (cmd) {
case COMP_CMD_SET_VALUE:
return volume_ctrl_set_cmd(dev, cdata);
case COMP_CMD_GET_VALUE:
return volume_ctrl_get_cmd(dev, cdata);
default:
return -EINVAL;
}
}
/**
* \brief Sets volume component state.
* \param[in,out] dev Volume base component device.
* \param[in] cmd Command type.
* \return Error code.
*/
static int volume_trigger(struct comp_dev *dev, int cmd)
{
trace_volume("trg");
return comp_set_state(dev, cmd);
}
/**
* \brief Copies and processes stream data.
* \param[in,out] dev Volume base component device.
* \return Error code.
*/
static int volume_copy(struct comp_dev *dev)
{
struct comp_data *cd = comp_get_drvdata(dev);
struct comp_buffer *sink;
struct comp_buffer *source;
tracev_volume("cpy");
/* volume components will only ever have 1 source and 1 sink buffer */
source = list_first_item(&dev->bsource_list,
struct comp_buffer, sink_list);
sink = list_first_item(&dev->bsink_list,
struct comp_buffer, source_list);
/* make sure source component buffer has enough data available and that
* the sink component buffer has enough free bytes for copy. Also
* check for XRUNs
*/
if (source->avail < cd->source_period_bytes) {
trace_volume_error("xru");
comp_underrun(dev, source, cd->source_period_bytes, 0);
return -EIO; /* xrun */
}
if (sink->free < cd->sink_period_bytes) {
trace_volume_error("xro");
comp_overrun(dev, sink, cd->sink_period_bytes, 0);
return -EIO; /* xrun */
}
/* copy and scale volume */
cd->scale_vol(dev, sink, source);
/* calc new free and available */
comp_update_buffer_produce(sink, cd->sink_period_bytes);
comp_update_buffer_consume(source, cd->source_period_bytes);
return dev->frames;
}
/**
* \brief Prepares volume component for processing.
* \param[in,out] dev Volume base component device.
* \return Error code.
*
* Volume component is usually first and last in pipelines so it makes sense
* to also do some type of conversion here.
*/
static int volume_prepare(struct comp_dev *dev)
{
struct comp_data *cd = comp_get_drvdata(dev);
struct comp_buffer *sinkb;
struct comp_buffer *sourceb;
struct sof_ipc_comp_config *sconfig;
struct sof_ipc_comp_config *config = COMP_GET_CONFIG(dev);
int i;
int ret;
trace_volume("pre");
ret = comp_set_state(dev, COMP_TRIGGER_PREPARE);
if (ret < 0)
return ret;
/* volume components will only ever have 1 source and 1 sink buffer */
sourceb = list_first_item(&dev->bsource_list,
struct comp_buffer, sink_list);
sinkb = list_first_item(&dev->bsink_list,
struct comp_buffer, source_list);
/* get source data format */
switch (sourceb->source->comp.type) {
case SOF_COMP_HOST:
case SOF_COMP_SG_HOST:
/* source format comes from IPC params */
cd->source_format = sourceb->source->params.frame_fmt;
cd->source_period_bytes = dev->frames *
comp_frame_bytes(sourceb->source);
break;
case SOF_COMP_DAI:
case SOF_COMP_SG_DAI:
default:
/* source format comes from DAI/comp config */
sconfig = COMP_GET_CONFIG(sourceb->source);
cd->source_format = sconfig->frame_fmt;
cd->source_period_bytes = dev->frames *
comp_frame_bytes(sourceb->source);
break;
}
/* get sink data format */
switch (sinkb->sink->comp.type) {
case SOF_COMP_HOST:
case SOF_COMP_SG_HOST:
/* sink format come from IPC params */
cd->sink_format = sinkb->sink->params.frame_fmt;
cd->sink_period_bytes = dev->frames *
comp_frame_bytes(sinkb->sink);
break;
case SOF_COMP_DAI:
case SOF_COMP_SG_DAI:
default:
/* sink format comes from DAI/comp config */
sconfig = COMP_GET_CONFIG(sinkb->sink);
cd->sink_format = sconfig->frame_fmt;
cd->sink_period_bytes = dev->frames *
comp_frame_bytes(sinkb->sink);
break;
}
/* rewrite params format for all downstream */
dev->params.frame_fmt = cd->sink_format;
dev->frame_bytes = cd->sink_period_bytes / dev->frames;
/* set downstream buffer size */
ret = buffer_set_size(sinkb, cd->sink_period_bytes *
config->periods_sink);
if (ret < 0) {
trace_volume_error("vp0");
goto err;
}
/* validate */
if (cd->sink_period_bytes == 0) {
trace_volume_error("vp1");
trace_error_value(dev->frames);
trace_error_value(sinkb->sink->frame_bytes);
ret = -EINVAL;
goto err;
}
if (cd->source_period_bytes == 0) {
trace_volume_error("vp2");
trace_error_value(dev->frames);
trace_error_value(sourceb->source->frame_bytes);
ret = -EINVAL;
goto err;
}
cd->scale_vol = vol_get_processing_function(dev);
if (!cd->scale_vol) {
trace_volume_error("vp3");
trace_error_value(cd->source_format);
trace_error_value(cd->sink_format);
trace_error_value(dev->params.channels);
ret = -EINVAL;
goto err;
}
for (i = 0; i < PLATFORM_MAX_CHANNELS; i++)
vol_sync_host(cd, i);
return 0;
err:
comp_set_state(dev, COMP_TRIGGER_RESET);
return ret;
}
/**
* \brief Resets volume component.
* \param[in,out] dev Volume base component device.
* \return Error code.
*/
static int volume_reset(struct comp_dev *dev)
{
trace_volume("res");
comp_set_state(dev, COMP_TRIGGER_RESET);
return 0;
}
/** \brief Volume component definition. */
struct comp_driver comp_volume = {
.type = SOF_COMP_VOLUME,
.ops = {
.new = volume_new,
.free = volume_free,
.params = volume_params,
.cmd = volume_cmd,
.trigger = volume_trigger,
.copy = volume_copy,
.prepare = volume_prepare,
.reset = volume_reset,
},
};
/**
* \brief Initializes volume component.
*/
void sys_comp_volume_init(void)
{
comp_register(&comp_volume);
}