VAE - Virtual Audio Engine 1
Small Data Driven Audio Engine
vae::core::SpatialProcessor Class Reference

#include <vae_spatial_processor.hpp>

Collaboration diagram for vae::core::SpatialProcessor:

Public Member Functions

Result init (Size hrtfVoices)
 
Size mix (VoiceManger &manager, Bank &bank, SpatialManager &spatial, SampleIndex frames, Size sampleRate)
 Process a single bank. More...
 
Result loadHRTF (const char *path, Size length, const char *rootPath, Size sampleRate)
 

Private Attributes

HRTF mHRTF
 Currently loaded HRTF, there can only be one. More...
 
HRTFLoader mHRTFLoader
 Struct to decode the hrtf. More...
 
HeapBuffer< VoiceHRTFmVoiceHRTFs
 Working data for convolution. More...
 
ScratchBuffer mScratchBuffer
 Temporary filtered/looped signal TODO this will not work with parallel bank processing. More...
 

Detailed Description

Definition at line 20 of file vae_spatial_processor.hpp.

Member Function Documentation

◆ init()

Result vae::core::SpatialProcessor::init ( Size  hrtfVoices)
inline

Definition at line 29 of file vae_spatial_processor.hpp.

29 {
30 VAE_PROFILER_SCOPE_NAMED("Spatial Processor Init")
31 mVoiceHRTFs.resize(hrtfVoices);
32 mScratchBuffer.resize(StaticConfig::MaxBlock);
33 return Result::Success;
34 }
ScratchBuffer mScratchBuffer
Temporary filtered/looped signal TODO this will not work with parallel bank processing.
HeapBuffer< VoiceHRTF > mVoiceHRTFs
Working data for convolution.
constexpr Size MaxBlock
Maximum block size.
Definition: vae.hpp:276
Result
Return Types for most engine functions.
Definition: vae.hpp:73
#define VAE_PROFILER_SCOPE_NAMED(name)
Profiles a scope and names it.
Here is the call graph for this function:

◆ loadHRTF()

Result vae::core::SpatialProcessor::loadHRTF ( const char *  path,
Size  length,
const char *  rootPath,
Size  sampleRate 
)
inline

Definition at line 282 of file vae_spatial_processor.hpp.

282 {
283 Result result = mHRTFLoader.load(path, length, rootPath, sampleRate, mHRTF);
284 if (result != Result::Success) { return result; }
285 for (auto& i : mVoiceHRTFs) {
286 i.convolutionBuffer.resize(mHRTF.irLength);
287 i.convolutionBuffer.set();
288 }
289 return Result::Success;
290 }
Result load(const char *path, Size length, const char *rootPath, const Size sampleRate, HRTF &hrtf)
HRTFLoader mHRTFLoader
Struct to decode the hrtf.
HRTF mHRTF
Currently loaded HRTF, there can only be one.
Here is the call graph for this function:
Here is the caller graph for this function:

◆ mix()

Size vae::core::SpatialProcessor::mix ( VoiceManger manager,
Bank bank,
SpatialManager spatial,
SampleIndex  frames,
Size  sampleRate 
)
inline

Process a single bank.

Parameters
manager
banks
frames
sampleRate
Returns
Number of voices mixed

Pan and mix templated lambda so we don't have to write this for each vonfig

Parameters
pannerGet's a panner instance with pan() and speakers() function

Definition at line 45 of file vae_spatial_processor.hpp.

49 {
50 Size actuallyRendered = 0;
51 VAE_PROFILER_SCOPE_NAMED("Spatial Processor")
52 manager.forEachVoice([&](Voice& v, Size vi) {
53 if (v.bank != bank.id) { return true; } // wrong bank
54 if (!v.spatialized) { return true; } // not spatialized
55 VAE_PROFILER_SCOPE_NAMED("Spatial Voice")
56 if (!spatial.hasEmitter(v.emitter)) {
57 VAE_DEBUG("Spatial voice is missing emitter")
58 return false; // ! needs emitter
59 }
60
61 auto& source = bank.sources[v.source];
62 auto& signal = source.signal;
63
64 const auto signalLength = signal.size();
65
66 if (signalLength == 0) { return false; } // ! no signal
67
68 v.time = v.time % signalLength; // Keep signal in bounds before starting
69
70 if (signal.sampleRate != sampleRate) {
71 // VAE_DEBUG("Spatial Voice samplerate mismatch. Enabled filter.")
72 v.filtered = true; // implicitly filter to resample
73 }
74
75 auto& emitter = spatial.getEmitter(v.emitter);
76 auto& target = bank.mixers[v.mixer].buffer;
77 const Sample gain = v.gain * source.gain;
78 auto& l = spatial.getListeners()[v.listener];
79
80 Real distanceAttenuated;
81 Vec3 relativeDirection;
82 // * Attenuation calculation
83 {
84 VAE_PROFILER_SCOPE_NAMED("Attenuation calculation")
85 // samething as graphics, make the world rotate round the listener
86 // TODO this should be possible without a 4x4 matrix?
87 glm::mat4x4 lookAt = glm::lookAt(l.position, l.position + l.front, l.up);
88 // listener is the world origin now
89 relativeDirection = (lookAt * glm::vec4(emitter.position, 1.f));
90
91
92 const Real distance = std::max(glm::length(relativeDirection), 0.1f);
93 relativeDirection /= distance;
94
95 if (v.attenuate) {
96 distanceAttenuated = distance;
97 distanceAttenuated = std::max(distanceAttenuated, Real(1)); // we don't want to get louder than 1
98 distanceAttenuated = Real(1) / distanceAttenuated;
99 } else {
100 distanceAttenuated = 1.0;
101 }
102 distanceAttenuated *= gain;
103 }
104
105 if (distanceAttenuated < StaticConfig::MinVolume) {
106 return true; // ! inaudible
107 // TODO maybe progress still progress time?
108 }
109 actuallyRendered++;
110 target.setValidSize(frames); // mark mtarget ixer as active
111 v.audible = true;
112
113 // * Filtering and looping logic
114
115 // TODO This thing is littered with branches, maybe needs some cleanup
116
117 const Sample* in; // The filtered, looped original signal used for panning later. We only do mono signals
118 SampleIndex remaining = frames; // playback speed and looping affects this
119 bool finished = false; // the return value of this function stops the voice
120
121 if (v.filtered) {
122 VAE_PROFILER_SCOPE_NAMED("Voice Filter")
123 auto& fd = manager.getVoiceFilter(vi);
124
125 if (!v.started) {
126 // Initialize filter variables when first playing the voice
127 fd.highpassScratch[0] = 0;
128 fd.lowpassScratch[0] = signal[0][v.time];
129 }
130
131 // Playback speed taking samplerate into account
132 const Sample speed = fd.speed * (Sample(signal.sampleRate) / Sample(sampleRate));
133
134 if (!v.loop) {
135 // If we're not looping, end time calculation is a bit more complex
136 remaining = std::min(
137 frames,
138 SampleIndex(std::floor((signalLength - v.time) / speed - fd.timeFract))
139 );
140 finished = remaining != frames; // we might have reached the end
141 }
142
143 // fractional time, we need the value after the loop, so it's defined outside
144 Real position;
145 for (SampleIndex s = 0; s < frames; s++) {
146 // Linear interpolation between two samples
147 position = v.time + (s * speed) + fd.timeFract;
148 const Real lastPosition = std::floor(position);
149 const Size lastIndex = (Size) lastPosition;
150 const Size nextIndex = (Size) lastPosition + 1;
151
152 Real mix = position - lastPosition;
153 // mix = 0.5 * (1.0 - cos((mix) * 3.1416)); // cosine interpolation, introduces new harmonics somehow
154
155 // TODO 30% of the time in here is spent on the modulo
156 const Sample last = signal[0][lastIndex % signalLength];
157 const Sample next = signal[0][nextIndex % signalLength];
158 // linear resampling, sounds alright enough
159 const Sample in = (last + mix * (next - last)) * gain;
160
161 // * super simple lowpass and highpass filter
162 // just lerps with a previous value
163 const Sample lpd = in + fd.lowpass * (fd.lowpassScratch[0] - in);
164 fd.lowpassScratch[0] = lpd;
165
166 const Sample hps = fd.highpassScratch[0];
167 const Sample hpd = hps + fd.highpass * (in - hps);
168 fd.highpassScratch[0] = hpd;
169
170 mScratchBuffer[0][s] = (lpd - hpd);
171 }
172 position += speed; // step to next sample
173 v.time = (SampleIndex) std::floor(position); // split the signal in normal sample position
174 fd.timeFract = position - v.time; // and fractional time for the next block
175 v.time = v.time; // set index back
176 in = mScratchBuffer[0]; // set the buffer to use for panning
177 } else {
178 VAE_PROFILER_SCOPE_NAMED("Non filtered Voice")
179 if (v.loop) {
180 // put the looped signal in scratch buffer eventhough we're not filtering
181 // so panning doesn't need to worry about looping
182 for (SampleIndex s = 0; s < frames; s++) {
183 mScratchBuffer[0][s] = signal[0][(v.time + s) % signalLength];
184 }
185 v.time = (v.time + frames); // progress the time
186 in = mScratchBuffer[0]; // set buffer for panning
187 finished = false; // never stop the voice
188 } else {
189 // Not filtering or looping
190 // Means we can use the original signal buffer but need to
191 // set the remaining samples so we don't run over the signal end
192 remaining = std::min(
193 frames, SampleIndex(signalLength - v.time
194 ));
195 in = signal[0] + v.time;
196 finished = remaining != frames; // we might have reached the end
197 v.time += remaining; // progress time in voice
198 }
199 }
200
201 if (l.configuration == Listener::Configuration::HRTF && v.HRTF && mHRTF.rate) {
202 // * HRTF Panning
203 VAE_ASSERT(vi < mVoiceHRTFs.size()) // only the lower voice can use hrtfs
204 VAE_PROFILER_SCOPE_NAMED("Render HRTF")
205
206 Size closestIndex = HRTFUtil::closest(mHRTF, relativeDirection);
207
208 if (closestIndex == ~Size(0)) { return true; } // ! no hrtf found?
209
210 auto& hrtfVoice = mVoiceHRTFs[vi];
211
212 if (!v.started) { // clear old data
213 hrtfVoice.convolutionIndex = 0;
214 hrtfVoice.convolutionBuffer.set();
215 }
216
218 mHRTF.positions[closestIndex],
219 hrtfVoice, remaining, target, in, distanceAttenuated
220 );
221
222 } else {
223 VAE_PROFILER_SCOPE_NAMED("Render SPCAP")
224 // * Normal SPCAP panning
225 auto& lastPan = manager.getVoicePan(vi);
226 VoicePan currentPan;
227 auto& currentVolumes = currentPan.volumes;
228 auto& lastVolumes = lastPan.volumes;
229
230 /**
231 * @brief Pan and mix templated lambda so we don't have to write this for each vonfig
232 * @param panner Get's a panner instance with pan() and speakers() function
233 */
234 const auto pan = [&](const auto& panner) {
235 // This is actually constexpr but not according to clangd
236 constexpr Size channels = std::min(Size(StaticConfig::MaxChannels), panner.speakers);
237 panner.pan(
238 relativeDirection, currentVolumes,
239 distanceAttenuated, emitter.spread
240 );
241
242 if (!v.started) {
243 // first time don't interpolate
244 for (Size c = 0; c < channels; c++) {
245 lastVolumes[c] = currentVolumes[c];
246 }
247 }
248
249 Sample t = 0;
250 for (SampleIndex s = 0; s < remaining; s++) {
251 const Sample sample = in[s];
252 // lerp between last and current channel volumes
253 // Not correct in terms of power convservation, but easy and efficient
254 for (Size c = 0; c < channels; c++) {
255 target[c][s] += sample * (lastVolumes[c] + t * (currentVolumes[c] - lastVolumes[c]));
256 }
257 t += Sample(1) / Sample(frames);
258 }
259 };
260
261 switch (l.configuration) {
268 }
269
270 lastPan = std::move(currentPan);
271 }
272 v.started = true;
273 if (finished) {
274 emitter.autoplaying = false;
275 return false;
276 }
277 return true;
278 });
279 return actuallyRendered;
280 }
Size mix(VoiceManger &manager, Bank &bank, SpatialManager &spatial, SampleIndex frames, Size sampleRate)
Process a single bank.
T min(const T &v1, const T &v2)
Definition: TMath.hpp:16
T max(const T &v1, const T &v2)
Definition: TMath.hpp:21
constexpr Sample MinVolume
Minimum volume before sounds will skip rendering.
Definition: vae.hpp:307
constexpr unsigned char MaxChannels
Maximum channel count used to pre allocate buffers.
Definition: vae.hpp:268
AudioBuffer::Size SampleIndex
Definition: vae_types.hpp:87
float Real
Definition: vae_types.hpp:48
glm::vec3 Vec3
Definition: vae_types.hpp:47
unsigned int Size
How the elements are addressed in the heapbuffer.
Definition: vae.hpp:33
float Sample
Default sample types used where ever possible, changing this means the engine needs to be recompiled,...
Definition: vae.hpp:32
HeapBuffer< Position > positions
Definition: vae_hrtf.hpp:13
static void apply(HRTF::Position &hrtf, VoiceHRTF &hrtfVoice, SampleIndex frames, ScratchBuffer &target, const Sample *in, Sample distanceAttenuated)
Applies simple time domain convolution.
static const SPCAPConfig< 2 > StereroSPCAP
Definition: vae_spcap.hpp:84
static const SPCAPConfig< 1 > MonoSPCAP
TODO there's probably a smart way to make this all constexpr.
Definition: vae_spcap.hpp:82
static const SPCAPConfig< 4 > QuadSPCAP
Definition: vae_spcap.hpp:85
static const SPCAPConfig< 5 > SuroundSPCAP
Definition: vae_spcap.hpp:86
static const SPCAPConfig< 2 > HeadphoneSPCAP
Definition: vae_spcap.hpp:83
#define VAE_DEBUG(msg,...)
Definition: vae_logger.hpp:83
#define VAE_ASSERT(condition)
Definition: vae_util.hpp:11
Here is the call graph for this function:
Here is the caller graph for this function:

Member Data Documentation

◆ mHRTF

HRTF vae::core::SpatialProcessor::mHRTF
private

Currently loaded HRTF, there can only be one.

Definition at line 21 of file vae_spatial_processor.hpp.

◆ mHRTFLoader

HRTFLoader vae::core::SpatialProcessor::mHRTFLoader
private

Struct to decode the hrtf.

Definition at line 22 of file vae_spatial_processor.hpp.

◆ mScratchBuffer

ScratchBuffer vae::core::SpatialProcessor::mScratchBuffer
private

Temporary filtered/looped signal TODO this will not work with parallel bank processing.

Definition at line 27 of file vae_spatial_processor.hpp.

◆ mVoiceHRTFs

HeapBuffer<VoiceHRTF> vae::core::SpatialProcessor::mVoiceHRTFs
private

Working data for convolution.

Definition at line 23 of file vae_spatial_processor.hpp.


The documentation for this class was generated from the following file: