VAE - Virtual Audio Engine 1
Small Data Driven Audio Engine
vae_spatial_processor.hpp
Go to the documentation of this file.
1#ifndef _VAE_SPATIAL_PROCESSOR
2#define _VAE_SPATIAL_PROCESSOR
3
4#include "../vae_types.hpp"
5#include "../vae_util.hpp"
6#include "../pod/vae_bank.hpp"
7#include "../voices/vae_voice.hpp"
8#include "../voices/vae_voice_filter.hpp"
9#include "../voices/vae_voice_pan.hpp"
10#include "../voices/vae_voice_hrtf.hpp"
11#include "../vae_voice_manager.hpp"
12#include "../vae_spatial_manager.hpp"
13#include "../algo/vae_spcap.hpp"
14#include "../algo/vae_hrtf_util.hpp"
15
16#include "../fs/vae_hrtf_loader.hpp"
17#include "../../../external/glm/glm/gtc/matrix_transform.hpp"
18
19namespace vae { namespace core {
21 HRTF mHRTF; ///< Currently loaded HRTF, there can only be one
22 HRTFLoader mHRTFLoader; ///< Struct to decode the hrtf
23 HeapBuffer<VoiceHRTF> mVoiceHRTFs; ///< Working data for convolution
24 /**
25 * @brief Temporary filtered/looped signal TODO this will not work with parallel bank processing
26 */
28 public:
29 Result init(Size hrtfVoices) {
30 VAE_PROFILER_SCOPE_NAMED("Spatial Processor Init")
31 mVoiceHRTFs.resize(hrtfVoices);
33 return Result::Success;
34 }
35
36 /**
37 * @brief Process a single bank
38 *
39 * @param manager
40 * @param banks
41 * @param frames
42 * @param sampleRate
43 * @return Number of voices mixed
44 */
46 VoiceManger& manager, Bank& bank,
47 SpatialManager& spatial,
48 SampleIndex frames, Size sampleRate
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 }
281
282 Result loadHRTF(const char* path, Size length, const char* rootPath, Size sampleRate) {
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 }
291 };
292
294
295} } // core::vae
296
297#endif // _VAE_SPATIAL_PROCESSOR
bool resize(const Size length, uchar channels)
! Will not keep the contents! Resizes the buffer to the desired length and channel count.
Basically a bad std::vector without exceptions which can also work with foreign memory.
Definition: THeapBuffer.hpp:49
Result load(const char *path, Size length, const char *rootPath, const Size sampleRate, HRTF &hrtf)
bool hasEmitter(EmitterHandle e)
Emitter & getEmitter(EmitterHandle e)
HRTFLoader mHRTFLoader
Struct to decode the hrtf.
ScratchBuffer mScratchBuffer
Temporary filtered/looped signal TODO this will not work with parallel bank processing.
HeapBuffer< VoiceHRTF > mVoiceHRTFs
Working data for convolution.
Size mix(VoiceManger &manager, Bank &bank, SpatialManager &spatial, SampleIndex frames, Size sampleRate)
Process a single bank.
HRTF mHRTF
Currently loaded HRTF, there can only be one.
Result loadHRTF(const char *path, Size length, const char *rootPath, Size sampleRate)
There is only one voice pool and VAE and it's managed here.
void forEachVoice(const Func &&func)
Callback provided to iterate voices, needs to return a bool to indicate when a voice needs to be stop...
T min(const T &v1, const T &v2)
Definition: TMath.hpp:16
constexpr Sample MinVolume
Minimum volume before sounds will skip rendering.
Definition: vae.hpp:307
constexpr Size MaxBlock
Maximum block size.
Definition: vae.hpp:276
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
constexpr int _VAE_SPATIAL_PROCESSOR_SIZE
Contains Typedefinitions and basic structures use by the public API and internally.
Definition: vae.hpp:31
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
Result
Return Types for most engine functions.
Definition: vae.hpp:73
Bank object containing Sources, Mixers and Events Can be loaded and unloaded at runtime.
Definition: vae_bank.hpp:14
HeapBuffer< Mixer > mixers
Audio Mixers which can have effects ! is presorted !
Definition: vae_bank.hpp:16
BankHandle id
Definition: vae_bank.hpp:18
HeapBuffer< Source > sources
Audio sources defined.
Definition: vae_bank.hpp:15
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
Barebones voice.
Definition: vae_voice.hpp:17
SourceHandle source
If invalid, means voice is not playing.
Definition: vae_voice.hpp:28
bool spatialized
If the voice has spatialization data.
Definition: vae_voice.hpp:18
ListenerHandle listener
If it's spatialized it's rendered for this listener.
Definition: vae_voice.hpp:32
Sample gain
Volume of the voice.
Definition: vae_voice.hpp:33
bool audible
Whether the voice was heard by any listener.
Definition: vae_voice.hpp:21
BankHandle bank
Which bank it belongs to.
Definition: vae_voice.hpp:27
bool filtered
This will enable high/lowpass filters and variable speed playback. Gets turned on when signal does no...
Definition: vae_voice.hpp:24
MixerHandle mixer
Where the voice should mix to.
Definition: vae_voice.hpp:31
EmitterHandle emitter
Emitter used to control voice properties.
Definition: vae_voice.hpp:30
bool loop
Voice will loop until killed.
Definition: vae_voice.hpp:23
SampleIndex time
Current time in samples.
Definition: vae_voice.hpp:34
#define VAE_PROFILER_SCOPE_NAMED(name)
Profiles a scope and names it.
#define VAE_ASSERT(condition)
Definition: vae_util.hpp:11