Skip to content

Commit

Permalink
Add 'matrix4_mb' effect; move some shared code to separate files.
Browse files Browse the repository at this point in the history
The 'matrix4_mb' effect utilizes the same core algorithm as 'matrix4',
but splits the signal into 10 independently steered bands for better
separation of multiple simultaneous direct sources.

The filter bank utilizes doubly complementary 5th-order butterworth
filters implemented as a parallel sum and difference of two all pass
sections. See [1] [2] and [3].

For reduced computational load, the event detection and matrix
coefficient calculations for each band are done at a reduced sample rate
(8x downsampling currently).

This commit also adds 'surround_delay' and 'linear_phase' options.

[1] P. P. Vaidyanathan, Sanjit K. Mitra, and Yrjö Neuvo, "A New Approach
    to the Realization of Low-Sensitivity IIR Digital Filters," IEEE
    Transactions on Acoustics, Speech, and Signal Processing, vol. 34,
    no. 2, pp. 350-361, 1986.

[2] Lajos Gazsi, "Explicit Formulas for Lattice Wave Digital Filters,"
    IEEE Transactions on Circuits and Systems, vol. 32, no. 1,
    pp. 68-88, 1985.

[3] Alexis Favrot and Christof Faller, "Complementary N-Band IIR
    Filterbank Based on 2-Band Complementary Filters," Proc. Intl.
    Works. on Acoust. Echo and Noise Control (IWAENC), 2010.
  • Loading branch information
bmc0 committed Aug 16, 2024
1 parent bc5aed4 commit 93bc075
Show file tree
Hide file tree
Showing 12 changed files with 1,449 additions and 410 deletions.
6 changes: 6 additions & 0 deletions GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ DSP_OBJ := dsp.o \
gain.o \
crossfeed.o \
matrix4.o \
matrix4_mb.o \
matrix4_common.o \
cap5.o \
remix.o \
st2ms.o \
delay.o \
Expand All @@ -28,6 +31,9 @@ LADSPA_DSP_OBJ := ladspa_dsp.o \
gain.o \
crossfeed.o \
matrix4.o \
matrix4_mb.o \
matrix4_common.o \
cap5.o \
remix.o \
st2ms.o \
delay.o \
Expand Down
31 changes: 22 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,25 +203,29 @@ Example:
* `crossfeed f0[k] separation`
Simple crossfeed for headphones. Very similar to Linkwitz/Meier/CMoy/bs2b
crossfeed.
* `matrix4 [[options] surround_level]`
* `matrix4 [options] [surround_level]`
2-to-4 channel (2 front and 2 surround) active matrix upmixer designed for
plain (i.e. unencoded) stereo material. The matrix coefficients and the
main ideas behind the steering behavior come from David Griesinger's
published works on matrix surround.

The intended speaker configuration is fronts at ±30° and surrounds between
±60° and ±120°. The surround speakers must be calibrated correctly in
level and frequency response for best results. No frequency contouring or
delay is done internally, so it is highly recommended to apply `delay` and
`lowpass_1` effects to the surround outputs:
level and frequency response for best results. The surrounds should be
delayed by about 10-25ms (acoustically) relative to the fronts. No
frequency contouring is done internally, so applying low pass and/or
shelving filters to the surround outputs is recommended:

```
matrix4 -6 :2,3 delay 15m lowpass_1 6k :
matrix4 surround_delay=15m -6 :2,3 lowpass_1 10k :
```

The settings shown above (-6dB surround level, 15ms delay, and 6kHz rolloff)
are a good starting point, but may be adjusted to taste. The default
`surround_level` is -6dB.
The settings shown above (-6dB surround level, 15ms delay, and 10kHz
rolloff) are a good starting point, but may be adjusted to taste. The
default `surround_level` is -6dB. Applying the `decorrelate` effect to the
surround outputs can be useful to eliminate coloration caused by comb
filtering (note: adjust `surround_delay` to compensate for the `decorrelate`
effect's group delay).

The front outputs replace the original input channels and the surround
outputs are appended to the end of the channel list.
Expand All @@ -235,7 +239,16 @@ Example:
debugging).
* `signal`
Toggle the effect when `effect.signal()` is called.

* `linear_phase` (`matrix4_mb` only)
Apply an FIR filter to correct the phase distortion caused by the IIR
filter bank. Has no effect with `matrix4`. Requires the `fir` effect.
* `surround_delay=delay[s|m|S]`
Surround output delay. Default is zero.

* `matrix4_mb [options] [surround_level]`
Like the `matrix4` effect, but divides the input into ten individually
steered bands in order to improve separation of concurrent sound sources.
The usage and options are the same as the `matrix4` effect.
* `remix channel_selector|. ...`
Select and mix input channels into output channels. Each channel selector
specifies the input channels to be mixed to produce each output channel.
Expand Down
57 changes: 57 additions & 0 deletions cap5.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#include <complex.h>
#include <math.h>
#include "cap5.h"

void ap1_reset(struct ap1_state *state)
{
state->i0 = 0.0;
state->o0 = 0.0;
}

void ap2_reset(struct ap2_state *state)
{
state->i0 = state->i1 = 0.0;
state->o0 = state->o1 = 0.0;
}

void ap3_reset(struct ap3_state *state)
{
ap2_reset(&state->ap2);
ap1_reset(&state->ap1);
}

void cap5_reset(struct cap5_state *state)
{
ap2_reset(&state->a1);
ap3_reset(&state->a2);
}

/* doubly complementary 5th-order butterworth filters implemented as the
sum (lowpass) and difference (highpass) of two allpass sections */
void cap5_init(struct cap5_state *state, double fs, double fc)
{
const double fc_w = 2.0*fs*tan(M_PI*fc/fs); /* pre-warped corner frequency */
double complex p[3]; /* first two have a complex conjugate (not stored), third is real */

for (int i = 0; i < 3; ++i) {
const double theta = (2.0*(i+1)-1.0)*M_PI/10.0;
p[i] = -sin(theta) + cos(theta)*I; /* normalized pole in s-plane */
p[i] = p[i]*fc_w; /* scale */
p[i] = (2.0*fs + p[i]) / (2.0*fs - p[i]); /* bilinear transform */
//LOG_FMT(LL_VERBOSE, "%s(): fc=%gHz: p[%d] = %f%+fi", __func__, fc, i, creal(p[i]), cimag(p[i]));
}

state->a2.ap2.c0 = -2.0*creal(p[0]);
state->a2.ap2.c1 = creal(p[0])*creal(p[0]) + cimag(p[0])*cimag(p[0]);

state->a1.c0 = -2.0*creal(p[1]);
state->a1.c1 = creal(p[1])*creal(p[1]) + cimag(p[1])*cimag(p[1]);

state->a2.ap1.c0 = -creal(p[2]);

//LOG_FMT(LL_VERBOSE, "%s(): fc=%gHz: a1: c0=%g c1=%g", __func__, fc, state->a1.c0, state->a1.c1);
//LOG_FMT(LL_VERBOSE, "%s(): fc=%gHz: a2.ap2: c0=%g c1=%g", __func__, fc, state->a2.ap2.c0, state->a2.ap2.c1);
//LOG_FMT(LL_VERBOSE, "%s(): fc=%gHz: a2.ap1: c0=%g", __func__, fc, state->a2.ap1.c0);

cap5_reset(state);
}
71 changes: 71 additions & 0 deletions cap5.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#ifndef _CAP5_H
#define _CAP5_H

#include "dsp.h"

struct ap1_state {
sample_t c0;
sample_t i0, o0;
};

struct ap2_state {
sample_t c0, c1;
sample_t i0, i1, o0, o1;
};

struct ap3_state {
struct ap2_state ap2;
struct ap1_state ap1;
};

struct cap5_state {
struct ap2_state a1;
struct ap3_state a2;
};

void ap1_reset(struct ap1_state *);
void ap2_reset(struct ap2_state *);
void ap3_reset(struct ap3_state *);
void cap5_reset(struct cap5_state *);
void cap5_init(struct cap5_state *, double, double);

static __inline__ sample_t ap1_run(struct ap1_state *state, sample_t s)
{
sample_t r = state->c0 * (s - state->o0)
+ state->i0;

state->i0 = s;
state->o0 = r;

return r;
}

static __inline__ sample_t ap2_run(struct ap2_state *state, sample_t s)
{
sample_t r = state->c1 * (s - state->o1)
+ state->c0 * (state->i0 - state->o0)
+ state->i1;

state->i1 = state->i0;
state->i0 = s;

state->o1 = state->o0;
state->o0 = r;

return r;
}

static __inline__ sample_t ap3_run(struct ap3_state *state, sample_t s)
{
return ap1_run(&state->ap1, ap2_run(&state->ap2, s));
}

static __inline__ void cap5_run(struct cap5_state *state, sample_t s, sample_t *lp, sample_t *hp)
{
sample_t a1 = ap2_run(&state->a1, s);
sample_t a2 = ap3_run(&state->a2, s);
*lp = (a1+a2)*0.5;
*hp = (a1-a2)*0.5;
}

#endif
34 changes: 25 additions & 9 deletions dsp.1
Original file line number Diff line number Diff line change
Expand Up @@ -191,41 +191,57 @@ argument is given.
Simple crossfeed for headphones. Very similar to Linkwitz/Meier/CMoy/bs2b
crossfeed.
.TP
\fBmatrix4\fR [[\fIoptions\fR] \fIsurround_level\fR]
\fBmatrix4\fR [\fIoptions\fR] [\fIsurround_level\fR]
2-to-4 channel (2 front and 2 surround) active matrix upmixer designed for
plain (i.e. unencoded) stereo material. The matrix coefficients and the
main ideas behind the steering behavior come from David Griesinger's
published works on matrix surround.

The intended speaker configuration is fronts at ±30° and surrounds between
±60° and ±120°. The surround speakers must be calibrated correctly in
level and frequency response for best results. No frequency contouring or
delay is done internally, so it is highly recommended to apply \fBdelay\fR and
\fBlowpass_1\fR effects to the surround outputs:
level and frequency response for best results. The surrounds should be
delayed by about 10-25ms (acoustically) relative to the fronts. No
frequency contouring is done internally, so applying low pass and/or
shelving filters to the surround outputs is recommended:
.EX
matrix4 -6 :2,3 delay 15m lowpass_1 6k :
matrix4 surround_delay=15m -6 :2,3 lowpass_1 10k :
.EE
The settings shown above (-6dB surround level, 15ms delay, and 6kHz rolloff)
are a good starting point, but may be adjusted to taste. The default
\fIsurround_level\fR is -6dB.
\fIsurround_level\fR is -6dB. Applying the \fBdecorrelate\fR effect to the
surround outputs can be useful to eliminate coloration caused by comb
filtering (note: adjust `surround_delay' to compensate for the \fBdecorrelate\fR
effect's group delay).

The front outputs replace the original input channels and the surround
outputs are appended to the end of the channel list.

Options are given as a comma-separated list. Recognized options are:
.RS
.TP
\fIno_dir_boost\fR
no_dir_boost
Disable directional boost of front channels.
.TP
\fIshow_status\fR
show_status
Show a status line (slightly broken currently, but still useful for
debugging).
.TP
\fIsignal\fR
signal
Toggle the effect when `effect.signal()' is called.
.TP
linear_phase (\fBmatrix4_mb\fR only)
Apply an FIR filter to correct the phase distortion caused by the IIR
filter bank. Has no effect with \fBmatrix4\fR. Requires the \fBfir\fR effect.
.TP
surround_delay=\fIdelay\fR[\fBs\fR|\fBm\fR|\fBS\fR]
Surround output delay. Default is zero.
.RE
.TP
\fBmatrix4_mb\fR [\fIoptions\fR] [\fIsurround_level\fR]
Similar to the \fBmatrix4\fR effect, but divides the input into ten
individually steered bands in order to improve separation of concurrent
sound sources. See the \fBmatrix4\fR effect description for more information.
.TP
\fBremix\fR \fIchannel_selector\fR|. ...
Select and mix input channels into output channels. Each channel selector
specifies the input channels to be mixed to produce each output channel.
Expand Down
4 changes: 3 additions & 1 deletion effect.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "gain.h"
#include "crossfeed.h"
#include "matrix4.h"
#include "matrix4_mb.h"
#include "remix.h"
#include "st2ms.h"
#include "delay.h"
Expand Down Expand Up @@ -45,7 +46,8 @@ static const struct effect_info effects[] = {
{ "mult", "mult [channel] multiplier", gain_effect_init, GAIN_EFFECT_NUMBER_MULT },
{ "add", "add [channel] value", gain_effect_init, GAIN_EFFECT_NUMBER_ADD },
{ "crossfeed", "crossfeed f0[k] separation", crossfeed_effect_init, 0 },
{ "matrix4", "matrix4 [[options] surround_level]", matrix4_effect_init, 0 },
{ "matrix4", "matrix4 [options] [surround_level]", matrix4_effect_init, 0 },
{ "matrix4_mb", "matrix4_mb [options] [surround_level]", matrix4_mb_effect_init, 0 },
{ "remix", "remix channel_selector|. ...", remix_effect_init, 0 },
{ "st2ms", "st2ms", st2ms_effect_init, ST2MS_EFFECT_NUMBER_ST2MS },
{ "ms2st", "ms2st", st2ms_effect_init, ST2MS_EFFECT_NUMBER_MS2ST },
Expand Down
55 changes: 55 additions & 0 deletions ewma.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#ifndef _EWMA_H
#define _EWMA_H

#include <math.h>

struct ewma_state {
double c0, c1, m0;
};

#define EWMA_RISE_TIME(x) ((x)/1000.0/2.1972) /* 10%-90% rise time in ms */

/* note: tc is the time constant in seconds */
static __inline__ void ewma_init(struct ewma_state *state, double fs, double tc)
{
const double a = 1.0-exp(-1.0/(fs*tc));
state->c0 = a;
state->c1 = 1.0-a;
state->m0 = 0.0;
}

static __inline__ double ewma_run(struct ewma_state *state, double s)
{
const double r = state->c0*s + state->c1*state->m0;
state->m0 = r;
return r;
}

/* note: sf > 1.0 means a faster rise time */
static __inline__ double ewma_run_scale(struct ewma_state *state, double s, double sf)
{
const double c = (state->c0*sf > 0.39) ? 0.39 : state->c0*sf;
const double r = c*s + (1.0-c)*state->m0;
state->m0 = r;
return r;
}

static __inline__ double ewma_run_set_max(struct ewma_state *state, double s)
{
if (s >= state->m0) s = ewma_run(state, s);
else state->m0 = s;
return s;
}

static __inline__ double ewma_set(struct ewma_state *state, double s)
{
state->m0 = s;
return s;
}

static __inline__ double ewma_get_last(struct ewma_state *state)
{
return state->m0;
}

#endif
Loading

0 comments on commit 93bc075

Please sign in to comment.