219 lines
6.4 KiB
C++
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
|