rockbox/firmware/pcm_sw_volume.c
Michael Sevakis f5a5b94686 Implement universal in-PCM-driver software volume control.
Implements double-buffered volume, balance and prescaling control in
the main PCM driver when HAVE_SW_VOLUME_CONTROL is defined ensuring
that all PCM is volume controlled and level changes are low in latency.

Supports -73 to +6 dB using a 15-bit factor so that no large-integer
math is needed.

Low-level hardware drivers do not have to implement it themselves but
parameters can be changed (currently defined in pcm-internal.h) to work
best with a particular SoC or to provide different volume ranges.

Volume and prescale calls should be made in the codec driver. It should
appear as a normal hardware interface. PCM volume calls expect .1 dB
units.

Change-Id: Idf6316a64ef4fb8abcede10707e1e6c6d01d57db
Reviewed-on: http://gerrit.rockbox.org/423
Reviewed-by: Michael Sevakis <jethead71@rockbox.org>
Tested-by: Michael Sevakis <jethead71@rockbox.org>
2013-04-11 22:55:16 +02:00

264 lines
7.5 KiB
C

/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2013 by Michael Sevakis
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
#include "config.h"
#include "system.h"
#include "pcm.h"
#include "pcm-internal.h"
#include "dsp-util.h"
#include "fixedpoint.h"
#include "pcm_sw_volume.h"
/* source buffer from client */
static const void * volatile src_buf_addr = NULL;
static size_t volatile src_buf_rem = 0;
#define PCM_PLAY_DBL_BUF_SIZE (PCM_PLAY_DBL_BUF_SAMPLE*PCM_SAMPLE_SIZE)
/* double buffer and frame length control */
static int16_t pcm_dbl_buf[2][PCM_PLAY_DBL_BUF_SAMPLES*2]
PCM_DBL_BUF_BSS MEM_ALIGN_ATTR;
static size_t pcm_dbl_buf_size[2];
static int pcm_dbl_buf_num = 0;
static size_t frame_size;
static unsigned int frame_count, frame_err, frame_frac;
#ifdef AUDIOHW_HAVE_PRESCALER
static int32_t prescale_factor = PCM_FACTOR_UNITY;
static int32_t vol_factor_l = 0, vol_factor_r = 0;
#endif /* AUDIOHW_HAVE_PRESCALER */
/* pcm scaling factors */
static int32_t pcm_factor_l = 0, pcm_factor_r = 0;
#define PCM_FACTOR_CLIP(f) \
MAX(MIN((f), PCM_FACTOR_MAX), PCM_FACTOR_MIN)
#define PCM_SCALE_SAMPLE(f, s) \
(((f) * (s) + PCM_FACTOR_UNITY/2) >> PCM_FACTOR_BITS)
/* TODO: #include CPU-optimized routines and move this to /firmware/asm */
static inline void pcm_copy_buffer(int16_t *dst, const int16_t *src,
size_t size)
{
int32_t factor_l = pcm_factor_l;
int32_t factor_r = pcm_factor_r;
if (LIKELY(factor_l <= PCM_FACTOR_UNITY && factor_r <= PCM_FACTOR_UNITY))
{
/* All cut or unity */
while (size)
{
*dst++ = PCM_SCALE_SAMPLE(factor_l, *src++);
*dst++ = PCM_SCALE_SAMPLE(factor_r, *src++);
size -= PCM_SAMPLE_SIZE;
}
}
else
{
/* Any positive gain requires clipping */
while (size)
{
*dst++ = clip_sample_16(PCM_SCALE_SAMPLE(factor_l, *src++));
*dst++ = clip_sample_16(PCM_SCALE_SAMPLE(factor_r, *src++));
size -= PCM_SAMPLE_SIZE;
}
}
}
bool pcm_play_dma_complete_callback(enum pcm_dma_status status,
const void **addr, size_t *size)
{
/* Check status callback first if error */
if (status < PCM_DMAST_OK)
status = pcm_play_call_status_cb(status);
size_t sz = pcm_dbl_buf_size[pcm_dbl_buf_num];
if (status >= PCM_DMAST_OK && sz)
{
/* Do next chunk */
*addr = pcm_dbl_buf[pcm_dbl_buf_num];
*size = sz;
return true;
}
else
{
/* This is a stop chunk or error */
pcm_play_stop_int();
return false;
}
}
/* Equitably divide large source buffers amongst double buffer frames;
frames smaller than or equal to the double buffer chunk size will play
in one chunk */
static void update_frame_params(size_t size)
{
int count = size / PCM_SAMPLE_SIZE;
frame_count = (count + PCM_PLAY_DBL_BUF_SAMPLES - 1) /
PCM_PLAY_DBL_BUF_SAMPLES;
int perframe = count / frame_count;
frame_size = perframe * PCM_SAMPLE_SIZE;
frame_frac = count - perframe * frame_count;
frame_err = 0;
}
/* Obtain the next buffer and prepare it for pcm driver playback */
enum pcm_dma_status
pcm_play_dma_status_callback_int(enum pcm_dma_status status)
{
if (status != PCM_DMAST_STARTED)
return status;
size_t size = pcm_dbl_buf_size[pcm_dbl_buf_num];
const void *addr = src_buf_addr + size;
size = src_buf_rem - size;
if (size == 0 && pcm_get_more_int(&addr, &size))
{
update_frame_params(size);
pcm_play_call_status_cb(PCM_DMAST_STARTED);
}
src_buf_addr = addr;
src_buf_rem = size;
if (size != 0)
{
size = frame_size;
if ((frame_err += frame_frac) >= frame_count)
{
frame_err -= frame_count;
size += PCM_SAMPLE_SIZE;
}
}
pcm_dbl_buf_num ^= 1;
pcm_dbl_buf_size[pcm_dbl_buf_num] = size;
pcm_copy_buffer(pcm_dbl_buf[pcm_dbl_buf_num], addr, size);
return PCM_DMAST_OK;
}
/* Prefill double buffer and start pcm driver */
static void start_pcm(bool reframe)
{
pcm_dbl_buf_num = 0;
pcm_dbl_buf_size[0] = 0;
if (reframe)
update_frame_params(src_buf_rem);
pcm_play_dma_status_callback(PCM_DMAST_STARTED);
pcm_play_dma_status_callback(PCM_DMAST_STARTED);
pcm_play_dma_start(pcm_dbl_buf[1], pcm_dbl_buf_size[1]);
}
void pcm_play_dma_start_int(const void *addr, size_t size)
{
src_buf_addr = addr;
src_buf_rem = size;
start_pcm(true);
}
void pcm_play_dma_pause_int(bool pause)
{
if (pause)
pcm_play_dma_pause(true);
else if (src_buf_rem)
start_pcm(false); /* Reprocess in case volume level changed */
else
pcm_play_stop_int(); /* Playing frame was last frame */
}
void pcm_play_dma_stop_int(void)
{
pcm_play_dma_stop();
src_buf_addr = NULL;
src_buf_rem = 0;
}
/* Return playing buffer from the source buffer */
const void * pcm_play_dma_get_peak_buffer_int(int *count)
{
const void *addr = src_buf_addr;
size_t size = src_buf_rem;
const void *addr2 = src_buf_addr;
if (addr == addr2 && size)
{
*count = size / PCM_SAMPLE_SIZE;
return addr;
}
*count = 0;
return NULL;
}
/* Return the scale factor corresponding to the centibel level */
static int32_t pcm_centibels_to_factor(int volume)
{
if (volume == PCM_MUTE_LEVEL)
return 0; /* mute */
/* Centibels -> fixedpoint */
return fp_factor(PCM_FACTOR_UNITY*volume / 10, PCM_FACTOR_BITS);
}
#ifdef AUDIOHW_HAVE_PRESCALER
/* Produce final pcm scale factor */
static void pcm_sync_prescaler(void)
{
int32_t factor_l = fp_mul(prescale_factor, vol_factor_l, PCM_FACTOR_BITS);
int32_t factor_r = fp_mul(prescale_factor, vol_factor_r, PCM_FACTOR_BITS);
pcm_factor_l = PCM_FACTOR_CLIP(factor_l);
pcm_factor_r = PCM_FACTOR_CLIP(factor_r);
}
/* Set the prescaler value for all PCM playback */
void pcm_set_prescaler(int prescale)
{
prescale_factor = pcm_centibels_to_factor(-prescale);
pcm_sync_prescaler();
}
/* Set the per-channel volume cut/gain for all PCM playback */
void pcm_set_master_volume(int vol_l, int vol_r)
{
vol_factor_l = pcm_centibels_to_factor(vol_l);
vol_factor_r = pcm_centibels_to_factor(vol_r);
pcm_sync_prescaler();
}
#else /* ndef AUDIOHW_HAVE_PRESCALER */
/* Set the per-channel volume cut/gain for all PCM playback */
void pcm_set_master_volume(int vol_l, int vol_r)
{
int32_t factor_l = pcm_centibels_to_factor(vol_l);
int32_t factor_r = pcm_centibels_to_factor(vol_r);
pcm_factor_l = PCM_FACTOR_CLIP(factor_l);
pcm_factor_r = PCM_FACTOR_CLIP(factor_r);
}
#endif /* AUDIOHW_HAVE_PRESCALER */