Virtual Bass Array

The double bass array is a recent hot topic in German DIY group. It is not until Uli mentioned this in Acourate forum do I know something about it. A quick search in the internet gives you a few links:

Uli further mentioned his way for dealing with it by using Acourate to create a virtual double bass array.

You may know already about the principles of a double bass array with speakers in the front as usual plus some speakers at the rear wall to compensate the longitudinal room modes.The idea has been to discuss a solution without such rear speakers and to compensate the echoes along the longitudinal axis by the main speaker itself.So I also like to report here about the virtual bass array.The result and the way to get it should be interesting for all Acourate users. BTW it also shows that you can do much more with Acourate than just follow the given macros.

I will show the procedure step by step. You should be able to do the same by yourself.

First some theory about a system with assumed ideal behaviour:
A speaker is positioned at the frontwall with zero distance. It sends a perfect Dirac signal. The soundwave moves to the back wall (behind a listener, the listener is not discussed here) and is reflected there. Some energy is lost, thus the reflection level is reduced by a factor x. Now the reflectied soundwave moves to the front wall and is also reflected there with some enery loss by a factor x.

So we get an echo after a time t = 2 * room length(speed of sound. The echo can be considered as a new played sound by the speaker. The next echo cycle starts.

As a result we will get a sequence built of the original Dirac pulse and the following echoes (with descending amplitudes).

The picture shows the resulting pulse response in the time domain chart and the according frequency respnse in the magnitude plot.

You can design such a pulse response by Generate – Testsignal – Length 65536, Set to one @ sample 0, length 1. Copy the pulse with Ctrl-C to the other curves 2-6. Lower the amplitude of the pulses accordingly with TD-Functions – Gain (here 0.7 with power of 1, 2, 3 …), rotate the pulses to right direction by TD-Functions – Rotate with a number of samples according to the echo time. The sum of the single pulses taken by TD-Functions – Add will lead to the shown result.

Of course we are not happy with the frequency response. We like to have a horizontal flat and smooth frequency response. So we need a filter with an inverse response. In this case the responses of the echo signal and the filter will add to zero (filtering in time domain = convolution, filtering in frequency domain = multiplication, filtering in logarithmic frequency doamin = addition).

The echo signal is a minimum phase signal. The inverse of a minphase signl is also minphase. So we can simply apply the Acourate function FD-Functions – Amplitude Inversion minphase and we will get the next result:

The inverse frequency response is as expected. But more interesting is now the time domain behaviour of the filter. It also consists of the first Dirac pulse followed by another negative Dirac pulse with amplitude 0.7 ! The distance is exactly the given echo time. What does this mean?

It means that we compensate all the echoes by playing the music first in its original format (see first Dirac pulse part) and by playing (exactly at the time of the first echo reflected from the front wall) the music signal with reduced gain and inverted polarity. In ideal case the compensation signal will cancel the echo perfectly and there will be no further echo anymore.

Our front speakers play the role of a virtual bass array. We do not need a second system (with a DBA system we have another speaker at the rear wall. It will compensate the echo of the rear wall, thus the soundwave will travel only one time through the room).

Further thoughts:

The inverted frequency response of the filter exceeds 0 dB. We have to consider that the compensation signal plus the original signal will exceed 0 dBFS (full scale). To avoid clipping the filter gain has to be lowered accordingly. This is the price to be paid. Of course the following amplifier may correct it.

In our example we have designed a FIR filter. A very simple one. It is also possible to send the music signal through a delay, to invert it and to add it to the original music signal. So no FIR filter is required, a stupid DSP may do it. The disdvantages of such a solution will be discussed later.

We have taken an ideal example. Beside the gain reduction in reality the echoes will have their own frequency response, the room will act as a filter to the echoes (think about absorption and diffusion).
In fact we complain about the usual boost of the lower frequencies. The task is to get rid of the room modes in the lower frequency range. So we must do a bit more and I will write about in a next message.

It is also interesting that the echo cancellation will also work in case of a distance between front speaker and front wall. You may prove by yourself, that the echo path is always the double room length.
As the echo path is constant we can apply the echo cancellation. It is not possible e.g. for sidewall reflections.

What we do not really know is how the echo looks like in reality. We do not know the damping factor and especially we do not know the pulse response of the echo/echoes and the frequency response/s. It will be very difficult to predict the shape of the echo signal after several reflections. And we must be aware that it is difficult too to measure the echos separately. In our usual rooms the echo is already there before a first cycle of a low frequency has taken place. With a frequency of 20 Hz the echo signal has crossed the room several times with one cycle time of 50 ms.

Ok, but we can still proceed and shoot a bit in the dark.In a first test a measurement of an uncorrected system has been taken. The length of the room is 4.74 m, at least one know data.
So we like to design an echo filter. The start of the filter is clear, it is a Dirac pulse. It takes cre for a 1:1 playback of the music.

The pulse response starts at sample 0 with amplitude 1, all other values are 0.
In a first approach we are not interested in frequencies above 150 Hz. This selection is arbitrary, you may play with other values.
So we create a lowpass filter by a rough assuption that the echo signal behaves like such a lowpass. With Acourate Generate – Crossover – Butterworth – 150 Hz and 4th order we can quickly create it. The we compute the minimum phase by TD-Functions – Phase Extraction – minphase because we expect the echo to react causal. Thus there we do not expect any pre-ringing.

Thus we have got a pulse response. Please note the amplitude scale left of the plot. Yep, the amplitude it really very small compared to the Dirac pulse with amplitude 1.

Now the lowpass filter has to be delayed. For small delays we can simply use the rotate function in Acourate. Vut how many samples? So we calculate:

4.74 m * 2 = 9.48 m double room length divided by 340 m/s speed of sound = 27,88 ms = 1338 samples (@ 48 kHz samplerate). We have to subtract the peak position of the lowpass pulse, in this case 167 samples. So we rotate the pulse by TD-Functions – Rotate with 1171 samples to bring the pulse peak to position 1338. Furthermore we apply TD-Functions – Gain – factor 0.7 to simulate a damped echo (I’ll later explain this factor). The operations result in:

As we need a pulse with opposite sign (see part 1) we apply TD-Function – Change Polarity.

Finally we can ad the Dirac pulse and the prepared lowpass pulse by TD-Function – Add and get

The resulting filter has the desired pulse response and it shows a frequency response as shown in the next picture

We are ready with a first echo compensation filter, the design is quite simple.

So here is a measurement taken in the example room of 4.74 m length:

The picture shows the already known frequenc response of the echo compensation pulse together with the frequency response of the room (up to 300 Hz). Especially the resonance peak at about 36 Hz can be immediately seen, the compensation has a corresponding dip. Great, it fits together.
For higher harmonics there are deviations. With a high probability it is possible, that our compensation pulse is not optimal. We do not know the real behaviour of the echo signal. But also we have to keep in mind that the measurement includes influences of other room modes and reflections.We can simulate the compensation by convolving the measurement and the compensation pulse, TD-Functions – ConvolutionAt 36 Hz the resulting amplitude is lowered by about 10 dB, this is pretty much. BTW, the previously described damping factor 0.7 has been found by the simulation. A greater factor results in a dip in the frequency response. A smaller factor causes a remaining peak. Now the curve is quite smooth (still a big bass boost due to another reason). In the 50 to 60 Hz range a dip is boosted y the compensation.How does it sound? A music file has been prep’d with the compensation pulse. There is a bass drum that shows a big bass boost in uncorrected playback. I’m sure you cn imagine. Now just with the simple echo cancellation the bass drum sounds dry and precise. So the virtual bass array works.Of course we must expect playback colorations (btw we must also expect this by a DBA approach). And of course I recommend an overall room correction on top of the virtual bass array. This means to use the compensation pulse as a prefilter and to convolve it with a room correction filter to get a final correction. The prefilter is quite stable and it may assist in getting a more stable correction.Here is the excerpt from another veteren Accurate user and has successfully implemented Virtual bass array. Here is an excerpt of what he did:a) run Acouratesamplerate 480001: Acourate V1.8.1: Start 24.09.2012 08:39:01   Workspace=C:\AcourateProjects\LarsBoman
2: **********************
3: Testsignal=1 type=SetAtPos length=65536 atsample=0 samplelength=1
4: XO f1=100 f2= f3= f4= typ=butterworth order=4  phase=linear taps=65536 format=double samplerate=48000
5: Load=2 by=C:\AcourateProjects\LarsBoman\XO1L48.dbl samplerate=48000
6: PhaseExtract=2 from=2 type=minphase below=0 above=0
7: Gain=2 factor=0.700 dB=-3.098
8: Rotate=2 by=1125
9: SwitchPolarity=2
10: Add=3 sum=12
11: Load=4 by=C:\AcourateProjects\LarsBoman\Pulse48Lstart.dbl samplerate=48000
12: Convolve=5 by=3 with=4
13: CutNWindow=5 from=5 length=65536 poscount=0 type=Startpos markmin=0 markmax= 0 opt1=false opt2=false opt3=false
14: **********************
15: Acourate: Stop 24.09.2012 08:40:49Rotation 1125 is calculated by 9.75 m : 340 m/s * 48000 = 1376 samples minus 251 samples peakpos of XO1You can play with gain 0.7 and with rotation 1125 samples and off course also with corner frequency of lowpass (here 100 Hz)You can use the generated result as a prefilter. E.g. use the result as a filter in AcourateNAS (you may need to generate the filter with 44100 Hz samplerate before).So I follow the steps and tried on my setup.I am using 96000Hz sampling rate so my setup is slightly different, but the concept is the same. I have listed here the screen shots for easy referenceThis is minimal phase extractionUntitled0Over here, you can see the low pass filter that is created for the virtual bass array.Untitled1Over here, the blue line represents the room measurement. The green is the convoluted response to “see” what happened after the virtual line array is in placed. You can see that the trough of the filter fits the peak of the room response perfectly!Untitled3The brown lines is the filter created with this virtual line array.  Untitled4

Reading the Accurate forum in German sites  give more detailed information. In one of the replies, Uli indicated a technique to identify the frequency by checking the ringing on the step response.

If you go back to my original PulseL96, there is resonance at 34.69Hz.


If you look at the step response, you can also see the ringing pattern. Now, if we calculate the frequency by checking the step response, there are 13640 samples and 5 cycles or 2728 samples/cycles. At a sampling rate of 96K, that’s 96000/2728 = 35.19Hz which matches the resonance you see on the frequency response very well.


Now, with the pre-filter that we just created and this time check the step response.



Most of the ringing is gone. However there remains a ringing of resonance peak at 18.74Hz.



It is then incorporated into BruteFIR as prefilter. Here is the script for you to follow:


float_bits: 64;             # internal floating point precision
sampling_rate: 96000;       # sampling rate in Hz of audio interfaces
filter_length: 4096,16;      # length of filters
# config_file: "/home/audiovero/.brutefir_config"; # standard location of main config file
overflow_warnings: true;    # echo warnings to stderr if overflow occurs
show_progress: false;        # echo filtering progress to stderr
max_dither_table_size: 0;   # maximum size in bytes of precalculated dither
allow_poll_mode: false;     # allow use of input poll mode
modules_path: "/usr/lib/brutefir";   # extra path where to find BruteFIR modules
monitor_rate: true;        # monitor sample rate
powersave: true;           # pause filtering when input is zero
lock_memory: true;          # try to lock memory if realtime prio is set
convolver_config: "/home/audiovero/.brutefir_convolver"; # location of convolver config file

## LOGIC ##

logic: "cli" { port: 3000; };

## COEFFS ##

### Virtual Bass Array coeff ###

coeff "lVBA" {
      filename: "/audiovero/brutefir/filter/96a/nocorrection/lVBAfile.dbl";
      shared_mem: false;
      format: "FLOAT64_LE";

coeff "rVBA" {
      filename: "/audiovero/brutefir/filter/96a/nocorrection/rVBAfile.dbl";
      shared_mem: false;
      format: "FLOAT64_LE";

### XO coeff ###

coeff "leftsub" {
       filename: "/audiovero/brutefir/filter/96a/nocorrection/COR1L96.dbl";
       format: "FLOAT64_LE";

coeff "rightsub" {
       filename: "/audiovero/brutefir/filter/96a/nocorrection/COR1R96.dbl";
       format: "FLOAT64_LE";

coeff "leftwof" {
       filename: "/audiovero/brutefir/filter/96a/nocorrection/COR2L96.dbl";
       format: "FLOAT64_LE";

coeff "rightwof" {
       filename: "/audiovero/brutefir/filter/96a/nocorrection/COR2R96.dbl";
       format: "FLOAT64_LE";

coeff "leftmid" {
       filename: "/audiovero/brutefir/filter/96a/nocorrection/COR3L96.dbl";
       format: "FLOAT64_LE";

coeff "rightmid" {
       filename: "/audiovero/brutefir/filter/96a/nocorrection/COR3R96.dbl";
       format: "FLOAT64_LE";

coeff "lefttwet" {
       filename: "/audiovero/brutefir/filter/96a/nocorrection/COR4L96.dbl";
       format: "FLOAT64_LE";

coeff "righttwet" {
       filename: "/audiovero/brutefir/filter/96a/nocorrection/COR4R96.dbl";
       format: "FLOAT64_LE";


#input "left", "right", "ltsurr", "rtsurr", "cent" {
input "left", "right" {
        device: "alsa" { device: "hw:0";}; # ignore_xrun: true; };
        sample: "S24_4LE";
#	channels: 26/0,1,2,3,4;
	channels: 26/0,1;

output "rsub",  "rlow", "rmid", "rhigh", "lhigh", "lmid", "llow", "lsub"{
       device: "alsa" { device: "hw:0";}; # ignore_xrun: true; };
       sample: "S24_4LE";
       channels: 26/0,1,2,3,4,5,6,7;
       delay: 0,65,71,75,75,71,65,0;
       dither: true;


### Virtual Bass Array filters ###

filter "lVBAfilter" {
      from_inputs: "left"/0.5;
      to_filters: "lsubfilter", "llowfilter", "lmidfilter", "lhighfilter";
      coeff: "lVBA";

filter "rVBAfilter" {
      from_inputs: "left"/0.5;
      to_filters: "rsubfilter", "rlowfilter", "rmidfilter", "rhighfilter";
      coeff: "rVBA";          

### XO filters ###

filter "lsubfilter" {
       from_filters: "lVBAfilter";
       to_outputs: "lsub"/0;
       coeff: "leftsub";
#      coeff: -1;

filter "rsubfilter" {
       from_filters: "rVBAfilter";
       to_outputs: "rsub"/0;
       coeff: "rightsub";
#	coeff: -1;

filter "llowfilter" {
       from_filters: "lVBAfilter";
       to_outputs: "llow"/0;
       coeff: "leftwof";
#	coeff: -1;

filter "rlowfilter" {
       from_filters: "rVBAfilter";
       to_outputs: "rlow"/0;
       coeff: "rightwof";
#	coeff: -1;

filter "lmidfilter" {
       from_filters: "lVBAfilter";
       to_outputs: "lmid"/0;
       coeff: "leftmid";
#	coeff: -1;

filter "rmidfilter" {
      from_filters: "rVBAfilter";
      to_outputs: "rmid"/0;
      coeff: "rightmid";
#	coeff: -1;

filter "lhighfilter" {
      from_filters: "lVBAfilter";
      to_outputs: "lhigh"/0;
      coeff: "lefttwet";
#     coeff: -1;

filter "rhighfilter" {
      from_filters: "rVBAfilter";
      to_outputs: "rhigh"/0;
      coeff: "righttwet";
#     coeff: -1;


The sound! well you have to here it to believe it.


Here are a few FAQ that I got over the email exchanges

1. Is there a reason we restrict the correction to bass region? It the aim of the array is to get it of standing wave, it should also help the mid range

Yes, the reason is, that we make a very simplified  assumption on how the standig wave will build up in a room.

This assumpltion will not be valid in the midrange. In addition we have to guess how strong different frequncies are reflected  by the backwall.Its hard to do this for the midrange.

2. why this problem is not corrected by standard room correction approach?

Acoourate applies time windowing.In the  prefilter you can does not applie a time window.

3. Is there any point of running virtual bass array for the width and height dimensions?

See no.1 Its very hard to develop a model how the standing wave build up in a real room.
Also you try to compensate at single points in your room. To do it for multiple modes you will end up with a seperate real bus array for each mode.

There is one difference in applying a VBA as a prefilter:
The pefilter can correct every time window independent from the time window acoourate applies before correction. So you can have long correction window for a specific room mode and shorter window for the overall correction in accourate. This may result in better correction of the room mode if you cannot appliy long enought windows in  acourate for other reasons.

In applying prefilters you make it easier for acourate to find the correction without getting unstable results, because you have allready manuallly corrected a singel aspect before.