Difference between revisions of "PCSX2 Documentation/Reverb Engine"

From PCSX2 Wiki
Jump to navigation Jump to search
Line 1: Line 1:
{{DocTabs|Section=2}}
+
==Introduction==
Introduction
+
I will try to explain here what I know of the SPU2's Reverb, which is practically identical to the one in the PS1. All my knowledge comes mostly form the great (but not quite accurate) document by Neill Corlett, and some pages I found online about reverberation and impulse responses.
I will try to explain here what I know of the SPU2's Reverb, which is practically identical to the one in the PS1.
 
  
All my knowledge comes mostly form the great (but not quite accurate) document by Neill Corlett, and some pages I found online about reverberation and impulse responses.
+
==The Reverb Engine==
 +
The basis of the reverb engine is a standard Schroeder Reverberator. This reverberator is formed by four parallel Comb filters, taking data form four sample queues which are fed from the input. These filters are the source of the main echos, the first reflections of the sound waves coming from the walls of the virtual room. The mixed output from the four Comb filters passes through a pair of All-pass filters, which contain a sample queue each, and they feed themselves the data, causing a controlled feedback loop which multiplies the density of the echos, but reduces them in volume over time.
  
The Reverb Engine
+
==The SPU2 Reverb Engine==
The basis of the reverb engine is a standard Schroeder Reverberator.
 
 
 
This reverberator is formed by four parallel Comb filters, taking data form four sample queues which are fed from the input.
 
 
 
These filters are the source of the main echos, the first reflections of the sound waves coming from the walls of the virtual room.
 
 
 
The mixed output from the four Comb filters passes through a pair of All-pass filters, which contain a sample queue each, and they feed themselves the data, causing a controlled feedback loop which multiplies the density of the echos, but reduces them in volume over time.
 
 
 
The SPU2 Reverb Engine
 
 
Instead of having a series of separate queues, the SPU2 has a single rotating buffer and uses offsets to split this buffer into the queues it will require. The different blocks of the reverb engine will write to different locations of the buffer, which will advance on each processing cycle, and then data will be read from locations some samples away.
 
Instead of having a series of separate queues, the SPU2 has a single rotating buffer and uses offsets to split this buffer into the queues it will require. The different blocks of the reverb engine will write to different locations of the buffer, which will advance on each processing cycle, and then data will be read from locations some samples away.
  
While the data is eventually overwritten by other queues, it does not matter because the readers have already used the data while it was available.
+
While the data is eventually overwritten by other queues, it does not matter because the readers have already used the data while it was available. The reverb engine takes the data from the input lines (stereo), passes it through a configurable IIR filter, and puts it in the input queue. At the same time, the four comb filters take data from different points of the input queue, which is shared for all of them. The All-pass filters have their own queue each, and seem to match the standard design.
 
 
The reverb engine takes the data from the input lines (stereo), passes it through a configurable IIR filter, and puts it in the input queue.
 
 
 
At the same time, the four comb filters take data from different points of the input queue, which is shared for all of them.
 
 
 
The All-pass filters have their own queue each, and seem to match the standard design.
 
  
Pseudo-C Implementation
+
==Pseudo-C Implementation==
 
In the following code, Buffer represents the rotating buffer, which will be used using something similar to
 
In the following code, Buffer represents the rotating buffer, which will be used using something similar to
  
 +
<source lang="cpp">
 
Buffer[x] === Spu2_Memory[ EffectsBufferBase + (EffectsBufferPosition + offset) % EffectsBufferSize ]
 
Buffer[x] === Spu2_Memory[ EffectsBufferBase + (EffectsBufferPosition + offset) % EffectsBufferSize ]
 +
</source>
 
The Revb structure contains all the (wrong) register names currently used. If this implementation turns out to work correctly, I will change the names of those registers so they represent their actual usage.
 
The Revb structure contains all the (wrong) register names currently used. If this implementation turns out to work correctly, I will change the names of those registers so they represent their actual usage.
  
 
This is the current state of the reference implementation I used in an Impulse Response Analyzer I wrote:
 
This is the current state of the reference implementation I used in an Impulse Response Analyzer I wrote:
  
 +
<source lang="cpp">
 
// Input filter
 
// Input filter
 
// Writes the data to the input queues for the echos below
 
// Writes the data to the input queues for the echos below
Line 111: Line 99:
  
 
return new StereoOut32( ACC0 >> 16, ACC1 >> 16);
 
return new StereoOut32( ACC0 >> 16, ACC1 >> 16);
 +
</source>
  
{{PCSX2 Main Documentation Navbox}}
+
{{PCSX2 Documentation Navbox}}

Revision as of 20:51, 19 July 2015

Introduction

I will try to explain here what I know of the SPU2's Reverb, which is practically identical to the one in the PS1. All my knowledge comes mostly form the great (but not quite accurate) document by Neill Corlett, and some pages I found online about reverberation and impulse responses.

The Reverb Engine

The basis of the reverb engine is a standard Schroeder Reverberator. This reverberator is formed by four parallel Comb filters, taking data form four sample queues which are fed from the input. These filters are the source of the main echos, the first reflections of the sound waves coming from the walls of the virtual room. The mixed output from the four Comb filters passes through a pair of All-pass filters, which contain a sample queue each, and they feed themselves the data, causing a controlled feedback loop which multiplies the density of the echos, but reduces them in volume over time.

The SPU2 Reverb Engine

Instead of having a series of separate queues, the SPU2 has a single rotating buffer and uses offsets to split this buffer into the queues it will require. The different blocks of the reverb engine will write to different locations of the buffer, which will advance on each processing cycle, and then data will be read from locations some samples away.

While the data is eventually overwritten by other queues, it does not matter because the readers have already used the data while it was available. The reverb engine takes the data from the input lines (stereo), passes it through a configurable IIR filter, and puts it in the input queue. At the same time, the four comb filters take data from different points of the input queue, which is shared for all of them. The All-pass filters have their own queue each, and seem to match the standard design.

Pseudo-C Implementation

In the following code, Buffer represents the rotating buffer, which will be used using something similar to

Buffer[x] === Spu2_Memory[ EffectsBufferBase + (EffectsBufferPosition + offset) % EffectsBufferSize ]

The Revb structure contains all the (wrong) register names currently used. If this implementation turns out to work correctly, I will change the names of those registers so they represent their actual usage.

This is the current state of the reference implementation I used in an Impulse Response Analyzer I wrote:

// Input filter
// Writes the data to the input queues for the echos below
{
    var INPUT_SAMPLE_L = Input.Left * Revb.IN_COEF_L;
    var INPUT_SAMPLE_R = Input.Right * Revb.IN_COEF_R;

    var IIR_INPUT_A0 = Buffer[Revb.IIR_SRC_A0] * Revb.IIR_COEF + INPUT_SAMPLE_L;
    var IIR_INPUT_A1 = Buffer[Revb.IIR_SRC_A1] * Revb.IIR_COEF + INPUT_SAMPLE_R;
    var IIR_INPUT_B0 = Buffer[Revb.IIR_SRC_B0] * Revb.IIR_COEF + INPUT_SAMPLE_L;
    var IIR_INPUT_B1 = Buffer[Revb.IIR_SRC_B1] * Revb.IIR_COEF + INPUT_SAMPLE_R;

    var IIR_DA0 = Buffer[Revb.IIR_DEST_A0];
    var IIR_DA1 = Buffer[Revb.IIR_DEST_A1];
    var IIR_DB0 = Buffer[Revb.IIR_DEST_B0];
    var IIR_DB1 = Buffer[Revb.IIR_DEST_B1];

    Buffer[Revb.IIR_DEST_A0 + one] = clamp_mix( IIR_DA0 + ((((IIR_INPUT_A0 >> 16) - IIR_DA0) * Revb.IIR_ALPHA) >> 16) );
    Buffer[Revb.IIR_DEST_A1 + one] = clamp_mix( IIR_DA1 + ((((IIR_INPUT_A1 >> 16) - IIR_DA1) * Revb.IIR_ALPHA) >> 16) );
    Buffer[Revb.IIR_DEST_B0 + one] = clamp_mix( IIR_DB0 + ((((IIR_INPUT_B0 >> 16) - IIR_DB0) * Revb.IIR_ALPHA) >> 16) );
    Buffer[Revb.IIR_DEST_B1 + one] = clamp_mix( IIR_DB1 + ((((IIR_INPUT_B1 >> 16) - IIR_DB1) * Revb.IIR_ALPHA) >> 16) );
}

int ACC0 = 0;
int ACC1 = 0;

// Classic Schroeder Reverberator:
{
    // 4 Comb filters
    ACC0 += Buffer[Revb.ACC_SRC_A0] * Revb.ACC_COEF_A;
    ACC1 += Buffer[Revb.ACC_SRC_A1] * Revb.ACC_COEF_A;
    ACC0 += Buffer[Revb.ACC_SRC_B0] * Revb.ACC_COEF_B;
    ACC1 += Buffer[Revb.ACC_SRC_B1] * Revb.ACC_COEF_B;
    ACC0 += Buffer[Revb.ACC_SRC_C0] * Revb.ACC_COEF_C;
    ACC1 += Buffer[Revb.ACC_SRC_C1] * Revb.ACC_COEF_C;
    ACC0 += Buffer[Revb.ACC_SRC_D0] * Revb.ACC_COEF_D;
    ACC1 += Buffer[Revb.ACC_SRC_D1] * Revb.ACC_COEF_D;

    // First All-pass filter:
    {
        // Take delayed input
        var FB_A0 = Buffer[Revb.MIX_DEST_A0 - Revb.FB_SRC_A];
        var FB_A1 = Buffer[Revb.MIX_DEST_A1 - Revb.FB_SRC_A];

        // Apply gain and add to input
        var MIX_A0 = (ACC0 + FB_A0 * Revb.FB_ALPHA) >> 16;
        var MIX_A1 = (ACC1 + FB_A1 * Revb.FB_ALPHA) >> 16;
                                        
        // Write to queue
        Buffer[Revb.MIX_DEST_A0] = clamp_mix(MIX_A0);
        Buffer[Revb.MIX_DEST_A1] = clamp_mix(MIX_A1);

        // Apply second gain and add
        ACC0 += (FB_A0 << 16) - MIX_A0 * Revb.FB_ALPHA;
        ACC1 += (FB_A1 << 16) - MIX_A1 * Revb.FB_ALPHA;
    }

    // Second All-pass filter:
    {
        // Take delayed input
        var FB_B0 = Buffer[Revb.MIX_DEST_B0 - Revb.FB_SRC_B];
        var FB_B1 = Buffer[Revb.MIX_DEST_B1 - Revb.FB_SRC_B];

        // Apply gain and add to input
        var MIX_B0 = (ACC0 + FB_B0 * Revb.FB_X) >> 16;
        var MIX_B1 = (ACC1 + FB_B1 * Revb.FB_X) >> 16;
                                        
        // Write to queue
        Buffer[Revb.MIX_DEST_B0] = clamp_mix(MIX_B0);
        Buffer[Revb.MIX_DEST_B1] = clamp_mix(MIX_B1);

        // Apply second gain and add
        ACC0 += (FB_B0 << 16) - MIX_B0 * Revb.FB_X;
        ACC1 += (FB_B1 << 16) - MIX_B1 * Revb.FB_X;
    }

}

return new StereoOut32( ACC0 >> 16, ACC1 >> 16);