namespace android::audio_utils {
-// Implementation detail: parametric volume curve transfer function
-
-constexpr float CURVE_PARAMETER = 2.f;
-
-static inline float curve(float parameter, float inVolume) {
- return exp(parameter * inVolume) - 1.f;
-}
-
-Balance::Balance()
- : mCurveNorm(1.f / curve(CURVE_PARAMETER, 1.f /* inVolume */))
-{
- setChannelMask(AUDIO_CHANNEL_OUT_STEREO);
-}
-
void Balance::setChannelMask(audio_channel_mask_t channelMask)
{
channelMask &= ~ AUDIO_CHANNEL_HAPTIC_ALL;
mChannelMask = channelMask;
mChannelCount = audio_channel_count_from_out_mask(channelMask);
- // reset mVolumes (the next process() will recalculate if needed).
+ // save mBalance into balance for later restoring, then reset
+ const float balance = mBalance;
+ mBalance = 0.f;
+
+ // reset mVolumes
mVolumes.resize(mChannelCount);
std::fill(mVolumes.begin(), mVolumes.end(), 1.f);
- mBalance = 0.f;
+
+ // reset ramping variables
+ mRampBalance = 0.f;
+ mRampVolumes.clear();
+
+ if (audio_channel_mask_get_representation(mChannelMask)
+ == AUDIO_CHANNEL_REPRESENTATION_INDEX) {
+ mSides.clear(); // mSides unused for channel index masks.
+ setBalance(balance); // recompute balance
+ return;
+ }
// Implementation detail (may change):
// For implementation speed, we precompute the side (left, right, center),
mSides.resize(mChannelCount);
for (unsigned i = 0, channel = channelMask; channel != 0; ++i) {
const int index = __builtin_ctz(channel);
- if (index < sizeof(sideFromChannel) / sizeof(sideFromChannel[0])) {
- mSides[i] = 2; // consider center
- } else {
+ if (index < std::size(sideFromChannel)) {
mSides[i] = sideFromChannel[index];
+ } else {
+ mSides[i] = 2; // consider center
}
channel &= ~(1 << index);
}
+ setBalance(balance); // recompute balance
}
-void Balance::process(float *buffer, float balance, size_t frames)
+void Balance::process(float *buffer, size_t frames)
{
- setBalance(balance);
+ if (mBalance == 0.f || mChannelCount < 2) {
+ return;
+ }
+
+ if (mRamp) {
+ if (mRampVolumes.size() != mVolumes.size()) {
+ // If mRampVolumes is empty, we do not ramp in this process() but directly
+ // apply the existing mVolumes. We save the balance and volume state here
+ // and fall through to non-ramping code below. The next process() will ramp if needed.
+ mRampBalance = mBalance;
+ mRampVolumes = mVolumes;
+ } else if (mRampBalance != mBalance) {
+ if (frames > 0) {
+ std::vector<float> mDeltas(mVolumes.size());
+ const float r = 1.f / frames;
+ for (size_t j = 0; j < mChannelCount; ++j) {
+ mDeltas[j] = (mVolumes[j] - mRampVolumes[j]) * r;
+ }
+
+ // ramped balance
+ for (size_t i = 0; i < frames; ++i) {
+ const float findex = i;
+ for (size_t j = 0; j < mChannelCount; ++j) { // better precision: delta * i
+ *buffer++ *= mRampVolumes[j] + mDeltas[j] * findex;
+ }
+ }
+ }
+ mRampBalance = mBalance;
+ mRampVolumes = mVolumes;
+ return;
+ }
+ // fall through
+ }
+
+ // non-ramped balance
for (size_t i = 0; i < frames; ++i) {
for (size_t j = 0; j < mChannelCount; ++j) {
*buffer++ *= mVolumes[j];
}
}
-// Implementation detail (may change):
-// This is not an energy preserving balance (e.g. using sin/cos cross fade or some such).
-// Rather it preserves full gain on left and right when balance is 0.f,
-// and decreases the right or left as one changes the balance.
void Balance::computeStereoBalance(float balance, float *left, float *right) const
{
if (balance > 0.f) {
- *left = curve(CURVE_PARAMETER, 1.f - balance) * mCurveNorm;
+ *left = mCurve(1.f - balance);
*right = 1.f;
} else if (balance < 0.f) {
*left = 1.f;
- *right = curve(CURVE_PARAMETER, 1.f + balance) * mCurveNorm;
+ *right = mCurve(1.f + balance);
} else {
*left = 1.f;
*right = 1.f;
}
// Functionally:
- // *left = balance > 0.f ? curve(CURVE_PARAMETER, 1.f - balance) * mCurveNorm : 1.f;
- // *right = balance < 0.f ? curve(CURVE_PARAMETER, 1.f + balance) * mCurveNorm : 1.f;
+ // *left = balance > 0.f ? mCurve(1.f - balance) : 1.f;
+ // *right = balance < 0.f ? mCurve(1.f + balance) : 1.f;
}
std::string Balance::toString() const
for (float volume : mVolumes) {
ss << " " << volume;
}
+ // we do not show mSides, which is only valid for channel position masks.
return ss.str();
}
void Balance::setBalance(float balance)
{
- if (isnan(balance) || fabs(balance) > 1.f // balance out of range
- || mBalance == balance || mChannelCount < 2) { // change not applicable
+ if (mBalance == balance // no change
+ || isnan(balance) || fabs(balance) > 1.f) { // balance out of range
return;
}
mBalance = balance;
- // handle the common cases
+ if (mChannelCount < 2) { // if channel count is 1, mVolumes[0] is already set to 1.f
+ return; // and if channel count < 2, we don't do anything in process().
+ }
+
+ // Handle the common cases:
+ // stereo and channel index masks only affect the first two channels as left and right.
if (mChannelMask == AUDIO_CHANNEL_OUT_STEREO
|| audio_channel_mask_get_representation(mChannelMask)
== AUDIO_CHANNEL_REPRESENTATION_INDEX) {
return;
}
+ // For position masks with more than 2 channels, we consider which side the
+ // speaker position is on to figure the volume used.
float balanceVolumes[3]; // left, right, center
computeStereoBalance(balance, &balanceVolumes[0], &balanceVolumes[1]);
balanceVolumes[2] = 1.f; // center TODO: consider center scaling.
}
} // namespace android::audio_utils
-
#ifndef ANDROID_AUDIO_UTILS_BALANCE_H
#define ANDROID_AUDIO_UTILS_BALANCE_H
-#include <math.h> /* exp */
+#include <math.h> /* expf */
#include <sstream>
#include <system/audio.h>
#include <vector>
class Balance {
public:
- Balance();
+ /**
+ * \brief Balance processing of left-right volume on audio data.
+ *
+ * Allows processing of audio data with a single balance parameter from [-1, 1].
+ * For efficiency, the class caches balance and channel mask data between calls;
+ * hence, use by multiple threads will require caller locking.
+ *
+ * \param ramp whether to ramp volume or not.
+ * \param curve a monotonic increasing function f: [0, 1] -> [a, b]
+ * which represents the volume steps from an input domain of [0, 1] to
+ * an output range [a, b] (ostensibly also from 0 to 1).
+ * If [a, b] is not [0, 1], it is normalized to [0, 1].
+ * Curve is typically a convex function, some possible examples:
+ * [](float x) { return expf(2.f * x); }
+ * or
+ * [](float x) { return x * (x + 0.2f); }
+ */
+ explicit Balance(
+ bool ramp = true,
+ std::function<float(float)> curve = [](float x) { return x * (x + 0.2f); })
+ : mRamp(ramp)
+ , mCurve(normalize(std::move(curve))) { }
+
+ /**
+ * \brief Sets whether the process ramps left-right volume changes.
+ *
+ * The default value is true.
+ * The ramp will take place, if needed, on the following process()
+ * using the current balance and volume as the starting point.
+ *
+ * Toggling ramp off and then back on will reset the ramp starting point.
+ *
+ * \param ramp whether ramping is used to smooth volume changes.
+ */
+ void setRamp(bool ramp) {
+ if (ramp == mRamp) return; // no change
+ mRamp = ramp;
+ if (mRamp) { // use current volume and balance as starting point.
+ mRampVolumes = mVolumes;
+ mRampBalance = mBalance;
+ }
+ }
/**
* \brief Sets the channel mask for data passed in.
*
- * \param channelMask The audio output channel mask to use.
+ * setChannelMask() must called before process() to set
+ * a valid output audio channel mask.
+ *
+ * \param channelMask the audio output channel mask to use.
* Invalid channel masks are ignored.
*
*/
void setChannelMask(audio_channel_mask_t channelMask);
+
+ /**
+ * \brief Sets the left-right balance parameter.
+ *
+ * setBalance() should be called before process() to set
+ * the balance. The initial value is 0.f (no action).
+ *
+ * \param balance from -1.f (left) to 0.f (center) to 1.f (right).
+ *
+ */
+ void setBalance(float balance);
+
/**
* \brief Processes balance for audio data.
*
+ * setChannelMask() should be called at least once before calling process()
+ * to set the channel mask. A balance of 0.f or a channel mask of
+ * less than 2 channels will return with the buffer untouched.
+ *
* \param buffer pointer to the audio data to be modified in-place.
- * \param balance from -1.f (left) to 0.f (center) to 1.f (right)
- * \param frames numer of frames of audio data to convert.
+ * \param frames number of frames of audio data to convert.
*
*/
- void process(float *buffer, float balance, size_t frames);
+ void process(float *buffer, size_t frames);
/**
* \brief Computes the stereo gains for left and right channels.
*
- * \param balance from -1.f (left) to 0.f (center) to 1.f (right)
+ * Implementation detail (may change):
+ * This is not an energy preserving balance (e.g. using sin/cos cross fade or some such).
+ * Rather balance preserves full gain on left and right when balance is 0.f,
+ * and decreases the right or left as one changes the balance parameter.
+ *
+ * \param balance from -1.f (left) to 0.f (center) to 1.f (right).
* \param left pointer to the float where the left gain will be stored.
* \param right pointer to the float where the right gain will be stored.
*/
void computeStereoBalance(float balance, float *left, float *right) const;
/**
- * \brief Creates a std::string representation of Balance object for logging,
+ * \brief Creates a std::string representation of Balance object for logging.
+ *
* \return string representation of Balance object
*/
std::string toString() const;
private:
- // Called by process() to recompute mVolumes as needed.
- void setBalance(float balance);
+ /**
+ * \brief Normalizes f: [0, 1] -> [a, b] to g: [0, 1] -> [0, 1].
+ *
+ * A helper function to normalize a float volume function.
+ * g(0) is exactly zero, but g(1) may not necessarily be 1 since we
+ * use reciprocal multiplication instead of division to scale.
+ *
+ * \param f a function from [0, 1] -> [a, b]
+ * \return g a function from [0, 1] -> [0, 1] as a linear function of f.
+ */
+ template<typename T>
+ static std::function<T(T)> normalize(std::function<T(T)> f) {
+ const T f0 = f(0);
+ const T r = T(1) / (f(1) - f0); // reciprocal multiplication
+
+ if (f0 != T(0) || // must be exactly 0 at 0, since we promise g(0) == 0
+ fabs(r - T(1)) > std::numeric_limits<T>::epsilon() * 3) { // some fudge allowed on r.
+ return [f, f0, r](T x) { return r * (f(x) - f0); };
+ }
+ // no translation required.
+ return f;
+ }
+
+ // setBalance() changes mBalance and mVolumes based on the channel geometry information.
+ float mBalance = 0.f; // balance: -1.f (left), 0.f (center), 1.f (right)
+ std::vector<float> mVolumes; // per channel, the volume adjustment due to balance.
- const float mCurveNorm; // curve normalization, fixed constant.
+ // setChannelMask() updates mChannelMask, mChannelCount, and mSides to cache the geometry
+ // and then calls setBalance() to update mVolumes.
- // process() sets cached mBalance through setBalance().
- float mBalance = 0.f; // balance: -1.f (left), 0.f (center), 1.f (right)
+ audio_channel_mask_t mChannelMask = AUDIO_CHANNEL_INVALID;
+ size_t mChannelCount = 0; // from mChannelMask, 0 means no processing done.
- audio_channel_mask_t mChannelMask; // setChannelMask() sets the current channel mask
- size_t mChannelCount; // derived from mChannelMask
std::vector<int> mSides; // per channel, the side (0 = left, 1 = right, 2 = center)
+ // only used for channel position masks.
- // process() sets the cached mVolumes
- std::vector<float> mVolumes; // per channel, the volume adjustment due to balance.
+ // Ramping variables
+ bool mRamp; // whether ramp is enabled.
+ float mRampBalance = 0.f; // last (starting) balance to begin ramp.
+ std::vector<float> mRampVolumes; // last (starting) volumes to begin ramp, clear for no ramp.
+
+ const std::function<float(float)> mCurve; // monotone volume transfer func [0, 1] -> [0, 1]
};
} // namespace android::audio_utils