Audio Rendering Parameter
Generation on the SPU
A tale of porting a heavyweight
function.
SoundEngine::Process()
Fills out all sound rendering parameters that are
passed into the synthesis engine.
– Eg. Volume, azimuth, elevation, reverb send levels,
filter cutoff.
Rendering parameters are calculated from:
– listener position,
– listener direction, and
– sound positions,
– output from indirect audio.
Called once per sound, per frame.
Perfect candidate for parallelization.
SPURS Jobs: A convenient fit
Appropriate for short bits of code that is executed
many times on independent data.
SPURS will take care of scheduling and will play
nicely with other SPURS workloads.
SPURS takes care of running multiple
SoundEngineProcessJob’s which have been queued
up in a job list in parallel!
SPURS takes care of pipelining asynchronous I/O.
– While the current job is running, the output from the
last job is being DMA’s back to PPU, and/or the input for
the next job is being DMA’d to the SPU.
Determining Inputs
Listener Position, direction.
Sound Position.
Static sound rendering parameters.
– Defined for each individual sound .csv
– Encapsulated in “Spatial Info” struct
Filtered render state.
– Output data from indirect audio, smoothed over time.
Reverb preset for sound’s position in space.
Is the sound visible to the listener?
– Causes direct sound to be filtered.
Is the sound in the same reverb region as the
listener.
– Causes reverberated sound to be filtered.
Determining Outputs
SoundParams struct.
– Passed into SCREAM to tell NextSynth how to
render the sound.
– Defines vol, pan, pitch, SCREAM registers,
various filters, send levels, special FX, etc.
Reverberation accumulation
– There are many sounds, but only 6 reverb
units.
– Reverb units “accumulate” directional gain
from each currently playing sound.
Movin’ stuff around.
Pointers are no longer valid after data has been
copied to the SPU!
Classes with virtual functions will not work
without some v-table patching trickery. They are
best avoided.
SPU’s like data in flat arrays which are aligned to
16-byte boundaries, so they can be easily
copied.
Simplify complex classes into structs for data
that is copied to/from the SPU.
Use structures of arrays where appropriate for
vectorization.
A SoundEmitter now keeps track of the sounds that it is emitting in a struct of arrays,
instead of a list. This allows the SoundParams to be DMA’s directly into the array.
/* /*
* SoundsEmitter * SoundsEmitter
*/ */
class SoundEmitter: public SoundEmitterBase class SoundEmitter: public SoundEmitterBase
{ {
public: public:
struct SoundEntry: public
AudioPtrBase::type struct SoundEntryVector
{ {
typedef AudioPtr::type SoundEntryVector(uint size);
Ptr;
~SoundEntryVector();
SoundEntry(); SoundInstancePtr AddSound( … );
~SoundEntry(); void Erase(unsigned int i);
SoundInstancePtr mSound;
PlayParams mParams; SoundInstancePtr* mSoundsArray;
SoundParams* mParamsArray;
float mElapsedTime; float* mElapsedTimesArray;
float mTriggerTime; float* mTriggerTimesArray;
}; SpatialInfo* mSpatialInfoArray;
typedef List const SoundDef** mSoundDefsArray;
SoundEntryList;
unsigned int size;
SoundEntryList mSoundEntries; unsigned int maxSize;
};
…
SoundEntryVector mSoundEntries;
};
The IIR class gets flattened out, and vectorized. Turns out there is only one type of IIR
class IIR : public AlignedObject class IIRArray : public AlignedObject {
{ public:
public:
IIRArray();
IIR();
virtual ~IIR(); float GetValue(unsigned int idx) const;
float GetValue() const; void GetValuesRange(u32 offs, float* out, u32
void SetValue(float value); n) const;
void SetConstant(float constant); void SetValuesRange(u32 offs, const float*
virtual void AddSample(float newsample); value, u32 n);
void SetHalfLifes(const float* halflife);
protected:
float GetHalfLife(unsigned int idx) const;
float m_curval;
float m_constant;
float m_invConstant; void AddSamples(const float* newsamples, float
}; deltatime);
class TimeDependentIIR : public IIR protected:
{
static const uint ARRAY_SIZE = 24;
public:
TimeDependentIIR(); static const uint NUM_VECS = ARRAY_SIZE/4;
virtual ~TimeDependentIIR();
void SetHalfLife(float halflife); union{ vector float m_curval[NUM_VECS];
float GetHalfLife() const; float m_curval_s[ARRAY_SIZE]; };
virtual void AddSample(float sample, float
dt); union{ vector float m_constant[NUM_VECS];
float m_constant_s[ARRAY_SIZE];};
protected: union{ vector float m_invConstant [NUM_VECS];
float m_halfLife; float m_invConstant_s[ARRAY_SIZE];};
};
union{ vector float m_halfLife[NUM_VECS];
float m_halfLife_s[ARRAY_SIZE];};
};
class FilteredRenderState : public AlignedObject, public NonCopyable class FilteredRenderState : public AlignedObject, public NonCopyable
{
{
…
Private: public:
SMath::Vector m_unfilteredIndirectDirection; enum
SMath::Point m_unfilteredIndirectPosition; {
DirectDistanceIdx,
float m_unfilteredIndirectDistance;
DirectFocusIdx,
TimeDependentIIR m_filteredIndirectDistance;
There… DirectOcclusionLevelIdx,
float m_unfilteredDirectDistance; DirectObstructionLevelIdx,
TimeDependentIIR m_filteredDirectDistance; IndirectDistanceIdx,
IndirectFocusIdx,
float m_unfilteredDirectFocus;
TimeDependentIIR m_filteredDirectFocus; ..much better! IndirectOcclusionLevelIdx,
IndirectObstructionLevelIdx,
Float m_unfilteredIndirectFocus; IndirectDirection0Idx,
TimeDependentIIR m_filteredIndirectFocus; IndirectDirection1Idx,
IndirectDirection2Idx,
TimeDependentIIR m_filteredIndirectDirectionX;
IndirectDirection3Idx,
TimeDependentIIR m_filteredIndirectDirectionY;
TimeDependentIIR m_filteredIndirectDirectionZ; IndirectPosition0Idx,
IndirectPosition1Idx,
TimeDependentIIR m_filteredIndirectPositionX; IndirectPosition2Idx,
TimeDependentIIR m_filteredIndirectPositionY; IndirectPosition3Idx,
TimeDependentIIR m_filteredIndirectPositionZ;
SourceReverbGain0Idx,
float m_unfilteredDirectOcclusionLevel; SourceReverbGain1Idx,
TimeDependentIIR m_filteredDirectOcclusionLevel; SourceReverbGain2Idx,
SourceReverbGain3Idx,
float m_unfilteredIndirectOcclusionLevel; SourceReverbGain4Idx,
TimeDependentIIR m_filteredIndirectOcclusionLevel;
SourceReverbGain5Idx,
float m_unfilteredIndirectObstructionLevel; SourceReverbGain6Idx,
TimeDependentIIR m_filteredIndirectObstructionLevel; SourceReverbGain7Idx,
NUM_INDICIES
float m_unfilteredDirectObstructionLevel; };
TimeDependentIIR m_filteredDirectObstructionLevel;
…
float m_unfilteredSourceReverbGains[NUM_REVERB_GAINS];
TimeDependentIIR m_filteredSourceReverbGains[NUM_REVERB_GAINS]; float m_unfilteredValues[NUM_INDICIES];
IIRArray m_filteredValues;
};
Comparison of PPU/SPU methods
• 3-Player game.
• After data structure reorganization.
• SoundEngine::PreUpdate() w/ PPU Process() call
(PreUpdate calls Process directy.)
• Min 1.27 ms
• Max 5.51 ms
• Avg 2.959 ms
•SoundEngine::PreUpdate() w/ SPU Process() call. (PreUpdate queues up
SPU jobs)
•Min 529.2 us
•Max 3.18 ms
•Avg 1.16 ms