JPL Spatial
Sound spatialization and propagation library
Loading...
Searching...
No Matches
PanningService.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
30
31#include <algorithm>
32#include <concepts>
33#include <span>
34#include <memory>
35#include <vector>
36#include <initializer_list>
37#include <utility>
38#include <memory_resource>
39
40namespace JPL
41{
42 using StandardPanner = StandardPanner2D; // TODO: integrate 3D panning into PanningService
46
49
51 {
54
55 [[nodiscard]] JPL_INLINE constexpr bool operator==(const PanningCacheKey& other) const
56 {
57 return Handle == other.Handle && TargetChannelMap == other.TargetChannelMap;
58 }
59 };
60
67
68 [[nodiscard]] JPL_INLINE constexpr bool operator==(const SourceLayoutKey& lhs, const SourceLayoutKey& rhs)
69 {
70 return lhs.SourceMap == rhs.SourceMap && lhs.TargetMap == rhs.TargetMap;
71 }
72
73
74} // namespace JPL
75
76namespace std
77{
78 static_assert(sizeof(size_t) >= sizeof(uint64_t));
79 static_assert(std::convertible_to<uint64_t, size_t>);
80
81 template <>
82 struct hash<JPL::SourceLayoutKey>
83 {
84 size_t operator()(const JPL::SourceLayoutKey& key) const
85 {
86 return (static_cast<uint64_t>(key.SourceMap.GetChannelMask()) << 32) | key.TargetMap.GetChannelMask();
87 }
88 };
89
90 template <>
91 struct hash<JPL::PanningCacheKey>
92 {
93 size_t operator()(const JPL::PanningCacheKey& key) const
94 {
95 // Sanity check that handle is some kind of 32-bit int that we can just bit-shift pack
96 static_assert(sizeof(key.Handle) == 4);
97 return (static_cast<uint64_t>(std::hash<JPL::PanEffectHandle>{}(key.Handle)) << 32) | key.TargetChannelMap.GetChannelMask();
98 }
99 };
100}
101
102namespace JPL
103{
104 //==========================================================================
107 {
108 None,
109 Position,
111 };
112 // TODO: should we store spatialization type with the pan effect parameters,
113 // or keep it elsewhere since it shouldn't be dynamic?
114
116 {
117 float Focus;
118 float Spread;
119 };
120
122 {
123 ChannelMap SourceChannelMap; //< ChannelMap to initialize panning effect for
124 std::span<const ChannelMap> TargetChannelMaps; //< (required) Target ChannelMaps for the panning effect
125
126 PanEffectParameters EffectParameters //< Parameters controlling panning behavior
127 {
128 .Focus = 1.0f,
129 .Spread = 1.0f
130 };
131 };
132
133 //==========================================================================
134 template<class VBAPTraits>
136 {
137 public:
138 // Alias to override allocator for the internal FlatMap we use
139 template<class Key, class T>
141
142 using Vec3Type = typename VBAPTraits::Vec3Type;
143 using PannerTraits = VBAPTraits;
144
149
150 public:
151 PanningService() = default;
152
155
156 // High level API to:
157 // - create panners for different kinds of channel maps and panning settings
158 // - get channel gains for sources
159 // ...
160
163 JPL_INLINE const PannerType* CreatePannerFor(ChannelMap targetChannelMap);
164
166 JPL_INLINE const PannerType* GetPannerFor(ChannelMap targetChannelMap) const;
167
168 //==========================================================================
170
177
180 JPL_INLINE bool ReleasePanningEffect(PanEffectHandle source);
181
182 private:
186 JPL_INLINE bool AddSourceTargets(PanEffectHandle source, std::span<const ChannelMap> targetChannelMaps);
187 JPL_INLINE bool AddSourceTargets(PanEffectHandle source, std::initializer_list<const ChannelMap> targetChannelMaps);
188 public:
189
192 JPL_INLINE bool SetPanningEffectParameters(PanEffectHandle effect, const PanEffectParameters& parameters);
193
196 JPL_INLINE bool SetPanningEffectSpread(PanEffectHandle effect, float spread);
197
203 JPL_INLINE bool EvaluateDirection(PanEffectHandle source, const Position<Vec3Type>& position, ESpatializationType spatialziationType);
204
208 JPL_INLINE std::span<const float> GetChannelGainsFor(PanEffectHandle source, ChannelMap targetChannelMap) const;
209
210 //==========================================================================
214
215 // Create panning data for channel map and optionally associate source with it.
216 // This can be used to initialize panning data for known channel maps,
217 // as well as to associate that data with sources, since the data only created if
218 // it doesn't exist yet in the cache.
219 // @returns panning data created for the channel map, which can be nullptr if failed to initialize
220 JPL_INLINE std::shared_ptr<const SourceLayout> CreatePanningDataFor(SourceLayoutKey layout, PanEffectHandle source = {});
221
222 // @returns panning data for requrested channel map if it was previously created
223 // by calling CreatePanningDataFor
224 JPL_INLINE std::shared_ptr<const SourceLayout> GetPanningDataFor(SourceLayoutKey layout) const;
225
226 // Initialize channel gains for source channel map.
227 // Returned gains must be managed by the user.
228 // This overload is meant for the case when using Panners directly.
229 JPL_INLINE bool CreateChannelGainsFor(ChannelMap targetChannelMap, ChannelMap sourceChannelMap, std::pmr::vector<float>& outChannelGains) const;
230
231 // Initialize channel gains for source. This is called internally in AddSourceTargets function.
232 // The gains are manaded by PanningService and used to cache results of the last call to EvaluateDirection
233 JPL_INLINE bool CreateChannelGainsFor(ChannelMap targetChannelMap, PanEffectHandle source);
234
235 // @returns SourceLayout associated with the source if exists
236 JPL_INLINE std::shared_ptr<const SourceLayout> GetSourceLayoutFor(PanEffectHandle source) const;
237
238 private:
239 // TODO: do we need to clear these at any point?
240
244
245 //std::unordered_map<ChannelMap, std::shared_ptr<SourceLayout>> mInitializedSourceLayouts;
246 // TODO: maybe we could use number of channels everywhere instead of ChannelMap?
247 // In that case we could use simple static arrays here instead of maps
249 // TODO: we should probably delete unused/unreferenced source layouts at some point, right?
250
251 // Static association VBAPs with handles
253
254 // Dynamic parameters that can be changed at runtime
256
257 // Per target map - store channel mix map
258 // vector size = number of source channels * number of target channels
260 };
261} // namespace JPL
262
263
264
265//==============================================================================
266//
267// Code beyond this point is implementation detail...
268//
269//==============================================================================
270namespace JPL
271{
272 template<class VBAPTraits>
273 JPL_INLINE auto PanningService<VBAPTraits>::CreatePannerFor(ChannelMap targetChannelMap) -> const PannerType*
274 {
275 if (!targetChannelMap.IsValid())
276 return nullptr;
277
278 auto& panner = mPanners[targetChannelMap];
279 if (!panner.IsInitialized())
280 {
281 if (!panner.Initialize(targetChannelMap))
282 {
283 mPanners.erase(targetChannelMap);
284 return nullptr;
285 }
286 }
287 return &panner;
288 }
289
290 template<class VBAPTraits>
291 JPL_INLINE auto PanningService<VBAPTraits>::GetPannerFor(ChannelMap targetChannelMap) const -> const PannerType*
292 {
293 auto it = mPanners.find(targetChannelMap);
294 return it != mPanners.end() ? &(it->second) : nullptr;
295 }
296
297 template<class VBAPTraits>
299 -> std::shared_ptr<const SourceLayout>
300 {
301 if (!layout.SourceMap.IsValid())
302 return nullptr;
303
304 auto& sourceLayout = mInitializedSourceLayouts[layout];
305
306 if (!sourceLayout)
307 {
308 sourceLayout = make_pmr_shared<SourceLayout>();
309
310 // We have to initialize source for a specific target output channel layout
311 const auto* panner = CreatePannerFor(layout.TargetMap);
312
313 if (!panner || !panner->InitializeSourceLayout(layout.SourceMap, *sourceLayout))
314 {
315 JPL_ERROR_TAG("PanningService", "Failed to initialize SourceLayout");
316 reset_pmr_shared(sourceLayout);
317 mInitializedSourceLayouts.erase(layout);
318 }
319 }
320
321 if (source.IsValid() && sourceLayout)
322 mSourceLayouts[source] = sourceLayout;
323
324 return sourceLayout;
325 }
326
327 template<class VBAPTraits>
328 JPL_INLINE auto PanningService<VBAPTraits>::GetPanningDataFor(SourceLayoutKey layout) const -> std::shared_ptr<const SourceLayout>
329 {
330 auto sourceLayout = mInitializedSourceLayouts.find(layout);
331 if (sourceLayout != mInitializedSourceLayouts.end())
332 return sourceLayout->second;
333
334 return nullptr;
335 }
336
337 template<class VBAPTraits>
339 ChannelMap sourceChannelMap,
340 std::pmr::vector<float>& outChannelGains) const
341 {
342 if (!targetChannelMap.IsValid() || !sourceChannelMap.IsValid())
343 return false;
344
345 outChannelGains.resize(sourceChannelMap.GetNumChannels() * targetChannelMap.GetNumChannels());
346 std::ranges::fill(outChannelGains, 0.0f);
347
348 return true;
349 }
350
351 template<class VBAPTraits>
353 {
354 if (!targetChannelMap.IsValid() || !source.IsValid() || !mSourceLayouts.contains(source))
355 return false;
356
357 // TODO: do we want to ensure existance of VBAP association for this source?
358 // We could also try decoupling source channel map from source, and allow multiple source channel maps per source (?)
359
360 // Ensure we have gain arrays for each channel of the source
361 auto& channelGains = mPanningCache[
363 .Handle = source,
364 .TargetChannelMap = targetChannelMap
365 }];
366
367 channelGains.resize(mSourceLayouts[source]->ChannelGroups.size() * targetChannelMap.GetNumChannels());
368 std::ranges::fill(channelGains, 0.0f);
369
370 return true;
371 }
372
373 template<class VBAPTraits>
374 JPL_INLINE auto PanningService<VBAPTraits>::GetChannelGainsFor(PanEffectHandle source, ChannelMap targetChannelMap) const
375 -> std::span<const float>
376 {
377 auto cache = mPanningCache.find(
379 .Handle = source,
380 .TargetChannelMap = targetChannelMap
381 });
382
383 if (cache != mPanningCache.end())
384 return cache->second;
385
386 return {};
387 }
388
389 template<class VBAPTraits>
391 {
392 if (!initParameters.SourceChannelMap.IsValid())
393 return {};
394
395 if (initParameters.TargetChannelMaps.empty())
396 return {};
397
398 const auto newHandle = PanEffectHandle::New();
399
400 // Create panning data for each source->target pair requirested for this source
401 for (const ChannelMap& targetMap : initParameters.TargetChannelMaps)
402 {
403 std::shared_ptr<const SourceLayout> panningData =
404 CreatePanningDataFor(
406 .SourceMap = initParameters.SourceChannelMap,
407 .TargetMap = targetMap
408 }, newHandle);
409
410 if (!panningData)
411 {
412 ReleasePanningEffect(newHandle);
413 return {};
414 }
415 }
416
417 if (AddSourceTargets(newHandle, initParameters.TargetChannelMaps))
418 {
419 mPanningParams[newHandle] = initParameters.EffectParameters;
420
421 return newHandle;
422 }
423 else
424 {
425 // If we failed to initialize taret data,
426 // we need to clear other data that might have been initialized
427 ReleasePanningEffect(newHandle);
428 }
429
430 return {};
431 }
432
433 template<class VBAPTraits>
435 {
436 return mSourceLayouts.erase(source)
437 + mPanningParams.erase(source)
438 + mPanningCache.erase_if([source](const std::pair<const PanningCacheKey, std::pmr::vector<float>>& pair)
439 {
440 return pair.first.Handle == source;
441 });
442 }
443
444 template<class VBAPTraits>
445 JPL_INLINE bool PanningService<VBAPTraits>::AddSourceTargets(PanEffectHandle source, std::span<const ChannelMap> targetChannelMaps)
446 {
447 if (!source.IsValid())
448 return false;
449
450 if (!mSourceLayouts.contains(source))
451 {
452 JPL_ERROR_TAG("PanningService", "AddSoruceTargets failed because source handle wasn't initialized before adding source targets. "
453 "This is likely because the provided source handle was released previously and is no longer valid.");
454 return false;
455 }
456
457 if (targetChannelMaps.empty())
458 return true;
459
460 bool hasAnyTargetInitialized = false;
461
462 for (ChannelMap channelMap : targetChannelMaps)
463 {
464 hasAnyTargetInitialized |= CreateChannelGainsFor(channelMap, source);
465 }
466
467 return hasAnyTargetInitialized;
468 }
469
470 template<class VBAPTraits>
471 JPL_INLINE bool PanningService<VBAPTraits>::AddSourceTargets(PanEffectHandle source, std::initializer_list<const ChannelMap> targetChannelMaps)
472 {
473 return AddSourceTargets(source, std::span(targetChannelMaps));
474 }
475
476 namespace Impl
477 {
478 // If the source direction is pointing directly up, bias towards forward
479 template<CVec3 Vec3Type>
480 JPL_INLINE Vec3Type SanitizeSourceDir(const Vec3Type sourceDirection)
481 {
482 using Float = Vec3FloatType<Vec3Type>;
483
484 // TODO: This is a substantial offset, maybe we could revise some of the rotaion math to fall back to forward without needing this
485 static const Vec3Type fallbackDir = Normalized(Vec3Type(0, 1, -static_cast<Float>(5e-2))); // bias towards 2D forward (3D Z -> 2D Y axis)
486
487 Vec3Type sanitizedDirection = Math::IsNearlyEqual(
488 Math::Abs(GetY(sourceDirection)),
489 static_cast<Float>(1.0))
490 ? fallbackDir
491 : sourceDirection;
492
493 SetY(sanitizedDirection, std::copysign(GetY(sanitizedDirection), GetY(sourceDirection))); // We need to preserve the sign
494 return sanitizedDirection;
495 }
496 }
497
498 template<class VBAPTraits>
500 {
501 if (!source.IsValid())
502 return false;
503
504 // Assert Location is a normalized relative unit length direction vector
505 JPL_ASSERT(Math::IsNearlyEqual(LengthSquared(position.Location), 1.0f));
506
507 for (auto&& [key, gains] : mPanningCache)
508 {
509 if (key.Handle == source && !gains.empty())
510 {
511 JPL_ASSERT(mSourceLayouts.contains(key.Handle));
512 JPL_ASSERT(mPanners.contains(key.TargetChannelMap));
513
514 const auto& sourceLayout = mSourceLayouts[key.Handle];
515 const auto& params = mPanningParams[key.Handle];
516
517 JPL_ASSERT(sourceLayout);
518
519 // TODO: move this switch statement outside of the loop
520 switch (spatialziationType)
521 {
523 {
524 // TODO: we need to still do basic direct gain assignment
525 JPL_ASSERT(false, "Not implemented.");
526 }
527 break;
529 {
530 const PanData updateData
531 {
532 .SourceDirection = Impl::SanitizeSourceDir(position.Location), // location is direction
533 .Focus = params.Focus,
534 .Spread = params.Spread,
535 };
536 mPanners[key.TargetChannelMap].ProcessVBAPData(*sourceLayout, updateData, gains);
537 }
538 break;
540 {
541 const PanDataWithOrientation updateData
542 {
543 .Pan = {
544 .SourceDirection = Impl::SanitizeSourceDir(position.Location),
545 .Focus = params.Focus,
546 .Spread = params.Spread
547 },
548 .Orientation = position.Orientation // TODO: orientation is actually up and forward
549 };
550 mPanners[key.TargetChannelMap].ProcessVBAPData(*sourceLayout, updateData, gains);
551 }
552 break;
553 default:
554 JPL_ASSERT(false, "Should be unreachable");
555 break;
556 }
557 }
558 }
559
560 return true;
561 }
562
563 template<class VBAPTraits>
564 JPL_INLINE auto PanningService<VBAPTraits>::GetSourceLayoutFor(PanEffectHandle source) const -> std::shared_ptr<const SourceLayout>
565 {
566 auto data = mSourceLayouts.find(source);
567 return data == mSourceLayouts.end() ? nullptr : data->second;
568 }
569
570 template<class VBAPTraits>
572 {
573 if (!effect.IsValid())
574 return false;
575
576 mPanningParams[effect] = parameters;
577 return true;
578 }
579
580 template<class VBAPTraits>
582 {
583 if (!effect.IsValid())
584 return false;
585
586 mPanningParams[effect].Spread = spread;
587 return true;
588 }
589
590} // namespace JPL
#define JPL_ASSERT(inExpression,...)
Main assert macro, usage: JPL_ASSERT(condition, message) or JPL_ASSERT(condition)
Definition ErrorReporting.h:80
#define JPL_ERROR_TAG(tag, message)
Definition ErrorReporting.h:128
Definition ChannelMap.h:154
constexpr uint32 GetNumChannels() const noexcept
Definition ChannelMap.h:166
constexpr bool IsValid() const noexcept
Definition ChannelMap.h:164
constexpr uint32 GetChannelMask() const
Definition ChannelMap.h:198
Definition FlatMap.h:51
Definition PanningService.h:136
JPL_INLINE std::shared_ptr< const SourceLayout > GetPanningDataFor(SourceLayoutKey layout) const
Definition PanningService.h:328
JPL_INLINE const PannerType * CreatePannerFor(ChannelMap targetChannelMap)
Definition PanningService.h:273
JPL_INLINE bool CreateChannelGainsFor(ChannelMap targetChannelMap, ChannelMap sourceChannelMap, std::pmr::vector< float > &outChannelGains) const
Definition PanningService.h:338
JPL_INLINE bool CreateChannelGainsFor(ChannelMap targetChannelMap, PanEffectHandle source)
Definition PanningService.h:352
JPL_INLINE bool EvaluateDirection(PanEffectHandle source, const Position< Vec3Type > &position, ESpatializationType spatialziationType)
Definition PanningService.h:499
typename PannerType::SourceLayoutType SourceLayout
Definition PanningService.h:146
typename PannerType::PanUpdateData PanData
Definition PanningService.h:147
typename VBAPTraits::Vec3Type Vec3Type
Definition PanningService.h:142
JPL_INLINE std::span< const float > GetChannelGainsFor(PanEffectHandle source, ChannelMap targetChannelMap) const
Definition PanningService.h:374
JPL_INLINE bool SetPanningEffectSpread(PanEffectHandle effect, float spread)
Definition PanningService.h:581
PanningService()=default
PanningService & operator=(const PanningService &)=delete
VBAPTraits PannerTraits
Definition PanningService.h:143
JPL_INLINE bool SetPanningEffectParameters(PanEffectHandle effect, const PanEffectParameters &parameters)
Definition PanningService.h:571
typename PannerType::PanUpdateDataWithOrientation PanDataWithOrientation
Definition PanningService.h:148
JPL_INLINE const PannerType * GetPannerFor(ChannelMap targetChannelMap) const
Definition PanningService.h:291
JPL_INLINE PanEffectHandle InitializePanningEffect(const PanEffectInitParameters &initParameters)
High level API.
Definition PanningService.h:390
PanningService(const PanningService &)=delete
JPL_INLINE std::shared_ptr< const SourceLayout > GetSourceLayoutFor(PanEffectHandle source) const
Definition PanningService.h:564
JPL_INLINE std::shared_ptr< const SourceLayout > CreatePanningDataFor(SourceLayoutKey layout, PanEffectHandle source={})
Definition PanningService.h:298
JPL_INLINE bool ReleasePanningEffect(PanEffectHandle source)
Definition PanningService.h:434
FlatMapWithAllocator< Key, T, std::pmr::polymorphic_allocator > FlatMapType
Definition PanningService.h:140
Definition PannerBase.h:227
typename PanType::SourceLayout SourceLayoutType
Definition PannerBase.h:303
JPL_INLINE Vec3Type SanitizeSourceDir(const Vec3Type sourceDirection)
Definition PanningService.h:480
JPL_INLINE constexpr bool IsNearlyEqual(T a, T b, T tolerance=JPL_FLOAT_EPS_V< T >) noexcept
Definition Math.h:152
JPL_INLINE constexpr auto Abs(const T &value) noexcept
Standard abs is not constexpr in C++20.
Definition Math.h:87
Definition AcousticMaterial.h:36
std::remove_cvref_t< decltype(Vec3Access< Vec3Type >::GetX(std::declval< Vec3Type >()))> Vec3FloatType
Definition Vec3Traits.h:33
@ None
Definition DistanceAttenuation.h:34
JPL_INLINE void reset_pmr_shared(std::shared_ptr< T > &sharedPtr, Y *ptr)
Definition Memory.h:221
JPL_INLINE void SetY(Vec3Type &v, Vec3FloatType< Vec3Type > value) noexcept
Definition Vec3Traits.h:40
JPL_INLINE std::shared_ptr< T > make_pmr_shared(Args &&... args)
std::make_shared wrapper using our global memory resource
Definition Memory.h:206
typename StandardPanner::PanUpdateDataWithOrientation StandardPanDataWithOrientation
Definition PanningService.h:45
std::pmr::memory_resource * GetDefaultMemoryResource() noexcept
Definition Memory.h:42
ESpatializationType
Determines spatialized source channel oritentation.
Definition PanningService.h:107
JPL_INLINE constexpr bool operator==(const Vec2 &A, const Vec2 &B) noexcept
Definition MinimalVec2.h:59
IDType< PanningServiceIDTag > PanEffectHandle
Definition PanningService.h:48
JPL_INLINE auto GetY(const Vec3Type &v) noexcept
Definition Vec3Traits.h:36
typename StandardPanner::SourceLayoutType StandardSourceLayout
Definition PanningService.h:43
typename StandardPanner::PanUpdateData StandardPanData
Definition PanningService.h:44
VBAPanner2D<> StandardPanner2D
Alias for 2D panner that doesn't customize any traits.
Definition VBAPanning2D.h:59
Definition ChannelMap.h:272
static constexpr IDType New() noexcept
Definition IDType.h:44
constexpr bool IsValid() const noexcept
Definition IDType.h:46
Definition PanningService.h:122
PanEffectParameters EffectParameters
Definition PanningService.h:127
ChannelMap SourceChannelMap
Definition PanningService.h:123
std::span< const ChannelMap > TargetChannelMaps
Definition PanningService.h:124
Definition PanningService.h:116
float Spread
Definition PanningService.h:118
float Focus
Definition PanningService.h:117
Definition PanningService.h:51
JPL_INLINE constexpr bool operator==(const PanningCacheKey &other) const
Definition PanningService.h:55
ChannelMap TargetChannelMap
Definition PanningService.h:53
PanEffectHandle Handle
Definition PanningService.h:52
Definition PanningService.h:47
Location and orientation in one place.
Definition Position.h:80
OrientationData< Vec3Type > Orientation
Definition Position.h:82
Vec3Type Location
Definition Position.h:81
Definition PanningService.h:62
ChannelMap SourceMap
Definition PanningService.h:63
ChannelMap TargetMap
Definition PanningService.h:64
Parameters to update VBAP data for a source.
Definition PannerBase.h:238
size_t operator()(const JPL::PanningCacheKey &key) const
Definition PanningService.h:93
size_t operator()(const JPL::SourceLayoutKey &key) const
Definition PanningService.h:84