JPL Spatial
Sound spatialization and propagation library
Loading...
Searching...
No Matches
PannerBase.h
Go to the documentation of this file.
1//
2// ██╗██████╗ ██╗ ██╗██████╗ ███████╗
3// ██║██╔══██╗ ██║ ██║██╔══██╗██╔════╝ ** JPLSpatial **
4// ██║██████╔╝ ██║ ██║██████╔╝███████╗
5// ██ ██║██╔═══╝ ██║ ██║██╔══██╗╚════██║ https://github.com/Jaytheway/JPLSpatial
6// ╚█████╔╝██║ ███████╗██║██████╔╝███████║
7// ╚════╝ ╚═╝ ╚══════╝╚═╝╚═════╝ ╚══════╝
8//
9// Copyright 2024 Jaroslav Pevno, JPLSpatial is offered under the terms of the ISC license:
10//
11// Permission to use, copy, modify, and/or distribute this software for any purpose with or
12// without fee is hereby granted, provided that the above copyright notice and this permission
13// notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
14// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
15// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
16// CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
17// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
18// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19
20#pragma once
21
22#include "JPLSpatial/Core.h"
24
26
28
41
43
45//#include "JPLSpatial/Utilities/TypeUtilities.h"
46
47#include <array>
48#include <concepts>
49#include <cmath>
50#include <functional>
51#include <optional>
52#include <vector>
53#include <limits>
54#include <map>
55#include <span>
56#include <ranges>
57#include <algorithm>
58#include <numeric>
59#include <memory>
60#include <type_traits>
61
62#define JPL_VALIDATE_VBAP_LUT 0
63
64namespace JPL
65{
66#define JPL_SCOPE_TIME(outFloat) // TODO
67
68 // TODO: maybe find a better place for this function
69 JPL_INLINE float GetSpreadFromSourceSize(float sourceSize, float distance)
70 {
71 return distance <= 0.0f
72 ? 1.0f
73 : atanf((sourceSize * 0.5f) / distance) * JPL_INV_HALF_PI;
74 }
75
77 template<CVec3 Vec3>
78 struct VBAPBaseTraits;
79
80 template<class PannerType, class Traits>
81 class VBAPannerBase;
82
83 //======================================================================
85
86 //======================================================================
89 template<CVec3 Vec3>
91 {
92 using Vec3Type = Vec3;
93
99 static constexpr auto MAX_CHANNELS = 32;
100
101 static constexpr auto MAX_SOURCE_CHANNELS = 8;
102
104
105 using ChannelGains = std::array<float, MAX_CHANNELS>;
106
107 // TODO: customizable EChannel type and ChannelMap type as well?
108 static float GetChannelAngle(EChannel channel)
109 {
111 static constexpr auto gChannelAngles
112 = std::to_array({
113 std::pair<EChannel, float>{ FrontLeft, -0.785398f },
114 std::pair<EChannel, float>{ FrontRight, 0.785398f },
115 std::pair<EChannel, float>{ FrontCenter, 0.0f },
116 std::pair<EChannel, float>{ LFE, 0.0f },
117 std::pair<EChannel, float>{ BackLeft, -2.35619f },
118 std::pair<EChannel, float>{ BackRight, 2.35619f },
119 std::pair<EChannel, float>{ FrontLeftCenter, -0.321719f },
120 std::pair<EChannel, float>{ FrontRightCenter,0.321719f },
121 std::pair<EChannel, float>{ BackCenter, 3.14159f },
122 std::pair<EChannel, float>{ SideLeft, -1.5708f },
123 std::pair<EChannel, float>{ SideRight, 1.5708f },
124
125 std::pair<EChannel, float>{ TopCenter, 3.14159f },
126 std::pair<EChannel, float>{ TopFrontLeft, -0.785398f },
127 std::pair<EChannel, float>{ TopFrontCenter, 0.0f },
128 std::pair<EChannel, float>{ TopFrontRight, 0.785398f },
129
130 std::pair<EChannel, float>{ TopSideLeft, -1.5708f },
131 std::pair<EChannel, float>{ TopSideRight, 1.5708f },
132
133 std::pair<EChannel, float>{ TopBackLeft, -2.35619f },
134 std::pair<EChannel, float>{ TopBackCenter, 3.14159f },
135 std::pair<EChannel, float>{ TopBackRight, 2.35619f },
136
137 std::pair<EChannel, float>{ WideLeft, -1.0472f },
138 std::pair<EChannel, float>{ WideRight, 1.0472f }
139 });
140
141 return std::find_if(gChannelAngles.begin(), gChannelAngles.end(),
142 [channel](const std::pair<EChannel, float>& p)
143 {
144 return p.first == channel;
145 })->second;
146 }
147
149 {
151 static const auto gChannelVectors =
152 std::to_array({
153 std::pair<EChannel, Vec3Type>{ FrontLeft, { -0.7071f, 0.0f, -0.7071f } },
154 std::pair<EChannel, Vec3Type>{ FrontRight, { 0.7071f, 0.0f, -0.7071f } },
155 std::pair<EChannel, Vec3Type>{ FrontCenter, { 0.0f, 0.0f, -1.0f } },
156 std::pair<EChannel, Vec3Type>{ LFE, { 0.0f, 0.0f, -1.0f } },
157 std::pair<EChannel, Vec3Type>{ BackLeft, { -0.7071f, 0.0f, 0.7071f } },
158 std::pair<EChannel, Vec3Type>{ BackRight, { 0.7071f, 0.0f, 0.7071f } },
159 std::pair<EChannel, Vec3Type>{ FrontLeftCenter, { -0.3162f, 0.0f, -0.9487f } },
160 std::pair<EChannel, Vec3Type>{ FrontRightCenter, { 0.3162f, 0.0f, -0.9487f } },
161 std::pair<EChannel, Vec3Type>{ BackCenter, { 0.0f, 0.0f, 1.0f } },
162 std::pair<EChannel, Vec3Type>{ SideLeft, { -1.0f, 0.0f, 0.0f } },
163 std::pair<EChannel, Vec3Type>{ SideRight, { 1.0f, 0.0f, 0.0f } },
164
165 std::pair<EChannel, Vec3Type>{ TopCenter, { 0.0f, 1.0f, 0.0f } },
166 std::pair<EChannel, Vec3Type>{ TopFrontLeft, { -0.5774f, 0.5774f, -0.5774f } },
167 std::pair<EChannel, Vec3Type>{ TopFrontCenter, { 0.0f, 0.7071f, -0.7071f } },
168 std::pair<EChannel, Vec3Type>{ TopFrontRight, { 0.5774f, 0.5774f, -0.5774f } },
169
170 std::pair<EChannel, Vec3Type>{ TopSideLeft, { -0.7071f, 0.7071f, 0.0f } },
171 std::pair<EChannel, Vec3Type>{ TopSideRight, { 0.7071f, 0.7071f, 0.0f } },
172
173 std::pair<EChannel, Vec3Type>{ TopBackLeft, { -0.5774f, 0.5774f, 0.5774f } },
174 std::pair<EChannel, Vec3Type>{ TopBackCenter, { 0.0f, 0.7071f, 0.7071f } },
175 std::pair<EChannel, Vec3Type>{ TopBackRight, { 0.5774f, 0.5774f, 0.5774f } },
176
177 std::pair<EChannel, Vec3Type>{ WideLeft, { -0.866f, 0.0f, -0.5f } },
178 std::pair<EChannel, Vec3Type>{ WideRight, { 0.866f, 0.0f, -0.5f } },
179
180 });
181
182 return std::find_if(gChannelVectors.begin(), gChannelVectors.end(),
183 [channel](const std::pair<EChannel, Vec3Type>& p)
184 {
185 return p.first == channel;
186 })->second;
187 }
188 };
189
190 //======================================================================
193 [[nodiscard]] JPL_INLINE std::optional<const char*> IsValidSourceChannelMap(ChannelMap channelMap)
194 {
195 // TODO: handle top channels, maybe we can just ignore them for now, instead of failing to initialize?
196 if (!JPL_ENSURE(channelMap.IsValid()))
197 return "Failed to initialize SourceLayout, provided invalid channel map.";
198
199 // TODO: handle this more gracefully
200 if (!JPL_ENSURE(!channelMap.HasTopChannels()))
201 return "Failed to initialize SourceLayout for VBAPPanner, provided channel map has top channels that don't participate in panning.";
202
203 return std::nullopt;
204 }
205
206 //======================================================================
225 template<class PannerType, class Traits>
227 {
228 public:
230 using Vec3Type = typename Traits::Vec3Type;
231 template<class T> using Array = std::pmr::vector<T>;
232 using ChannelGainsRef = typename Traits::ChannelGains&;
233
234 // TODO: maybe move these params outside of the template, otherwise we have different types for 2D and 3D traits
235 //======================================================================
238 {
240 float Focus; // [0, 1]
241 float Spread; // [0, 1]
242 };
243
245
251
252 //======================================================================
256 {
257 Vec3Type Direction; // Direction relative to listener
258 float Weight; // Contribution to the resulting pan image
259 };
260
272
273 //======================================================================
281 {
284
285 [[nodiscard]] JPL_INLINE bool IsInitialized() const noexcept { return !ChannelGroups.empty(); }
286 [[nodiscard]] JPL_INLINE ChannelMap GetTargetChannelMap() const noexcept { return mTargetChannelMap; }
287
288 protected:
289 [[nodiscard]] JPL_INLINE float GetDefaultVSWeight() const noexcept { return mDefaultVSWeight; }
290 JPL_INLINE void SetTargetChannelMap(ChannelMap targetChannelMap) noexcept { mTargetChannelMap = targetChannelMap; }
291
292 bool InitializeBase(ChannelMap channelMap, ChannelMap targetMap, uint32 numVirtualSourcesPerChannel);
293
294 protected:
295 ChannelMap mTargetChannelMap; // Output channel map this source layout is made for
296 float mDefaultVSWeight; // Cached virtual source default weight based on number of virtual sources per channel
297 };
298
299 //======================================================================
300 using PanType = PannerType;
301 using LUTType = typename PanType::LUTType;
302 using LUTInterface = typename PanType::LUTInterface;
303 using SourceLayoutType = typename PanType::SourceLayout;
304 //..we have to declare PanType at the bottom to give it visibility
305 // to the types defiend in VBAPannerBase
306
307 // Sanity check
308 static_assert(std::same_as<typename LUTInterface::Vec3Type, Vec3Type>);
309
310 public:
311
312 //======================================================================
315 inline bool Initialize(ChannelMap channelMap);
316
317 [[nodiscard]] JPL_INLINE bool IsLUTInitialized() const noexcept { return mLUT != nullptr; }
318
320 [[nodiscard]] JPL_INLINE const LUTType* GetLUT() const noexcept { return mLUT.get(); }
321
322 //======================================================================
324 JPL_INLINE bool InitializeSourceLayout(ChannelMap channelMap, SourceLayoutType& outLayout) const;
325
329 JPL_INLINE void GetSpeakerGains(const Vec3Type& direction, std::span<float> outGains) const;
330
337 JPL_INLINE void GetSpeakerGains(const simd& dirX, const simd& dirY, const simd& dirZ, std::span<simd> outGains) const;
338
339
341 [[nodiscard]] JPL_INLINE bool IsInitialized() const noexcept { return mChannelMap.IsValid() && IsLUTInitialized(); }
342
346 [[nodiscard]] JPL_INLINE uint32 GetNumChannels() const noexcept { return mNumChannels; } // TODO: maybe move common members to base class as well?
347
349 [[nodiscard]] JPL_INLINE ChannelMap GetChannelMap() const noexcept { return mChannelMap; }
350
353 [[nodiscard]] JPL_INLINE float GetShortestSpeakerAperture() const noexcept { return mShortestEdgeAperture; }
354
355 //======================================================================
368 template<class OnChannelGeneratedCallback = std::identity>
369 JPL_INLINE void ProcessVBAPData(const SourceLayoutType& sourceLayout,
370 const PanUpdateData& updateData,
371 std::span<float> outChannelMixMap,
372 OnChannelGeneratedCallback&& onVSsGeneratedCb = {}) const;
373
374 template<class OnChannelGeneratedCallback = std::identity>
375 JPL_INLINE void ProcessVBAPData(const SourceLayoutType& sourceLayout,
376 const PanUpdateDataWithOrientation& updateData,
377 std::span<float> outChannelMixMap,
378 OnChannelGeneratedCallback&& onVSsGeneratedCb = {}) const;
379
380 //======================================================================
385 JPL_INLINE void ProcessVirtualSources(std::span<const VirtualSource> virtualSources,
386 std::span<float> outGains) const;
387
389 static JPL_INLINE void NormalizeWeights(std::span<VirtualSource> virtualSources);
390
391 private:
392 template<class OnChannelGeneratedCallback = std::identity>
393 inline void ProcessVBAPDataImpl(const SourceLayoutType& sourceLayout,
394 const PanUpdateData& updateData,
395 const Quat<Vec3Type>& panRotation,
396 std::span<float> outChannelMixMap,
397 OnChannelGeneratedCallback&& onVSsGeneratedCb = {}) const;
398
400 template<bool bAccumulatePow>
401 inline void ProcessVirtualSourcesImpl(std::span<const VirtualSource> virtualSources,
402 std::span<float> outGains) const;
403
404 template<bool bAccumulatePow>
405 inline void ProcessVirtualSourcesImplSIMD(const Vec3SIMDBufferView& vsDirections,
406 std::span<const simd> vsWeights,
407 std::span<float> outGains) const;
408
409 static JPL_INLINE void RotateChannelCap(Vec3SIMDBufferView& virtualSources, const Basis<Vec3Type>& rotation);
410 static JPL_INLINE void SlerpChannelCap(Vec3SIMDBufferView& virtualSources, const Vec3Type& targetDirection, float t);
411 private:
413
414 ChannelMap mChannelMap; // Target channel map
415 uint32 mNumChannels = 0; // Number of channels in target channel map
416
417 // Shortest aperture between two speakers (dot product for 3D, angle for 2D panner)
418 // Used to determine number of virtual sources required to leave no gaps in the target channel layout
419 float mShortestEdgeAperture = std::numeric_limits<float>::max();
420 };
421
422
423 //==============================================================================
424 //
425 // Code beyond this point is implementation detail...
426 //
427 //==============================================================================
428
429 // This is similar to Wwise, where each vs is slerped towards
430 // forward (target pan). Which is more expansive, but produces
431 // different distribution, which may or may not sound more natural.
432#define JPL_SLERP_SPREAD 1
433
434 template<class PannerType, class Traits>
436 {
437 return outLayout.Initialize(channelMap, mChannelMap, mShortestEdgeAperture);
438 }
439
440 template<class PannerType, class Traits>
441 JPL_INLINE void VBAPannerBase<PannerType, Traits>::GetSpeakerGains(const Vec3Type& direction, std::span<float> outGains) const
442 {
443 JPL_ASSERT(mLUT != nullptr);
444 LUTInterface::Query(*mLUT).GainsFor(direction, outGains);
445 }
446
447 template<class PannerType, class Traits>
448 JPL_INLINE void VBAPannerBase<PannerType, Traits>::GetSpeakerGains(const simd& dirX, const simd& dirY, const simd& dirZ, std::span<simd> outGains) const
449 {
450 JPL_ASSERT(mLUT != nullptr);
451 LUTInterface::Query(*mLUT).GainsFor(dirX, dirY, dirZ, outGains);
452 }
453
454 template<class PannerType, class Traits>
456 {
457 if (auto error = PanType::IsValidTargetChannelMap(channelMap))
458 {
459 JPL_ERROR_TAG("VBAPannerBase", error.value());
460 return false;
461 }
462
463 // Create LUT
465
466 // Construct LUT builder
467 auto lutBuilder = LUTInterface::MakeBuilder(channelMap, *mLUT);
468
469 // Find the shortest edge to later use for SourceLayout VSs initialization
470 mShortestEdgeAperture = lutBuilder.FindShortestAperture();
471 mChannelMap = channelMap; // target channel map
472 // TODO: we may or may not want include LFE here
473 mNumChannels = channelMap.GetNumChannels();// -channelMap.HasLFE();
474
475 const bool bLUTBuildSuccessful = lutBuilder.BuildForAllDirections();
476
477#if JPL_VALIDATE_VBAP_LUT
478 lutBuilder.ValidateLUT();
479#endif
480
481 return bLUTBuildSuccessful;
482 }
483
484 template<class PannerType, class Traits>
485 template<class OnChannelGeneratedCallback>
487 const PanUpdateData& updateData,
488 std::span<float> outChannelMixMap,
489 OnChannelGeneratedCallback&& callback) const
490 {
491 JPL_ASSERT(sourceLayout.GetTargetChannelMap() == mChannelMap, "VBAP Panner can only work with VBAP source layout created for that panner.");
492
493 static const Vec3Type cWorldUp(0, 1, 0); // TODO: Listener UP
494
495 // For panning based only on emitter Direction
496 // - Direction defines sound-field/ring forward vector
497 const auto qPan = Math::QuatLookAt(updateData.SourceDirection, cWorldUp);
498
499 ProcessVBAPDataImpl(sourceLayout, updateData, qPan, outChannelMixMap, callback);
500 }
501
502 template<class PannerType, class Traits>
503 template<class OnChannelGeneratedCallback>
505 const PanUpdateDataWithOrientation& updateData,
506 std::span<float> outChannelMixMap,
507 OnChannelGeneratedCallback&& onVSsGeneratedCb) const
508 {
509 JPL_ASSERT(sourceLayout.GetTargetChannelMap() == mChannelMap, "VBAP Panner can only work with VBAP source layout created for that panner.");
510 // For panning based on Direction + Orientation
511 // - Orientation (emitter relative to listeners) defines sound-field/ring forward vector
512 // - Direction defines the target of spread-based contraction
513 const auto qPan = Math::QuatFromUpAndForward(updateData.Orientation.Up, updateData.Orientation.Forward);
514
515 ProcessVBAPDataImpl(sourceLayout, updateData.Pan, qPan, outChannelMixMap, onVSsGeneratedCb);
516 }
517
518 template<class PannerType, class Traits>
519 template<class OnChannelGeneratedCallback>
520 inline void VBAPannerBase<PannerType, Traits>::ProcessVBAPDataImpl(const SourceLayoutType& sourceLayout,
521 const PanUpdateData& updateData,
522 const Quat<Vec3Type>& panRotation,
523 std::span<float> outChannelMixMap,
524 [[maybe_unused]] OnChannelGeneratedCallback&& onVSsGeneratedCb) const
525 {
526 JPL_SCOPE_TIME(Time_ProcessVBAPDataImpl);
527
528 JPL_ASSERT(outChannelMixMap.size() == (sourceLayout.ChannelGroups.size() * GetNumChannels()),
529 "outChannelMixMap size must be equal to [num source channels] * [num target channels]");
530
531 // The gains are normalized for the number of input channels to ensure stable power
532 /*
533 - For each output channel, the square of the gains of all contributing input channels are summed together.
534 - They are then divided by the number of input channels.
535 - And then the square root of the quotient is taken.
536 */
537 /*
538 // Effectively normalization is doing this:
539
540 std::vector<float> outputChannelGains;
541
542 const float invNumInChannels = 1.0f / inputChannels.size();
543
544 // 1. Accumulate output channel gains
545 for (const auto& channelGains : inputChannels)
546 {
547 for (uint32 outChannelI = 0; outChannelI < channelGains.size(); ++outChannelI)
548 outputChannelGains[outChannelI] += channelGains[outChannelI] * channelGains[outChannelI];
549 }
550
551 // 2. Normalize resulting output gains
552 for (float& g : outputChannelGains)
553 g = Math::Sqrt(g * invNumInChannels);
554
555 // ..but instead of mixing into the output channels here,
556 // we bake in normalization into each input channel gains group,
557 // which allows the inpout channels to be mixed simply by
558 // adding into the corresponding output channels.
559 */
560
561 namespace stdr = std::ranges;
562 namespace stdv = std::views;
563
564 // Clear the output channel mix map
565 stdr::fill(outChannelMixMap, 0.0f);
566
567 // General idea is to generate new channel cap relative to nominal
568 // pan direction then rotate it by channel offset + target pan
569
570 JPL_ASSERT(updateData.Focus >= 0.0f && updateData.Focus <= 1.0f);
571 JPL_ASSERT(updateData.Spread >= 0.0f && updateData.Spread <= 1.0f);
572
573 static constexpr auto vsBufferCapacity = static_cast<uint32>(SourceLayoutType::GetMaxNumVirtualSources());
574 static constexpr uint32 vsSIMDCapacity = GetNumSIMDOps(vsBufferCapacity);
575 const auto vsBufferSize = static_cast<uint32>(sourceLayout.GetNumVirtualSources());
576 const uint32 vsSIMDSize = GetNumSIMDOps(vsBufferSize);
577
578 // Large enough static buffer for directions
580 auto vsDirCanonBuffer = vsDirBuffer.MakeView(vsSIMDSize);
581
582 {
583 JPL_SCOPE_TIME(Time_GenerateSpreadCap); // 40-34 ns
584
585 // Generate canon channel cap of virtual sources for the requrested focus
586
587 // Note: clamp focus slightly below 1, to retain some information in the two axis
588 // we need for 3D -> 2D conversion when the source is pointing directly Up
589 const float focus = updateData.Focus == 1.0f ? 0.9999f : updateData.Focus;
590 sourceLayout.GenerateSpreadCap(vsDirCanonBuffer, focus);
591 }
592
593 auto rotateChannelCap = [&panRotation](Vec3SIMDBufferView& dirs, const Quat<Vec3Type>& channelRotation)
594 {
595 // pan * yaw
596 const Basis totalRotation = (panRotation * channelRotation).ToBasis(); // yaw-after-pan (local up axis)
597 //const Basis totalRotation = (qYaw * panRotation).ToBasis(); // yaw-after-pan (local up axis)
598
599 RotateChannelCap(dirs, totalRotation);
600 };
601
602 uint32 currentVsDirOffset = 0;
603
604 {
605 JPL_SCOPE_TIME(Time_RotateVirtualSources); // 32-76 ns
606
607 // Start from second channel to avoid copying cannon buffer if we have only one channel
608 for (uint32 channelIndex = 1; channelIndex < sourceLayout.ChannelGroups.size(); ++channelIndex)
609 {
610 const auto& channelGroup = sourceLayout.ChannelGroups[channelIndex];
611 JPL_ASSERT(channelGroup.Channel == channelIndex, "Channel groups must be sorted by channel index.");
612
613 currentVsDirOffset += vsSIMDSize;
614 auto vsDirScratchBuffer = vsDirBuffer.MakeView(currentVsDirOffset, vsSIMDSize);
615
616 // 1. Copy canon cap to scratch buffer for channel-specific modifications
617 vsDirCanonBuffer.CopyTo(vsDirScratchBuffer);
618
619 // 2. Rotate channel cap towards pan rotation + channel offset
620 rotateChannelCap(vsDirScratchBuffer, channelGroup.Rotation);
621 }
622
623 // 2. The first source channel can use the initial channel cap generated,
624 // after it has been reused by all other channels
625 {
626 const auto& channelGroup = sourceLayout.ChannelGroups[0];
627 JPL_ASSERT(channelGroup.Channel == 0, "Channel groups must be sorted by channel index.");
628
629 rotateChannelCap(vsDirCanonBuffer, channelGroup.Rotation);
630 }
631 }
632
633 const uint32 numVsUsed = currentVsDirOffset + vsSIMDSize;
634 {
635 JPL_SCOPE_TIME(Time_SlerpVirtualSources); // 192-252 ns
636
637 auto allUsedVss = vsDirBuffer.MakeView(numVsUsed);
638
639 // 3. Move virtual sources towards pan direction based on spread
640 // (this is most expansive operation, requiring slerping each vs individually)
641
642 // Note: clamp spread slightly above 0, to retain some information in the two axis
643 // we need for 3D -> 2D conversion when the source is pointing directly Up
644 const float spread = updateData.Spread == 0.0f ? 0.0001f : updateData.Spread;
645
646 SlerpChannelCap(allUsedVss, updateData.SourceDirection, 1.0f - spread);
647
648 // Mainly for debugging and testing
649 if constexpr (!std::same_as<std::remove_cvref_t<OnChannelGeneratedCallback>, std::identity>)
650 {
651 StaticArray<Vec3Type, vsBufferCapacity> vsDirections(vsBufferSize);
652
653 for (uint32 vsDirOffset = 0, channelIndex = 0; vsDirOffset < numVsUsed; vsDirOffset += vsSIMDSize, ++channelIndex)
654 {
655 auto vsChannelDirs = vsDirBuffer.MakeView(vsDirOffset, vsSIMDSize);
656 vsChannelDirs.Unpack(std::span(vsDirections));
657
658 (void)onVSsGeneratedCb(vsDirections, channelIndex);
659 }
660 }
661 }
662
663 // Buffer to mix in source channels and calculate the normalization coefficients
664 StaticArray<float, Traits::MAX_CHANNELS> mixBuffer(mNumChannels, 0.0f);
665
666 // Initialize buffer with virtual source weights,
667 // wich is constant for panning SourceLayoutType
668 {
669 JPL_SCOPE_TIME(Time_ProcessVirtualSourcesImpl); // 148-230 ns
670
671 StaticArray<simd, vsSIMDCapacity> weights(vsSIMDSize, simd(sourceLayout.GetDefaultVSWeight()));
672
673 // Compute new gains for each source channel
674 for (uint32 vsDirOffset = 0, mixMapOffset = 0; vsDirOffset < numVsUsed; vsDirOffset += vsSIMDSize, mixMapOffset += mNumChannels)
675 {
676 auto vsChannelDirs = vsDirBuffer.MakeView(vsDirOffset, vsSIMDSize);
677 auto gains = outChannelMixMap.subspan(mixMapOffset, mNumChannels);
678
679 static constexpr bool bAccumulatePow = true;
680 ProcessVirtualSourcesImplSIMD<bAccumulatePow>(vsChannelDirs, weights, gains);
681
682 // Accumulate output channel gain squares
683 for (uint32 i = 0; i < mixBuffer.size(); ++i)
684 {
685 mixBuffer[i] += gains[i];
686 }
687 }
688 }
689
690 // Normalize for the number of input channels
691 {
692 JPL_SCOPE_TIME(Time_NormalizeGains); // 32-45 ns
693
694 StaticArray<float, Traits::MAX_CHANNELS> normCoeffs(mNumChannels);
695
696 const auto numInChannels = static_cast<float>(sourceLayout.ChannelGroups.size());
697
698 // Calculate normalization coefficients per output channel
699 for (uint32 i = 0; i < mixBuffer.size(); ++i)
700 {
701 const float g = mixBuffer[i];
702 normCoeffs[i] = g > 0.0f ? Math::InvSqrt(g * numInChannels) : 0.0f;
703 }
704
705 // Pre-normalize source out channel gains
706 for (uint32 channelOffset = 0; channelOffset < outChannelMixMap.size(); channelOffset += mNumChannels)
707 {
708 auto gains = outChannelMixMap.subspan(channelOffset, mNumChannels);
709 for (uint32 i = 0; i < gains.size(); ++i)
710 {
711 gains[i] *= normCoeffs[i];
712 }
713 }
714
715 // ..now the input channels can be mixed into the output channels with simple add
716
717 // This is equivalent to the method above, only multiplying once per output channel during mixing.
718 //
719 // We could instead of backing in normalization into source channel gains groups,
720 // give the user the output gains coefficients to multiply when mixing.
721 /*for (uint32 i = 0; i < mixBuffer.size(); ++i)
722 {
723 float& g = mixBuffer[i];
724 float& c = normCoeffs[i];
725 g *= c;
726 }*/
727 }
728 }
729
730 template<class PannerType, class Traits>
731 JPL_INLINE void VBAPannerBase<PannerType, Traits>::ProcessVirtualSources(std::span<const VirtualSource> virtualSources,
732 std::span<float> outGains) const
733 {
734 JPL_ASSERT(outGains.size() <= GetNumChannels());
735
736 static constexpr bool bAccumulatePow = false;
737 ProcessVirtualSourcesImpl<bAccumulatePow>(virtualSources, outGains);
738
739 // Public ProcessVirtualSources is for the end use in mixing,
740 // so has to be normalized.
741 Algo::NormalizeL2(outGains);
742 }
743
744 template<class PannerType, class Traits>
745 template<bool bAccumulatePow>
746 inline void VBAPannerBase<PannerType, Traits>::ProcessVirtualSourcesImpl(std::span<const VirtualSource> virtualSources,
747 std::span<float> outGains) const
748 {
749 JPL_ASSERT(outGains.size() <= GetNumChannels());
750
751 using QueryType = typename LUTInterface::QueryType;
752
753 if constexpr (requires{ typename QueryType::VBAPCell; })
754 {
755 // Note: this cannot be parallel for, because the writes to `outGains` can overlap
756 std::ranges::for_each(virtualSources, [&](const VirtualSource& vs)
757 {
758 // Retrieve gains from the LUT
759 typename QueryType::VBAPCell cell;
760
761 LUTInterface::Query(*mLUT).GainsFor(vs.Direction, cell);
762
763 if constexpr (bAccumulatePow)
764 {
765 // Accumulate linear gains
766 outGains[cell.Speakers[0]] += vs.Weight * cell.Gains[0] * cell.Gains[0];
767 outGains[cell.Speakers[1]] += vs.Weight * cell.Gains[1] * cell.Gains[1];
768 outGains[cell.Speakers[2]] += vs.Weight * cell.Gains[2] * cell.Gains[2];
769 }
770 else
771 {
772 // Accumulate weighted linear gains (to be normalized later)
773 outGains[cell.Speakers[0]] += vs.Weight * cell.Gains[0];
774 outGains[cell.Speakers[1]] += vs.Weight * cell.Gains[1];
775 outGains[cell.Speakers[2]] += vs.Weight * cell.Gains[2];
776 }
777 });
778 }
779 else
780 {
781 float buffer[Traits::MAX_CHANNELS]{ float(0.0) };
782 std::span<float> vsSpeakerGains(buffer, outGains.size());
783
784 // Note: this cannot be parallel for, because the writes to `outGains` can overlap
785 std::ranges::for_each(virtualSources, [&](const VirtualSource& vs)
786 {
787 // Retrieve gains from the LUT
788 GetSpeakerGains(vs.Direction, vsSpeakerGains);
789
790 // Accumulate weighted linear gains (to be normalized later)
791 for (uint32 s = 0; s < outGains.size(); ++s)
792 {
793 if constexpr (bAccumulatePow)
794 {
795 outGains[s] += vs.Weight * vsSpeakerGains[s] * vsSpeakerGains[s];
796 }
797 else
798 {
799 outGains[s] += vs.Weight * vsSpeakerGains[s];
800 }
801 }
802 });
803 }
804 }
805
806 template<class PannerType, class Traits>
807 template<bool bAccumulatePow>
808 inline void VBAPannerBase<PannerType, Traits>::ProcessVirtualSourcesImplSIMD(const Vec3SIMDBufferView& vsDirections,
809 std::span<const simd> vsWeights,
810 std::span<float> outGains) const
811 {
812 JPL_ASSERT(vsDirections.size() == vsWeights.size());
813 JPL_ASSERT(outGains.size() <= GetNumChannels());
814
815 using QueryType = typename LUTInterface::QueryType;
816
817 if constexpr (requires{ typename QueryType::VBAPCell; }) // 3D panning (with height channels)
818 {
819 for (uint32 i = 0; i < vsDirections.size(); ++i)
820 {
821 // Retrieve gains from the LUT
822 std::array<uint8, simd::size() * 3> speakers;
823 std::array<float, simd::size() * 3> gains;
824
825 LUTInterface::Query(*mLUT).GainsFor(vsDirections.X[i], vsDirections.Y[i], vsDirections.Z[i], speakers, gains);
826
827 const simd& vsWeight = vsWeights[i];
828
829 for (uint32 s = 0; s < gains.size(); s += simd::size())
830 {
831 float* gp = &gains[s];
832 simd gs(gp);
833
834 if constexpr (bAccumulatePow)
835 {
836 // Accumulate linear gains
837 (vsWeight * gs * gs).store(gp);
838 }
839 else
840 {
841 // Accumulate weighted linear gains (to be normalized later)
842 (vsWeight * gs).store(gp);
843 }
844 }
845
846 for (uint32 s = 0; s < gains.size(); ++s)
847 {
848 outGains[speakers[s]] += gains[s];
849 }
850 }
851 }
852 else // 2D panning (without height channels)
853 {
854 StaticArray<simd, Traits::MAX_CHANNELS> vsSpeakerGains(outGains.size());
855
856 for (uint32 i = 0; i < vsDirections.size(); ++i)
857 {
858 GetSpeakerGains(vsDirections.X[i], vsDirections.Y[i], vsDirections.Z[i], vsSpeakerGains);
859
860 const simd& vsWeight = vsWeights[i];
861
862 // Accumulate weighted linear gains (to be normalized later)
863 for (uint32 s = 0; s < outGains.size(); ++s)
864 {
865 if constexpr (bAccumulatePow)
866 {
867 outGains[s] += (vsWeight * vsSpeakerGains[s] * vsSpeakerGains[s]).reduce();
868 }
869 else
870 {
871 outGains[s] += (vsWeight * vsSpeakerGains[s]).reduce();
872 }
873 }
874 }
875 }
876 }
877
878 template<class PannerType, class Traits>
879 JPL_INLINE void VBAPannerBase<PannerType, Traits>::NormalizeWeights(std::span<VirtualSource> virtualSources)
880 {
881 const float sum = Accumulate(virtualSources, 0.0f, [](float acc, const VirtualSource& vs)
882 {
883 return acc += Math::Abs(vs.Weight);
884 });
885
886 const float invSum = sum == 0.0f ? 0.0f : 1.0f / sum;
887
888 std::ranges::for_each(virtualSources, [invSum](VirtualSource& vs)
889 {
890 vs.Weight *= invSum;
891 });
892 }
893
894 template<class PannerType, class Traits>
895 JPL_INLINE void VBAPannerBase<PannerType, Traits>::RotateChannelCap(Vec3SIMDBufferView& virtualSources, const Basis<Vec3Type>& rotation)
896 {
897 for (uint32 i = 0; i < virtualSources.size(); ++i)
898 {
899 rotation.Transform(virtualSources.X[i], virtualSources.Y[i], virtualSources.Z[i]);
900 }
901 }
902
903 template<class PannerType, class Traits>
904 JPL_INLINE void VBAPannerBase<PannerType, Traits>::SlerpChannelCap(Vec3SIMDBufferView& virtualSources, const Vec3Type& targetDirection, float t)
905 {
906 const Vec3Pack target(targetDirection);
907
908 for (uint32 i = 0; i < virtualSources.size(); ++i)
909 {
910 // This copy doesn't mater for performance,
911 // if we use something like a reference wrapper
912 // the resulting performance is no different
913 Vec3Pack inOutVs(virtualSources.X[i], virtualSources.Y[i], virtualSources.Z[i]);
914 Math::Slerp(inOutVs, target, t);
915 virtualSources.X[i] = inOutVs.X;
916 virtualSources.Y[i] = inOutVs.Y;
917 virtualSources.Z[i] = inOutVs.Z;
918 }
919 }
920
921 template<class PannerType, class Traits>
922 inline bool VBAPannerBase<PannerType, Traits>::VBAPLayoutBase::InitializeBase(ChannelMap channelMap, ChannelMap targetMap, uint32 numVirtualSourcesPerChannel)
923 {
924#if 0 // !defined(JPL_TEST)
925 if (channelMap.HasLFE())
926 {
927 JPL_INFO_TAG("VBAP Panner", "Requested source channel map with LFE, LFE is going to be skipped in the resulting channel mix map "
928 "(e.g. 5.1 source channel set going to have 5 channels, not 6).");
929 }
930#endif
931
932 mTargetChannelMap = targetMap;
933
934 // Cache normalization factors for later
935 mDefaultVSWeight = 1.0f / numVirtualSourcesPerChannel;
936
937 const uint32 numChannels = channelMap.GetNumChannels() - channelMap.HasLFE();
938
939 // Source channel map is unsorted, we need to sort it
940 // to make our lives easier
942 VBAP::ChannelAngle::GetSortedChannelAngles(channelMap, sourceChannelsSorted, &Traits::GetChannelAngle);
943
944 // Width of a singe source channel in radians
945 const float channelWidth = JPL_TWO_PI / static_cast<float>(numChannels);
946
947 // If we don't have center channel, we need to offset channel groups
948 const float channelAngleOffset = !channelMap.Has(EChannel::FrontCenter)
949 ? 0.5f * channelWidth
950 : 0.0f;
951
952 // Create channel groups for source channels
953 ChannelGroups.clear();
954 ChannelGroups.resize(numChannels);
955
956 static const Vec3Type cUP(0.0f, 1.0f, 0.0f);
957
958 // 1. Find equal source channel positions for 100% spread
959 // 2. Lay out virtual sources for source channel group evenly withing its equal section
960 for (uint32 i = 0; i < sourceChannelsSorted.size(); ++i)
961 {
962 // Assign a centre angle of the next equal section of the source plane
963 const float channelAngle = channelAngleOffset + channelWidth * i;
964
965 // Create channel group for each source channel
966 ChannelGroup& channelGroup = ChannelGroups[i];
967 channelGroup.Rotation = Math::QuatRotation(cUP, -channelAngle); // right-handed rotation
968 channelGroup.Channel = sourceChannelsSorted[i].ChannelId;
969
970 /*
971 To handle LFE:
972 - channelGroup.Channel - is the index of the source channel
973 - in the audio process block, instead of iterating actual inputs of thes block, iterate VBAP channel groups
974 - if LFE pressent, simply copy input to output, applying other parameters, like distance attenuation, but skipping VBAP channel group
975
976 There's no need to access channel group by ID when sending audio to the output, since we coppy each group to each output channel
977 */
978 }
979
980 // Now that we have assigned the angels, we need to sort
981 // by the usual order of the channels in an audio block to make our lives easier later
982 std::ranges::sort(ChannelGroups, [](const ChannelGroup& lhs, const ChannelGroup& rhs) { return lhs.Channel < rhs.Channel; });
983
984 return true;
985 }
986
987} // namespace JPL
988
989#undef JPL_SCOPE_TIME
#define JPL_ASSERT(inExpression,...)
Main assert macro, usage: JPL_ASSERT(condition, message) or JPL_ASSERT(condition)
Definition ErrorReporting.h:80
#define JPL_ENSURE(inExpression,...)
Define ENSURE.
Definition ErrorReporting.h:94
#define JPL_ERROR_TAG(tag, message)
Definition ErrorReporting.h:128
#define JPL_INFO_TAG(tag, message)
Definition ErrorReporting.h:112
#define JPL_SCOPE_TIME(outFloat)
Definition PannerBase.h:66
Definition ChannelMap.h:154
constexpr uint32 GetNumChannels() const noexcept
Definition ChannelMap.h:166
constexpr bool IsValid() const noexcept
Definition ChannelMap.h:164
constexpr bool HasTopChannels() const noexcept
Definition ChannelMap.h:163
constexpr bool HasLFE() const noexcept
Definition ChannelMap.h:162
constexpr bool Has(EChannel channel) const noexcept
Definition ChannelMap.h:161
Definition PannerBase.h:227
JPL_INLINE bool InitializeSourceLayout(ChannelMap channelMap, SourceLayoutType &outLayout) const
Create/initialize SourceLayout for given source channelMap
Definition PannerBase.h:435
typename PanType::LUTInterface LUTInterface
Definition PannerBase.h:302
JPL_INLINE void ProcessVirtualSources(std::span< const VirtualSource > virtualSources, std::span< float > outGains) const
Definition PannerBase.h:731
typename Traits::Vec3Type Vec3Type
Aliases to avoid typing wordy templates.
Definition PannerBase.h:230
std::pmr::vector< T > Array
Definition PannerBase.h:231
JPL_INLINE void GetSpeakerGains(const Vec3Type &direction, std::span< float > outGains) const
Definition PannerBase.h:441
typename PanType::LUTType LUTType
Definition PannerBase.h:301
JPL_INLINE void ProcessVBAPData(const SourceLayoutType &sourceLayout, const PanUpdateData &updateData, std::span< float > outChannelMixMap, OnChannelGeneratedCallback &&onVSsGeneratedCb={}) const
Definition PannerBase.h:486
JPL_INLINE bool IsLUTInitialized() const noexcept
Definition PannerBase.h:317
typename PanType::SourceLayout SourceLayoutType
Definition PannerBase.h:303
JPL_INLINE const LUTType * GetLUT() const noexcept
Get the look-up table. Can be nullptr, if hasn't been initialized yet.
Definition PannerBase.h:320
PannerType PanType
Definition PannerBase.h:300
JPL_INLINE uint32 GetNumChannels() const noexcept
Definition PannerBase.h:346
typename Traits::ChannelGains & ChannelGainsRef
Definition PannerBase.h:232
static JPL_INLINE void NormalizeWeights(std::span< VirtualSource > virtualSources)
Normalize weights of the virtual sources to ensure consistent energy.
Definition PannerBase.h:879
JPL_INLINE float GetShortestSpeakerAperture() const noexcept
Definition PannerBase.h:353
JPL_INLINE ChannelMap GetChannelMap() const noexcept
Get the channel map the panner is initialized to.
Definition PannerBase.h:349
bool Initialize(ChannelMap channelMap)
Definition PannerBase.h:455
JPL_INLINE bool IsInitialized() const noexcept
Definition PannerBase.h:341
JPL_INLINE constexpr void NormalizeL2(ContainerType &&data)
Apply unit vector scaling, so that the magnitude of the vector = 1.
Definition Algorithm.h:98
JPL_INLINE constexpr T InvSqrt(T x) noexcept
Definition Math.h:283
JPL_INLINE constexpr auto Abs(const T &value) noexcept
Standard abs is not constexpr in C++20.
Definition Math.h:87
Vec3 Slerp(const Vec3 &v0, const Vec3 &v1, float t) noexcept
Input vectors must be normalized.
Definition Vec3Math.h:142
Definition AcousticMaterial.h:36
JPL_INLINE constexpr auto GetNumSIMDOps(std::unsigned_integral auto count) noexcept
Get number of SIMD operations that can fit into the count
Definition SIMDMath.h:51
std::uint32_t uint32
Definition Core.h:311
std::unique_ptr< T, PmrDeleter< T > > pmr_unique_ptr
Alias for unique ptr using pmr allocator and deleter.
Definition Memory.h:233
std::uint8_t uint8
Definition Core.h:309
Vec3BufferView< simd > Vec3SIMDBufferView
View into a Vec3-like SoA buffer, holding separate arrays of Vec3 components X, Y,...
Definition Vec3Buffer.h:56
JPL_INLINE std::optional< const char * > IsValidSourceChannelMap(ChannelMap channelMap)
Definition PannerBase.h:193
std::pmr::memory_resource * GetDefaultMemoryResource() noexcept
Definition Memory.h:42
EChannel
Definition ChannelMap.h:39
@ TopBackLeft
Definition ChannelMap.h:67
@ TopFrontRight
Definition ChannelMap.h:61
@ BackCenter
Definition ChannelMap.h:53
@ TopSideRight
Definition ChannelMap.h:65
@ TopFrontCenter
Definition ChannelMap.h:60
@ TopSideLeft
Definition ChannelMap.h:64
@ FrontLeftCenter
Definition ChannelMap.h:48
@ TopBackCenter
Definition ChannelMap.h:68
@ SideRight
Definition ChannelMap.h:46
@ SideLeft
Definition ChannelMap.h:45
@ FrontCenter
Definition ChannelMap.h:42
@ WideRight
Definition ChannelMap.h:56
@ BackRight
Definition ChannelMap.h:52
@ TopBackRight
Definition ChannelMap.h:69
@ BackLeft
Definition ChannelMap.h:51
@ FrontRightCenter
Definition ChannelMap.h:49
@ TopCenter
Definition ChannelMap.h:58
@ LFE
Definition ChannelMap.h:43
@ FrontLeft
Definition ChannelMap.h:40
@ WideLeft
Definition ChannelMap.h:55
@ TopFrontLeft
Definition ChannelMap.h:59
@ FrontRight
Definition ChannelMap.h:41
JPL_INLINE float GetSpreadFromSourceSize(float sourceSize, float distance)
Definition PannerBase.h:69
JPL_INLINE pmr_unique_ptr< T > make_pmr_unique(Args &&... args)
std::make_unique wrapper using our global memory resource
Definition Memory.h:237
Orthonormal basis (column-major)
Definition MinimalBasis.h:35
JPL_INLINE Vec3 Transform(const Vec3 &vector) const noexcept
Apply rotation local -> world.
Definition MinimalBasis.h:109
Minimum data required to do our orientation math.
Definition Position.h:32
Vec3Type Up
Definition Position.h:37
Vec3Type Forward
Definition Position.h:38
Minimal quaternion (w + xi + yj + zk)
Definition MinimalQuat.h:38
Forward declaration.
Definition PannerBase.h:91
static constexpr auto MAX_CHANNELS
Definition PannerBase.h:99
static constexpr auto MAX_CHANNEL_MIX_MAP_SIZE
Definition PannerBase.h:103
Vec3 Vec3Type
Definition PannerBase.h:92
static constexpr auto MAX_SOURCE_CHANNELS
Definition PannerBase.h:101
static Vec3Type GetChannelVector(EChannel channel)
Definition PannerBase.h:148
static float GetChannelAngle(EChannel channel)
Definition PannerBase.h:108
std::array< float, MAX_CHANNELS > ChannelGains
Definition PannerBase.h:105
static void GetSortedChannelAngles(ChannelMap channelMap, ArrayType< ChannelAngle, Args... > &sortedChannelAngles, std::function< float(EChannel)> getChannelAngle, bool skipLFE=true)
Get channel angles from ChannelMap, normalize to [0, Pi] and sort in assending order.
Definition VBAPEx.h:258
Definition PannerBase.h:265
Quat< Vec3Type > Rotation
Channel group rotation offset as per the channel this group is associated to.
Definition PannerBase.h:267
uint32 Channel
Index or Id of the channel this group is associated to.
Definition PannerBase.h:270
PanSourceOrientation Orientation
Definition PannerBase.h:249
PanUpdateData Pan
Definition PannerBase.h:248
Parameters to update VBAP data for a source.
Definition PannerBase.h:238
Vec3Type SourceDirection
Definition PannerBase.h:239
float Spread
Definition PannerBase.h:241
float Focus
Definition PannerBase.h:240
Definition PannerBase.h:281
float mDefaultVSWeight
Definition PannerBase.h:296
Array< ChannelGroup > ChannelGroups
Groups of virtual sources associated with source channels.
Definition PannerBase.h:283
JPL_INLINE float GetDefaultVSWeight() const noexcept
Definition PannerBase.h:289
ChannelMap mTargetChannelMap
Definition PannerBase.h:295
JPL_INLINE ChannelMap GetTargetChannelMap() const noexcept
Definition PannerBase.h:286
JPL_INLINE void SetTargetChannelMap(ChannelMap targetChannelMap) noexcept
Definition PannerBase.h:290
bool InitializeBase(ChannelMap channelMap, ChannelMap targetMap, uint32 numVirtualSourcesPerChannel)
Definition PannerBase.h:922
JPL_INLINE bool IsInitialized() const noexcept
Definition PannerBase.h:285
Definition PannerBase.h:256
float Weight
Definition PannerBase.h:258
Vec3Type Direction
Definition PannerBase.h:257
Definition Vec3Buffer.h:89
T * Z
Definition Vec3Buffer.h:92
JPL_INLINE std::size_t size() const noexcept
Definition Vec3Buffer.h:95
T * Y
Definition Vec3Buffer.h:91
T * X
Definition Vec3Buffer.h:90
Definition Vec3Buffer.h:66
JPL_INLINE Vec3BufferView< T > MakeView(std::size_t count) noexcept
Definition Vec3Buffer.h:71
Minimal 4-wide 32-bit float vector implementation for SIMD.
Definition SIMD.h:60
static constexpr std::size_t size() noexcept
Get number of element of the vector.
Definition SIMD.h:97