summaryrefslogtreecommitdiffstats
path: root/audio_utils/include/audio_utils/Balance.h
blob: da9b836c30534ed5f261ba954f9002d223d97580 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#ifndef ANDROID_AUDIO_UTILS_BALANCE_H
#define ANDROID_AUDIO_UTILS_BALANCE_H

#include <functional>
#include <math.h>       /* expf */
#include <sstream>
#include <system/audio.h>
#include <vector>

namespace android::audio_utils {

class Balance {
public:
   /**
     * \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.
     *
     * 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 frames    number of frames of audio data to convert.
     *
     */
    void process(float *buffer, size_t frames);

    /**
     * \brief Computes the stereo gains for left and right channels.
     *
     * 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.
     *
     * \return string representation of Balance object
     */
    std::string toString() const;

private:

    /**
     * \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.

    // setChannelMask() updates mChannelMask, mChannelCount, and mSides to cache the geometry
    // and then calls setBalance() to update mVolumes.

    audio_channel_mask_t mChannelMask = AUDIO_CHANNEL_INVALID;
    size_t mChannelCount = 0;          // from mChannelMask, 0 means no processing done.

    std::vector<int> mSides;           // per channel, the side (0 = left, 1 = right, 2 = center)
                                       // only used for channel position masks.

    // 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

#endif // !ANDROID_AUDIO_UTILS_BALANCE_H