2023-12-01 15:52:37 +00:00

219 lines
6.4 KiB
C++

#include "./common.h"
#ifndef SIGNALSMITH_DSP_MULTI_CHANNEL_H
#define SIGNALSMITH_DSP_MULTI_CHANNEL_H
#include <array>
namespace signalsmith {
namespace mix {
/** @defgroup Mix Multichannel mixing
@brief Utilities for stereo/multichannel mixing operations
@{
@file
*/
/** @defgroup Matrices Orthogonal matrices
@brief Some common matrices used for audio
@ingroup Mix
@{ */
/// @brief Hadamard: high mixing levels, N log(N) operations
template<typename Sample, int size=-1>
class Hadamard {
public:
static_assert(size >= 0, "Size must be positive (or -1 for dynamic)");
/// Applies the matrix, scaled so it's orthogonal
template<class Data>
static void inPlace(Data &&data) {
unscaledInPlace(data);
Sample factor = scalingFactor();
for (int c = 0; c < size; ++c) {
data[c] *= factor;
}
}
/// Scaling factor applied to make it orthogonal
static Sample scalingFactor() {
/// TODO: test for C++20, or whatever makes this constexpr. Maybe a `#define` in `common.h`?
return std::sqrt(Sample(1)/(size ? size : 1));
}
/// Skips the scaling, so it's a matrix full of `1`s
template<class Data, int startIndex=0>
static void unscaledInPlace(Data &&data) {
if (size <= 1) return;
constexpr int hSize = size/2;
Hadamard<Sample, hSize>::template unscaledInPlace<Data, startIndex>(data);
Hadamard<Sample, hSize>::template unscaledInPlace<Data, startIndex + hSize>(data);
for (int i = 0; i < hSize; ++i) {
Sample a = data[i + startIndex], b = data[i + startIndex + hSize];
data[i + startIndex] = (a + b);
data[i + startIndex + hSize] = (a - b);
}
}
};
/// @brief Hadamard with dynamic size
template<typename Sample>
class Hadamard<Sample, -1> {
int size;
public:
Hadamard(int size) : size(size) {}
/// Applies the matrix, scaled so it's orthogonal
template<class Data>
void inPlace(Data &&data) const {
unscaledInPlace(data);
Sample factor = scalingFactor();
for (int c = 0; c < size; ++c) {
data[c] *= factor;
}
}
/// Scaling factor applied to make it orthogonal
Sample scalingFactor() const {
return std::sqrt(Sample(1)/(size ? size : 1));
}
/// Skips the scaling, so it's a matrix full of `1`s
template<class Data>
void unscaledInPlace(Data &&data) const {
int hSize = size/2;
while (hSize > 0) {
for (int startIndex = 0; startIndex < size; startIndex += hSize*2) {
for (int i = startIndex; i < startIndex + hSize; ++i) {
Sample a = data[i], b = data[i + hSize];
data[i] = (a + b);
data[i + hSize] = (a - b);
}
}
hSize /= 2;
}
}
};
/// @brief Householder: moderate mixing, 2N operations
template<typename Sample, int size=-1>
class Householder {
public:
static_assert(size >= 0, "Size must be positive (or -1 for dynamic)");
template<class Data>
static void inPlace(Data &&data) {
if (size < 1) return;
/// TODO: test for C++20, which makes `std::complex::operator/` constexpr
const Sample factor = Sample(-2)/Sample(size ? size : 1);
Sample sum = data[0];
for (int i = 1; i < size; ++i) {
sum += data[i];
}
sum *= factor;
for (int i = 0; i < size; ++i) {
data[i] += sum;
}
}
/// @deprecated The matrix is already orthogonal, but this is here for compatibility with Hadamard
constexpr static Sample scalingFactor() {
return 1;
}
};
/// @brief Householder with dynamic size
template<typename Sample>
class Householder<Sample, -1> {
int size;
public:
Householder(int size) : size(size) {}
template<class Data>
void inPlace(Data &&data) const {
if (size < 1) return;
const Sample factor = Sample(-2)/Sample(size ? size : 1);
Sample sum = data[0];
for (int i = 1; i < size; ++i) {
sum += data[i];
}
sum *= factor;
for (int i = 0; i < size; ++i) {
data[i] += sum;
}
}
/// @deprecated The matrix is already orthogonal, but this is here for compatibility with Hadamard
constexpr static Sample scalingFactor() {
return 1;
}
};
/// @}
/** @brief Upmix/downmix a stereo signal to an (even) multi-channel signal
When spreading out, it rotates the input by various amounts (e.g. a four-channel signal would produce `(left, right, mid side)`), such that energy is preserved for each pair.
When mixing together, it uses the opposite rotations, such that upmix → downmix produces the same stereo signal (when scaled by `.scalingFactor1()`.
*/
template<typename Sample, int channels>
class StereoMultiMixer {
static_assert((channels/2)*2 == channels, "StereoMultiMixer must have an even number of channels");
static_assert(channels > 0, "StereoMultiMixer must have a positive number of channels");
static constexpr int hChannels = channels/2;
std::array<Sample, channels> coeffs;
public:
StereoMultiMixer() {
coeffs[0] = 1;
coeffs[1] = 0;
for (int i = 1; i < hChannels; ++i) {
double phase = M_PI*i/channels;
coeffs[2*i] = std::cos(phase);
coeffs[2*i + 1] = std::sin(phase);
}
}
template<class In, class Out>
void stereoToMulti(In &input, Out &output) const {
output[0] = input[0];
output[1] = input[1];
for (int i = 2; i < channels; i += 2) {
output[i] = input[0]*coeffs[i] + input[1]*coeffs[i + 1];
output[i + 1] = input[1]*coeffs[i] - input[0]*coeffs[i + 1];
}
}
template<class In, class Out>
void multiToStereo(In &input, Out &output) const {
output[0] = input[0];
output[1] = input[1];
for (int i = 2; i < channels; i += 2) {
output[0] += input[i]*coeffs[i] - input[i + 1]*coeffs[i + 1];
output[1] += input[i + 1]*coeffs[i] + input[i]*coeffs[i + 1];
}
}
/// Scaling factor for the downmix, if channels are phase-aligned
static constexpr Sample scalingFactor1() {
return 2/Sample(channels);
}
/// Scaling factor for the downmix, if channels are independent
static Sample scalingFactor2() {
return std::sqrt(scalingFactor1());
}
};
/// A cheap (polynomial) almost-energy-preserving crossfade
/// Maximum energy error: 1.06%, average 0.64%, curves overshoot by 0.3%
/// See: http://signalsmith-audio.co.uk/writing/2021/cheap-energy-crossfade/
template<typename Sample, typename Result>
void cheapEnergyCrossfade(Sample x, Result &toCoeff, Result &fromCoeff) {
Sample x2 = 1 - x;
// Other powers p can be approximated by: k = -6.0026608 + p*(6.8773512 - 1.5838104*p)
Sample A = x*x2, B = A*(1 + (Sample)1.4186*A);
Sample C = (B + x), D = (B + x2);
toCoeff = C*C;
fromCoeff = D*D;
}
/** @} */
}} // signalsmith::delay::
#endif // include guard