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)
67#define JPL_SCOPE_TIME(outFloat) // TODO
68 // TODO: maybe find a better place for this function
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?
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 //======================================================================
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
287
288 protected:
291
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 //======================================================================
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 //======================================================================
325
328 JPL_INLINE void GetSpeakerGains(const Vec3Type& direction, std::span<float> outGains) const;
329
332 JPL_INLINE void GetSpeakerGains(const simd& dirX, const simd& dirY, const simd& dirZ, std::span<simd> outGains) const;
333
334
336 [[nodiscard]] JPL_INLINE bool IsInitialized() const noexcept { return mChannelMap.IsValid() && IsLUTInitialized(); }
337
341 [[nodiscard]] JPL_INLINE uint32 GetNumChannels() const noexcept { return mNumChannels; } // TODO: maybe move common members to base class as well?
342
345
348 [[nodiscard]] JPL_INLINE float GetShortestSpeakerAperture() const noexcept { return mShortestEdgeAperture; }
349
350 //======================================================================
360 template<class OnChannelGeneratedCallback = std::identity>
362 const PanUpdateData& updateData,
363 std::span<float> outChannelMixMap,
365
366 template<class OnChannelGeneratedCallback = std::identity>
368 const PanUpdateDataWithOrientation& updateData,
369 std::span<float> outChannelMixMap,
371
372 //======================================================================
375 JPL_INLINE void ProcessVirtualSources(std::span<const VirtualSource> virtualSources,
376 std::span<float> outGains) const;
377
379 static JPL_INLINE void NormalizeWeights(std::span<VirtualSource> virtualSources);
380
381 private:
382 template<class OnChannelGeneratedCallback = std::identity>
383 inline void ProcessVBAPDataImpl(const SourceLayoutType& sourceLayout,
384 const PanUpdateData& updateData,
385 const Quat<Vec3Type>& panRotation,
386 std::span<float> outChannelMixMap,
388
390 template<bool bAccumulatePow>
391 inline void ProcessVirtualSourcesImpl(std::span<const VirtualSource> virtualSources,
392 std::span<float> outGains) const;
393
394 template<bool bAccumulatePow>
395 inline void ProcessVirtualSourcesImplSIMD(const Vec3SIMDBufferView& vsDirections,
396 std::span<const simd> vsWeights,
397 std::span<float> outGains) const;
398
399 static JPL_INLINE void RotateChannelCap(Vec3SIMDBufferView& virtualSources, const Basis<Vec3Type>& rotation);
400 static JPL_INLINE void SlerpChannelCap(Vec3SIMDBufferView& virtualSources, const Vec3Type& targetDirection, float t);
401 private:
403
404 ChannelMap mChannelMap; // Target channel map
405 uint32 mNumChannels = 0; // Number of channels in target channel map
406
407 // Shortest aperture between two speakers (dot product for 3D, angle for 2D panner)
408 // Used to determine number of virtual sources required to leave no gaps in the target channel layout
409 float mShortestEdgeAperture = std::numeric_limits<float>::max();
410 };
411
412
413 //==============================================================================
414 //
415 // Code beyond this point is implementation detail...
416 //
417 //==============================================================================
418
419 // This is similar to Wwise, where each vs is slerped towards
420 // forward (target pan). Which is more expansive, but produces
421 // different distribution, which may or may not sound more natural.
422#define JPL_SLERP_SPREAD 1
423
424 template<class PannerType, class Traits>
426 {
427 return outLayout.Initialize(channelMap, mChannelMap, mShortestEdgeAperture);
428 }
429
430 template<class PannerType, class Traits>
432 {
433 JPL_ASSERT(mLUT != nullptr);
434 LUTInterface::Query(*mLUT).GainsFor(direction, outGains);
435 }
436
437 template<class PannerType, class Traits>
439 {
440 JPL_ASSERT(mLUT != nullptr);
441 LUTInterface::Query(*mLUT).GainsFor(dirX, dirY, dirZ, outGains);
442 }
443
444 template<class PannerType, class Traits>
446 {
447 if (auto error = PanType::IsValidTargetChannelMap(channelMap))
448 {
449 JPL_ERROR_TAG("VBAPannerBase", error.value());
450 return false;
451 }
452
453 // Create LUT
455
456 // Construct LUT builder
457 auto lutBuilder = LUTInterface::MakeBuilder(channelMap, *mLUT);
458
459 // Find the shortest edge to later use for SourceLayout VSs initialization
460 mShortestEdgeAperture = lutBuilder.FindShortestAperture();
461 mChannelMap = channelMap; // target channel map
462 // TODO: we may or may not want include LFE here
463 mNumChannels = channelMap.GetNumChannels();// -channelMap.HasLFE();
464
465 const bool bLUTBuildSuccessful = lutBuilder.BuildForAllDirections();
466
467#if JPL_VALIDATE_VBAP_LUT
468 lutBuilder.ValidateLUT();
469#endif
470
471 return bLUTBuildSuccessful;
472 }
473
474 template<class PannerType, class Traits>
475 template<class OnChannelGeneratedCallback>
478 std::span<float> outChannelMixMap,
480 {
481 JPL_ASSERT(sourceLayout.GetTargetChannelMap() == mChannelMap, "VBAP Panner can only work with VBAP source layout created for that panner.");
482
483 static const Vec3Type cWorldUp(0, 1, 0); // TODO: Listener UP
484
485 // For panning based only on emitter Direction
486 // - Direction defines sound-field/ring forward vector
487 const auto qPan = Math::QuatLookAt(updateData.SourceDirection, cWorldUp);
488
489 ProcessVBAPDataImpl(sourceLayout, updateData, qPan, outChannelMixMap, callback);
490 }
491
492 template<class PannerType, class Traits>
493 template<class OnChannelGeneratedCallback>
496 std::span<float> outChannelMixMap,
498 {
499 JPL_ASSERT(sourceLayout.GetTargetChannelMap() == mChannelMap, "VBAP Panner can only work with VBAP source layout created for that panner.");
500 // For panning based on Direction + Orientation
501 // - Orientation (emitter relative to listeners) defines sound-field/ring forward vector
502 // - Direction defines the target of spread-based contraction
503 const auto qPan = Math::QuatFromUpAndForward(updateData.Orientation.Up, updateData.Orientation.Forward);
504
505 ProcessVBAPDataImpl(sourceLayout, updateData.Pan, qPan, outChannelMixMap, onVSsGeneratedCb);
506 }
507
508 template<class PannerType, class Traits>
509 template<class OnChannelGeneratedCallback>
511 const PanUpdateData& updateData,
513 std::span<float> outChannelMixMap,
515 {
517
518 JPL_ASSERT(outChannelMixMap.size() == (sourceLayout.ChannelGroups.size() * GetNumChannels()),
519 "outChannelMixMap size must be equal to [num source channels] * [num target channels]");
520
521 // The gains are normalized for the number of input channels to ensure stable power
522 /*
523 - For each output channel, the square of the gains of all contributing input channels are summed together.
524 - They are then divided by the number of input channels.
525 - And then the square root of the quotient is taken.
526 */
527 /*
528 // Effectively normalization is doing this:
529
530 std::vector<float> outputChannelGains;
531
532 const float invNumInChannels = 1.0f / inputChannels.size();
533
534 // 1. Accumulate output channel gains
535 for (const auto& channelGains : inputChannels)
536 {
537 for (uint32 outChannelI = 0; outChannelI < channelGains.size(); ++outChannelI)
538 outputChannelGains[outChannelI] += channelGains[outChannelI] * channelGains[outChannelI];
539 }
540
541 // 2. Normalize resulting output gains
542 for (float& g : outputChannelGains)
543 g = Math::Sqrt(g * invNumInChannels);
544
545 // ..but instead of mixing into the output channels here,
546 // we bake in normalization into each input channel gains group,
547 // which allows the inpout channels to be mixed simply by
548 // adding into the corresponding output channels.
549 */
550
551 namespace stdr = std::ranges;
552 namespace stdv = std::views;
553
554 // Clear the output channel mix map
555 stdr::fill(outChannelMixMap, 0.0f);
556
557 // General idea is to generate new channel cap relative to nominal
558 // pan direction then rotate it by channel offset + target pan
559
560 JPL_ASSERT(updateData.Focus >= 0.0f && updateData.Focus <= 1.0f);
561 JPL_ASSERT(updateData.Spread >= 0.0f && updateData.Spread <= 1.0f);
562
563 static constexpr auto vsBufferCapacity = static_cast<uint32>(SourceLayoutType::GetMaxNumVirtualSources());
565 const auto vsBufferSize = static_cast<uint32>(sourceLayout.GetNumVirtualSources());
567
568 // Large enough static buffer for directions
570 auto vsDirCanonBuffer = vsDirBuffer.MakeView(vsSIMDSize);
571
572 {
574
575 // Generate canon channel cap of virtual sources for the requrested focus
576
577 // Note: clamp focus slightly below 1, to retain some information in the two axis
578 // we need for 3D -> 2D conversion when the source is pointing directly Up
579 const float focus = updateData.Focus == 1.0f ? 0.9999f : updateData.Focus;
580 sourceLayout.GenerateSpreadCap(vsDirCanonBuffer, focus);
581 }
582
583 auto rotateChannelCap = [&panRotation](Vec3SIMDBufferView& dirs, const Quat<Vec3Type>& channelRotation)
584 {
585 // pan * yaw
586 const Basis totalRotation = (panRotation * channelRotation).ToBasis(); // yaw-after-pan (local up axis)
587 //const Basis totalRotation = (qYaw * panRotation).ToBasis(); // yaw-after-pan (local up axis)
588
589 RotateChannelCap(dirs, totalRotation);
590 };
591
593
594 {
596
597 // Start from second channel to avoid copying cannon buffer if we have only one channel
598 for (uint32 channelIndex = 1; channelIndex < sourceLayout.ChannelGroups.size(); ++channelIndex)
599 {
600 const auto& channelGroup = sourceLayout.ChannelGroups[channelIndex];
601 JPL_ASSERT(channelGroup.Channel == channelIndex, "Channel groups must be sorted by channel index.");
602
605
606 // 1. Copy canon cap to scratch buffer for channel-specific modifications
608
609 // 2. Rotate channel cap towards pan rotation + channel offset
611 }
612
613 // 2. The first source channel can use the initial channel cap generated,
614 // after it has been reused by all other channels
615 {
616 const auto& channelGroup = sourceLayout.ChannelGroups[0];
617 JPL_ASSERT(channelGroup.Channel == 0, "Channel groups must be sorted by channel index.");
618
620 }
621 }
622
624 {
626
627 auto allUsedVss = vsDirBuffer.MakeView(numVsUsed);
628
629 // 3. Move virtual sources towards pan direction based on spread
630 // (this is most expansive operation, requiring slerping each vs individually)
631
632 // Note: clamp spread slightly above 0, to retain some information in the two axis
633 // we need for 3D -> 2D conversion when the source is pointing directly Up
634 const float spread = updateData.Spread == 0.0f ? 0.0001f : updateData.Spread;
635
636 SlerpChannelCap(allUsedVss, updateData.SourceDirection, 1.0f - spread);
637
638 // Mainly for debugging and testing
639 if constexpr (!std::same_as<std::remove_cvref_t<OnChannelGeneratedCallback>, std::identity>)
640 {
642
644 {
646 vsChannelDirs.Unpack(std::span(vsDirections));
647
649 }
650 }
651 }
652
653 // Buffer to mix in source channels and calculate the normalization coefficients
655
656 // Initialize buffer with virtual source weights,
657 // wich is constant for panning SourceLayoutType
658 {
660
662
663 // Compute new gains for each source channel
665 {
667 auto gains = outChannelMixMap.subspan(mixMapOffset, mNumChannels);
668
669 static constexpr bool bAccumulatePow = true;
671
672 // Accumulate output channel gain squares
673 for (uint32 i = 0; i < mixBuffer.size(); ++i)
674 {
675 mixBuffer[i] += gains[i];
676 }
677 }
678 }
679
680 // Normalize for the number of input channels
681 {
683
685
686 const auto numInChannels = static_cast<float>(sourceLayout.ChannelGroups.size());
687
688 // Calculate normalization coefficients per output channel
689 for (uint32 i = 0; i < mixBuffer.size(); ++i)
690 {
691 const float g = mixBuffer[i];
692 normCoeffs[i] = g > 0.0f ? Math::InvSqrt(g * numInChannels) : 0.0f;
693 }
694
695 // Pre-normalize source out channel gains
697 {
698 auto gains = outChannelMixMap.subspan(channelOffset, mNumChannels);
699 for (uint32 i = 0; i < gains.size(); ++i)
700 {
701 gains[i] *= normCoeffs[i];
702 }
703 }
704
705 // ..now the input channels can be mixed into the output channels with simple add
706
707 // This is equivalent to the method above, only multiplying once per output channel during mixing.
708 //
709 // We could instead of backing in normalization into source channel gains groups,
710 // give the user the output gains coefficients to multiply when mixing.
711 /*for (uint32 i = 0; i < mixBuffer.size(); ++i)
712 {
713 float& g = mixBuffer[i];
714 float& c = normCoeffs[i];
715 g *= c;
716 }*/
717 }
718 }
719
720 template<class PannerType, class Traits>
722 std::span<float> outGains) const
723 {
724 JPL_ASSERT(outGains.size() <= GetNumChannels());
725
726 static constexpr bool bAccumulatePow = false;
728
729 // Public ProcessVirtualSources is for the end use in mixing,
730 // so has to be normalized.
732 }
733
734 template<class PannerType, class Traits>
735 template<bool bAccumulatePow>
736 inline void VBAPannerBase<PannerType, Traits>::ProcessVirtualSourcesImpl(std::span<const VirtualSource> virtualSources,
737 std::span<float> outGains) const
738 {
739 JPL_ASSERT(outGains.size() <= GetNumChannels());
740
741 using QueryType = typename LUTInterface::QueryType;
742
743 if constexpr (requires{ typename QueryType::VBAPCell; })
744 {
745 // Note: this cannot be parallel for, because the writes to `outGains` can overlap
746 std::ranges::for_each(virtualSources, [&](const VirtualSource& vs)
747 {
748 // Retrieve gains from the LUT
749 typename QueryType::VBAPCell cell;
750
751 LUTInterface::Query(*mLUT).GainsFor(vs.Direction, cell);
752
753 if constexpr (bAccumulatePow)
754 {
755 // Accumulate linear gains
756 outGains[cell.Speakers[0]] += vs.Weight * cell.Gains[0] * cell.Gains[0];
757 outGains[cell.Speakers[1]] += vs.Weight * cell.Gains[1] * cell.Gains[1];
758 outGains[cell.Speakers[2]] += vs.Weight * cell.Gains[2] * cell.Gains[2];
759 }
760 else
761 {
762 // Accumulate weighted linear gains (to be normalized later)
763 outGains[cell.Speakers[0]] += vs.Weight * cell.Gains[0];
764 outGains[cell.Speakers[1]] += vs.Weight * cell.Gains[1];
765 outGains[cell.Speakers[2]] += vs.Weight * cell.Gains[2];
766 }
767 });
768 }
769 else
770 {
771 float buffer[Traits::MAX_CHANNELS]{ float(0.0) };
772 std::span<float> vsSpeakerGains(buffer, outGains.size());
773
774 // Note: this cannot be parallel for, because the writes to `outGains` can overlap
775 std::ranges::for_each(virtualSources, [&](const VirtualSource& vs)
776 {
777 // Retrieve gains from the LUT
778 GetSpeakerGains(vs.Direction, vsSpeakerGains);
779
780 // Accumulate weighted linear gains (to be normalized later)
781 for (uint32 s = 0; s < outGains.size(); ++s)
782 {
783 if constexpr (bAccumulatePow)
784 {
785 outGains[s] += vs.Weight * vsSpeakerGains[s] * vsSpeakerGains[s];
786 }
787 else
788 {
789 outGains[s] += vs.Weight * vsSpeakerGains[s];
790 }
791 }
792 });
793 }
794 }
795
796 template<class PannerType, class Traits>
797 template<bool bAccumulatePow>
798 inline void VBAPannerBase<PannerType, Traits>::ProcessVirtualSourcesImplSIMD(const Vec3SIMDBufferView& vsDirections,
799 std::span<const simd> vsWeights,
800 std::span<float> outGains) const
801 {
803 JPL_ASSERT(outGains.size() <= GetNumChannels());
804
805 using QueryType = typename LUTInterface::QueryType;
806
807 if constexpr (requires{ typename QueryType::VBAPCell; }) // 3D panning (with height channels)
808 {
809 for (uint32 i = 0; i < vsDirections.size(); ++i)
810 {
811 // Retrieve gains from the LUT
812 std::array<uint8, simd::size() * 3> speakers;
813 std::array<float, simd::size() * 3> gains;
814
815 LUTInterface::Query(*mLUT).GainsFor(vsDirections.X[i], vsDirections.Y[i], vsDirections.Z[i], speakers, gains);
816
817 const simd& vsWeight = vsWeights[i];
818
819 for (uint32 s = 0; s < gains.size(); s += simd::size())
820 {
821 float* gp = &gains[s];
822 simd gs(gp);
823
824 if constexpr (bAccumulatePow)
825 {
826 // Accumulate linear gains
827 (vsWeight * gs * gs).store(gp);
828 }
829 else
830 {
831 // Accumulate weighted linear gains (to be normalized later)
832 (vsWeight * gs).store(gp);
833 }
834 }
835
836 for (uint32 s = 0; s < gains.size(); ++s)
837 {
838 outGains[speakers[s]] += gains[s];
839 }
840 }
841 }
842 else // 2D panning (without height channels)
843 {
845
846 for (uint32 i = 0; i < vsDirections.size(); ++i)
847 {
848 GetSpeakerGains(vsDirections.X[i], vsDirections.Y[i], vsDirections.Z[i], vsSpeakerGains);
849
850 const simd& vsWeight = vsWeights[i];
851
852 // Accumulate weighted linear gains (to be normalized later)
853 for (uint32 s = 0; s < outGains.size(); ++s)
854 {
855 if constexpr (bAccumulatePow)
856 {
857 outGains[s] += (vsWeight * vsSpeakerGains[s] * vsSpeakerGains[s]).reduce();
858 }
859 else
860 {
861 outGains[s] += (vsWeight * vsSpeakerGains[s]).reduce();
862 }
863 }
864 }
865 }
866 }
867
868 template<class PannerType, class Traits>
870 {
871 const float sum = Accumulate(virtualSources, 0.0f, [](float acc, const VirtualSource& vs)
872 {
873 return acc += Math::Abs(vs.Weight);
874 });
875
876 const float invSum = sum == 0.0f ? 0.0f : 1.0f / sum;
877
878 std::ranges::for_each(virtualSources, [invSum](VirtualSource& vs)
879 {
880 vs.Weight *= invSum;
881 });
882 }
883
884 template<class PannerType, class Traits>
886 {
887 for (uint32 i = 0; i < virtualSources.size(); ++i)
888 {
890 }
891 }
892
893 template<class PannerType, class Traits>
894 JPL_INLINE void VBAPannerBase<PannerType, Traits>::SlerpChannelCap(Vec3SIMDBufferView& virtualSources, const Vec3Type& targetDirection, float t)
895 {
896 const Vec3Pack target(targetDirection);
897
898 for (uint32 i = 0; i < virtualSources.size(); ++i)
899 {
900 // This copy doesn't mater for performance,
901 // if we use something like a reference wrapper
902 // the resulting performance is no different
905 virtualSources.X[i] = inOutVs.X;
906 virtualSources.Y[i] = inOutVs.Y;
907 virtualSources.Z[i] = inOutVs.Z;
908 }
909 }
910
911 template<class PannerType, class Traits>
913 {
914#if 0 // !defined(JPL_TEST)
915 if (channelMap.HasLFE())
916 {
917 JPL_INFO_TAG("VBAP Panner", "Requested source channel map with LFE, LFE is going to be skipped in the resulting channel mix map "
918 "(e.g. 5.1 source channel set going to have 5 channels, not 6).");
919 }
920#endif
921
923
924 // Cache normalization factors for later
926
927 const uint32 numChannels = channelMap.GetNumChannels() - channelMap.HasLFE();
928
929 // Source channel map is unsorted, we need to sort it
930 // to make our lives easier
933
934 // Width of a singe source channel in radians
935 const float channelWidth = JPL_TWO_PI / static_cast<float>(numChannels);
936
937 // If we don't have center channel, we need to offset channel groups
939 ? 0.5f * channelWidth
940 : 0.0f;
941
942 // Create channel groups for source channels
943 ChannelGroups.clear();
945
946 static const Vec3Type cUP(0.0f, 1.0f, 0.0f);
947
948 // 1. Find equal source channel positions for 100% spread
949 // 2. Lay out virtual sources for source channel group evenly withing its equal section
950 for (uint32 i = 0; i < sourceChannelsSorted.size(); ++i)
951 {
952 // Assign a centre angle of the next equal section of the source plane
954
955 // Create channel group for each source channel
957 channelGroup.Rotation = Math::QuatRotation(cUP, -channelAngle); // right-handed rotation
958 channelGroup.Channel = sourceChannelsSorted[i].ChannelId;
959
960 /*
961 To handle LFE:
962 - channelGroup.Channel - is the index of the source channel
963 - in the audio process block, instead of iterating actual inputs of thes block, iterate VBAP channel groups
964 - if LFE pressent, simply copy input to output, applying other parameters, like distance attenuation, but skipping VBAP channel group
965
966 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
967 */
968 }
969
970 // Now that we have assigned the angels, we need to sort
971 // by the usual order of the channels in an audio block to make our lives easier later
972 std::ranges::sort(ChannelGroups, [](const ChannelGroup& lhs, const ChannelGroup& rhs) { return lhs.Channel < rhs.Channel; });
973
974 return true;
975 }
976
977} // namespace JPL
978
979#undef JPL_SCOPE_TIME
#define JPL_ASSERT(inExpression,...)
Main assert macro, usage: JPL_ASSERT(condition, message) or JPL_ASSERT(condition)
Definition ErrorReporting.h:76
#define JPL_ENSURE(inExpression,...)
Define ENSURE.
Definition ErrorReporting.h:90
#define JPL_ERROR_TAG(tag, message)
Definition ErrorReporting.h:124
#define JPL_INFO_TAG(tag, message)
Definition ErrorReporting.h:108
#define JPL_SCOPE_TIME(outFloat)
Definition PannerBase.h:66
Definition ChannelMap.h:150
constexpr bool IsValid() const noexcept
Definition ChannelMap.h:160
Definition PannerBase.h:227
JPL_INLINE bool InitializeSourceLayout(ChannelMap channelMap, SourceLayoutType &outLayout) const
Create/initialize SourceLayout for given source channelMap
Definition PannerBase.h:425
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:721
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:431
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:476
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
bool InitializeLUT(ChannelMap channelMap)
Definition PannerBase.h:445
PannerType PanType
Definition PannerBase.h:300
JPL_INLINE uint32 GetNumChannels() const noexcept
Definition PannerBase.h:341
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:869
JPL_INLINE float GetShortestSpeakerAperture() const noexcept
Definition PannerBase.h:348
JPL_INLINE ChannelMap GetChannelMap() const noexcept
Get the channel map the panner is initialized to.
Definition PannerBase.h:344
JPL_INLINE bool IsInitialized() const noexcept
Definition PannerBase.h:336
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
JPL_INLINE std::optional< const char * > IsValidSourceChannelMap(ChannelMap channelMap)
Definition PannerBase.h:193
std::pmr::memory_resource * GetDefaultMemoryResource() noexcept
Definition Memory.h:42
Vec3BufferView< simd > Vec3SIMDBufferView
View into a Vec3-like SoA buffer, holding separate arrays of Vec3 components X, Y,...
Definition Vec3Buffer.h:56
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
std::uint8_t uint8
Definition Core.h:309
JPL_INLINE simd min(const simd &a, const simd &b) noexcept
Element-wise min.
Definition SIMD.h:1813
JPL_INLINE float GetSpreadFromSourceSize(float sourceSize, float distance)
Definition PannerBase.h:69
Orthonormal basis (column-major)
Definition MinimalBasis.h:35
Minimum data required to do our orientation math.
Definition Position.h:32
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:255
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:912
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
Definition Vec3Buffer.h:66
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